Power Platform QA Solution
Power Platform QA
Home
Solutions
Reports
Settings
User Guide
Back to Analyzers
Configure Analyzer —
General Code Quality Analyzer
CUSTOM
Metadata
ID
general-code-quality-analyzer
Result Key
codeQualityIssues
File
general-code-quality-analyzer.js
Built-in
No
Analyzer Classification
Category
— This determines the type of Analyzer you are building. Categories can be set up under settings.
Uncategorized
Performance
Data Operations
Variables & Scoping
Maintenance
Test sets
— Include this analyzer in predefined runs. Select one or more test sets this analyzer should belong to.
Dataverse test set
Some tests
Description
Analyzes Power Apps controls for common code quality issues including empty handlers, hardcoded values, and suspicious patterns.
Prompt
Provider
Anthropic (Claude) — no key
OpenAI (GPT) — no key
Google (Gemini) — no key
Re-run AI builder
dsdthftu
Source Code
general-code-quality-analyzer.js ·
Input data reference ↗
Checking…
Save JavaScript
export default { name: "General Code Quality Analyzer", description: "Analyzes Power Apps controls for common code quality issues including empty handlers, hardcoded values, and suspicious patterns.", resultKey: "codeQualityIssues", resultSchema: { keys: [ { key: "name", label: "Control / Item", suggestedFormat: "name-copy" }, { key: "type", label: "Issue Type", suggestedFormat: "badge-info" }, { key: "message", label: "Description", suggestedFormat: "truncate:120" }, { key: "confidence", label: "Confidence", suggestedFormat: "badge-confidence" }, { key: "locations", label: "Locations", suggestedFormat: "locations" } ] }, analyze(controlTree, refGraph, extraction) { const results = []; // Helper: check if a formula is effectively empty or a no-op function isEmptyOrNoop(formula) { if (!formula) return true; const trimmed = formula.trim(); if (trimmed === "") return true; if (trimmed === "false" || trimmed === "true") return false; // Common no-op patterns const noops = ["false", "//", "/*"]; if (trimmed === "") return true; return false; } // Helper: strip comments from formula function stripComments(formula) { // Remove line comments let stripped = formula.replace(/\/\/[^\n]*/g, ""); // Remove block comments stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, ""); return stripped.trim(); } // 1. Detect empty OnSelect handlers on interactive controls const interactiveTypes = new Set([ "button", "icon", "image", "label", "rectangle", "circle", "pentagon", "triangle", "arrow", "htmltext" ]); for (const node of controlTree.allNodes) { if (node.isApp || node.isScreen || node.isComponent) continue; const baseTypeLower = (node.baseType || node.type || "").toLowerCase(); const onSelect = node.formulas.get("OnSelect"); if (onSelect !== undefined) { const stripped = stripComments(onSelect); if (!stripped || stripped === "" || stripped === "false") { results.push({ name: node.name, type: "empty-onselect", message: `Control "${node.name}" has an empty or no-op OnSelect handler. If the control is interactive, this may confuse users or indicate missing logic.`, confidence: "medium", locations: [{ control: node.name, property: "OnSelect", file: node.filePath, snippet: onSelect }] }); } } } // 2. Detect hardcoded color values (RGBA with explicit non-transparent values) const hardcodedColorRegex = /RGBA\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[01](\.\d+)?\s*\)/gi; const colorPropsToCheck = ["Fill", "Color", "BorderColor", "HoverFill", "HoverColor", "PressedFill", "PressedColor", "FocusedBorderColor"]; for (const node of controlTree.allNodes) { if (node.isApp) continue; for (const prop of colorPropsToCheck) { const formula = node.formulas.get(prop); if (!formula) continue; // Skip if it references a theme or variable if (/gbl|theme|color\w+|var\w+|loc\w+/i.test(formula)) continue; const matches = formula.match(hardcodedColorRegex); if (matches && matches.length > 0) { results.push({ name: node.name, type: "hardcoded-color", message: `Control "${node.name}" uses hardcoded RGBA color in "${prop}". Consider using theme variables or named color constants for maintainability.`, confidence: "low", locations: [{ control: node.name, property: prop, file: node.filePath, snippet: formula.substring(0, 120) }] }); } } } // 3. Detect controls with deeply nested If/Switch logic (complexity smell) function countNestedIf(formula) { const ifMatches = (formula.match(/\bIf\s*\(/gi) || []).length; const switchMatches = (formula.match(/\bSwitch\s*\(/gi) || []).length; return ifMatches + switchMatches; } for (const entry of extraction.allFormulas) { const { control, property, formula, file } = entry; if (!formula) continue; const count = countNestedIf(formula); if (count >= 4) { results.push({ name: control, type: "complex-formula", message: `Formula in "${control}.${property}" contains ${count} If/Switch branches, indicating high complexity. Consider refactoring with named formulas or helper variables.`, confidence: count >= 6 ? "high" : "medium", locations: [{ control, property, file, snippet: formula.substring(0, 120) }] }); } } // 4. Detect variables written but never read for (const [varName, writes] of extraction.variableWrites.entries()) { if (!refGraph.variablesRead.has(varName)) { const locations = writes.map(w => ({ control: w.control, property: w.property, file: w.file, snippet: w.snippet ? w.snippet.substring(0, 120) : null })); results.push({ name: varName, type: "write-only-variable", message: `Variable "${varName}" is set but never read anywhere in the app. It may be leftover from refactoring or a naming inconsistency.`, confidence: "high", locations }); } } // 5. Detect collections written but never read for (const [colName, writes] of extraction.collectionWrites.entries()) { if (!refGraph.collectionsRead.has(colName)) { const locations = writes.map(w => ({ control: w.control, property: w.property, file: w.file, snippet: w.snippet ? w.snippet.substring(0, 120) : null })); results.push({ name: colName, type: "write-only-collection", message: `Collection "${colName}" is populated but never read. It may be unused or referenced under a different name.`, confidence: "high", locations }); } } // 6. Detect screens that are never navigated to const navigatedScreens = new Set(extraction.navigateRefs.keys()); for (const screen of controlTree.screens) { // Skip if it's the start screen if (controlTree.startScreenFormula && controlTree.startScreenFormula.includes(screen.name)) continue; if (!navigatedScreens.has(screen.name)) { results.push({ name: screen.name, type: "unreachable-screen", message: `Screen "${screen.name}" has no Navigate() calls pointing to it and is not the start screen. It may be unreachable dead UI.`, confidence: "medium", locations: [{ control: screen.name, property: null, file: screen.filePath, snippet: null }] }); } } // 7. Detect named formulas defined but never used for (const [formulaName, def] of extraction.namedFormulaDefs.entries()) { if (!refGraph.namedFormulasRead.has(formulaName)) { results.push({ name: formulaName, type: "unused-named-formula", message: `Named formula "${formulaName}" is defined but never referenced. Remove it or verify the name matches its usage.`, confidence: "high", locations: [{ control: def.control, property: def.property, file: def.file, snippet: null }] }); } } // 8. Detect controls that are referenced via Select() but have no OnSelect for (const [controlName, refs] of extraction.selectRefs.entries()) { const node = controlTree.nodeIndex.get(controlName); if (!node) continue; const onSelect = node.formulas.get("OnSelect"); if (!onSelect || stripComments(onSelect) === "" || stripComments(onSelect) === "false") { const locations = refs.map(r => ({ control: r.control, property: r.property, file: r.file, snippet: r.snippet ? r.snippet.substring(0, 120) : null })); results.push({ name: controlName, type: "select-no-handler", message: `Control "${controlName}" is triggered via Select() but has an empty or missing OnSelect handler.`, confidence: "high", locations }); } } // 9. Detect suspiciously long single-line formulas (possible readability issue) for (const entry of extraction.allFormulas) { const { control, property, formula, file } = entry; if (!formula) continue; // Check for very long lines without newlines const lines = formula.split("\n"); for (const line of lines) { if (line.trim().length > 500) { results.push({ name: control, type: "long-formula-line", message: `Formula in "${control}.${property}" has a line exceeding 500 characters. Consider breaking it up for readability.`, confidence: "low", locations: [{ control, property, file, snippet: line.trim().substring(0, 120) + "..." }] }); break; // Only report once per formula } } } // 10. Detect Reset() calls targeting controls that have no Default property set for (const [controlName, refs] of extraction.resetRefs.entries()) { const node = controlTree.nodeIndex.get(controlName); if (!node) continue; const hasDefault = node.formulas.has("Default") || node.formulas.has("DefaultDate") || node.formulas.has("DefaultSelectedItems"); if (!hasDefault) { const locations = refs.map(r => ({ control: r.control, property: r.property, file: r.file, snippet: r.snippet ? r.snippet.substring(0, 120) : null })); results.push({ name: controlName, type: "reset-no-default", message: `Control "${controlName}" is reset via Reset() but has no Default/DefaultDate/DefaultSelectedItems formula. The reset target value may be unintentional.`, confidence: "low", locations }); } } return results; } };
Report Output Columns
Columns shown in the generated report. Toggle off to hide; pick a declared key or use "Custom…" for dot-paths like
locations.0.file
.
+ Add column
Enabled
Key
Label
Format
Width
name
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
type
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
message
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
confidence
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
locations
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
Cancel
Test analyzer
Save Columns
Running analyzer against solution…
This can take up to a minute for large solutions.
Test General Code Quality Analyzer
×
Runs this analyzer in isolation against the chosen solution. Nothing is saved — the result is shown here only.
Use an existing solution
No solutions uploaded yet
Upload a solution file
Cancel
Run test
Test results
×