← Back to Create Analyzer

Analyzer Data Reference

Every analyzer's analyze() function receives three arguments: controlTree, refGraph, and extraction. This page documents every property they expose and shows how to use them.

Analyzer Signature

Every analyzer is an ES module whose default export matches this shape:

export default {
  name:        "Human-readable name",
  description: "What this analyzer detects",
  resultKey:   "camelCaseResultKey",

  resultSchema: {
    keys: [
      { key: "name",       label: "Name",       suggestedFormat: "name-copy" },
      { key: "type",       label: "Type",       suggestedFormat: "badge-info" },
      { key: "confidence", label: "Confidence", suggestedFormat: "badge-confidence" },
      { key: "message",    label: "Details",    suggestedFormat: "text-sm" },
      { key: "locations",  label: "Locations",  suggestedFormat: "locations" }
    ]
  },

  analyze(controlTree, refGraph, extraction) {
    const results = [];
    // ...inspect the three arguments and push result objects...
    return results;
  }
};

Always return an array (empty if nothing was found). Each result object should use the result shape described at the bottom of this page.

resultSchema declares every key a result row can have. The config page uses it to populate the key dropdown and to auto-fill column labels and formats. Every row must have exactly the keys listed — no conditional fields. Use null when a key doesn't apply to a specific row. Missing or extra keys are surfaced as warnings on the Test analyzer modal (they do not abort the run). suggestedFormat accepts any value from the Format catalog:

Preview — what the report looks like

The sample below is rendered live with the same function the report uses, driven by two fake result rows for the resultSchema above. Every column uses the format noted on the header; the second row's Locations cell shows the “…and N more” trailer that kicks in past three entries.

Empty OnSelect2 found
NameTypeConfidenceDetailsLocations
btnSubmit.OnSelectempty-onselectHIGHbtnSubmit.OnSelect is empty or a no-op.
btnSubmit > OnSelect > Src/HomeScreen.fx.yaml
OnSelect: =Select(Parent)
lblHint.OnSelectempty-onselectMEDIUMlblHint.OnSelect is set but contains only whitespace.
lblHint > OnSelect > Src/HomeScreen.fx.yaml
OnSelect: =
lblHintAlt > OnSelect > Src/DetailScreen.fx.yaml
OnSelect: =
lblFooter > OnSelect > Src/HomeScreen.fx.yaml
OnSelect: =false
...and 1 more
On this page

controlTree

A unified hierarchy of the entire app: the App node, all screens, all component definitions, and every control inside them. Use this when you need to walk or query the structure of the app.

controlTree.screens
Array of screen nodes (top-level screens that appear in the app). Each element is a control node.
controlTree.components
Array of component definitions (the reusable templates). Component instances placed on screens are separate nodes in allNodes with isComponentInstance === true.
controlTree.allNodes
Flat array containing every node in the tree (App, screens, components, and every nested control). Most analyzers iterate this directly.
controlTree.nodeIndex
Map<controlName, node> for O(1) lookup by name. Use this when you have a control name from a formula and need its node.
controlTree.appNode
The App-level node. Its formulas map holds OnStart, OnError, Formulas (named formula definitions), etc. May be absent on malformed apps — always null-check.
controlTree.startScreenFormula string | null
The formula text of the App's StartScreen property, with the leading = stripped (e.g. If(User().Email = ..., HomeScreen, LoginScreen)). null when no StartScreen is declared.

Example — find all buttons across all screens

const buttons = controlTree.allNodes.filter(
  node => node.baseType === 'Button' || node.baseType === 'ModernButton'
);

Example — flag screens with too many controls

for (const screen of controlTree.screens) {
  const descendants = countDescendants(screen);
  if (descendants > 50) {
    results.push({
      name: screen.name,
      type: 'screen-too-many-controls',
      message: `Screen has ${descendants} controls — consider splitting it.`,
      locations: [{ control: screen.name, property: 'definition', file: screen.filePath }],
      confidence: 'medium'
    });
  }
}

function countDescendants(node) {
  let n = node.children.length;
  for (const child of node.children) n += countDescendants(child);
  return n;
}

Control node shape

Every entry in allNodes, screens, components and nodeIndex is a control node with the following properties:

name string
Unique control name as used in formulas (e.g. btnSubmit).
type string
The full type string as stored in YAML (may include a namespace/variant prefix, e.g. FluentV8/Button).
baseType string
The bare type without namespace/variant (e.g. Button, Gallery, Label). Produced by stripping everything after the first @ or /. Prefer this over type when matching control kinds.
variant string | null
The Variant: value from the YAML, when present (e.g. 'Primary'). null otherwise.
isApp, isScreen, isComponent, isComponentInstance, isLocked boolean
Classification flags. isComponent marks a component definition; isComponentInstance marks a component placed on a screen. isLocked is set on read-only template controls (e.g. gallery row templates).
children Array<node>
Direct children. Walk recursively if you need the full subtree.
parent node | null
Parent node (screen, component definition, or enclosing control). null on the App node, screens, and top-level component definitions.
formulas Map<string, string>
All properties set to a Power Fx formula (strings starting with = in YAML). Keys are property names like OnSelect, Text, Items; values are the formula source with the leading = stripped.
properties Map<string, string>
Non-formula scalar properties (booleans, numbers, literal strings). All values are stringified — a property set to 2 in YAML is stored as the string "2", and true is stored as "true". Parse with parseInt / parseFloat / equality checks as needed.
screen string | null
Name of the screen this node lives on. null for the App node and for component definitions.
filePath string | null
Path of the YAML source file inside the unpacked solution — use this when populating locations[].file. May be null for the App node.
componentName string | null
On component instances, the name of the component definition being instantiated. null everywhere else.
group string | null
The editor group name (Group: in YAML), when the control was placed into a design-time group. null otherwise.
customProperties object
Only present on component definition nodes. The raw CustomProperties block from YAML, keyed by custom property name. Useful for linting a component's public surface.

Example — find controls with an empty OnSelect

for (const node of controlTree.allNodes) {
  if (!node.formulas.has('OnSelect')) continue;
  const onSelect = node.formulas.get('OnSelect').trim();
  if (onSelect === '' || onSelect === 'false' || onSelect === 'Select(Parent)') {
    results.push({
      name: node.name,
      type: 'empty-onselect',
      message: `${node.name}.OnSelect is empty or a no-op`,
      locations: [{ control: node.name, property: 'OnSelect', file: node.filePath }],
      confidence: 'high'
    });
  }
}

refGraph

A cross-reference graph built on top of the control tree and raw extraction. Use it to answer questions like “is this control referenced anywhere?” or “is this variable ever read?”.

refGraph.referencedControls
Map<controlName, Array<{control, property, file, screen, refType, snippet}>> — for each control name, the places it is referenced from. refType is one of 'Select()', 'Reset()', 'dot access', or 'identifier'. Entries are de-duplicated by (control, property, refType). If a control is missing from the map (or its list is empty), nothing in the app references it by name.
refGraph.referencedScreens
Map<screenName, Array<{control, property, file, screen, snippet}>> — screens that are reached via Navigate(). Screens absent from this map and not equal to the start screen are unreachable.
refGraph.variablesRead
Set<string> of variable names that are actually consumed by at least one formula (not just assigned with Set()/UpdateContext()).
refGraph.collectionsRead
Set<string> of collection names that are read. A collection that is Collect()ed but never read is not in this set.
refGraph.namedFormulasRead
Set<string> of named formulas (defined in App.Formulas) that are referenced from at least one other formula.

Example — find unused controls

for (const node of controlTree.allNodes) {
  if (node.isApp || node.isScreen || node.isComponent) continue;

  const refs = refGraph.referencedControls.get(node.name);
  if (refs && refs.length > 0) continue;

  results.push({
    name: node.name,
    type: 'unused-control',
    message: `${node.name} is not referenced by any formula.`,
    locations: [{ control: node.name, property: 'definition', file: node.filePath }],
    confidence: 'medium'
  });
}

Example — variables that are written but never read

for (const [varName, writes] of extraction.variableWrites) {
  if (refGraph.variablesRead.has(varName)) continue;
  results.push({
    name: varName,
    type: 'dead-variable',
    message: `Variable '${varName}' is set but never read.`,
    locations: writes.map(w => ({
      control: w.control, property: w.property, file: w.file, snippet: w.snippet
    })),
    confidence: 'high'
  });
}

extraction

The raw result of parsing every Power Fx formula in the app. Each entry carries enough context (control, property, file, often snippet) to point directly at the offending line — feed these straight into the locations array of a result.

Every location entry below shares the shape { control, property, file, screen, snippet } (except allFormulas, which carries the full formula instead of a snippet, and namedFormulaDefs, which has no snippet). snippet is a short ±20/+40-character window around the matched call, with newlines collapsed and markers where the surrounding formula was trimmed.

extraction.allFormulas
Array<{control, property, file, screen, formula}> — every Power Fx formula found in the app, with the leading = already stripped. Good starting point for text-based scans (e.g. pattern matching across the whole codebase).
extraction.variableWrites
Map<varName, Array<{control, property, file, screen, snippet}>> — every Set() / UpdateContext() call grouped by the variable being written.
extraction.collectionWrites
Map<colName, Array<{control, property, file, screen, snippet}>> — every Collect() / ClearCollect() call grouped by collection name.
extraction.navigateRefs
Map<screenName, Array<{control, property, file, screen, snippet}>> — every Navigate() call grouped by destination screen. Screens missing from this map are unreachable by navigation (unless they are the start screen).
extraction.selectRefs
Map<controlName, Array<{control, property, file, screen, snippet}>> — every Select(ControlName) call grouped by target.
extraction.resetRefs
Same shape as selectRefs but for Reset() calls.
extraction.dotAccessRefs
Same shape — every ControlName.Property dot access (e.g. TextInput1.Text). The strongest signal that a control is actually used.
extraction.namedFormulaDefs
Map<formulaName, {control, property, file, screen}> — named formulas defined in App.Formulas. One location per formula (not an array).
extraction.allIdentifiersInFormulas
Set<string> of every bare identifier that appears in any formula. Useful for “is this name mentioned anywhere?” questions without crafting your own tokenizer.
extraction.knownControlNames
Set<string> of every control name declared in the app. Paired with allIdentifiersInFormulas, this lets you separate formula identifiers that refer to real controls from identifiers that point at variables, collections, or functions.
extraction.knownScreenNames
Set<string> of every screen name declared in the app.

Example — find unreachable screens

const startScreen = (controlTree.startScreenFormula || '').trim();

for (const screen of controlTree.screens) {
  const navigatedTo = extraction.navigateRefs.has(screen.name);
  const isStart     = startScreen.includes(screen.name);
  if (navigatedTo || isStart) continue;

  results.push({
    name: screen.name,
    type: 'unreachable-screen',
    message: `Screen '${screen.name}' is never navigated to and is not the start screen.`,
    locations: [{ control: screen.name, property: 'definition', file: screen.filePath }],
    confidence: 'medium'
  });
}

Example — hardcoded colors

const HEX_COLOR = /\bColor\.(RGBA|FromHex)\s*\(\s*["']?#[0-9a-f]{3,8}/i;

for (const { control, property, formula, file } of extraction.allFormulas) {
  if (!HEX_COLOR.test(formula)) continue;
  results.push({
    name: `${control}.${property}`,
    type: 'hardcoded-color',
    message: `${control}.${property} uses a hardcoded color — consider a theme named formula.`,
    locations: [{ control, property, file }],
    confidence: 'low'
  });
}

Result shape

Each entry your analyze() pushes into the returned array should look like this. The UI uses these fields to render issues, link back to source, and group findings.

{
  name:       "btnSubmit.OnSelect",        // short identifier shown in the report
  type:       "empty-onselect",            // category/slug — group related findings
  message:    "btnSubmit.OnSelect is empty or a no-op",
  locations: [
    {
      control:  "btnSubmit",
      property: "OnSelect",
      file:     "Src/HomeScreen.fx.yaml",
      snippet:  "OnSelect: =Select(Parent)"   // optional
    }
  ],
  confidence: "high"                        // 'high' | 'medium' | 'low' (optional)
}

Tips & common pitfalls