|
| 1 | +# How to Write a Rule |
| 2 | + |
| 3 | +## What You Need to Know Upfront |
| 4 | + |
| 5 | +Your rule only has to implement one method: **check()**. |
| 6 | +Everything else (suppression handling, wildcard “\*”, conversion to RuleResult) is done for you by RuleCommon.execute(). |
| 7 | + |
| 8 | +- You return every violation you find – no manual “if (!suppressions.has(name))” needed in 95% of cases. |
| 9 | +- The base class automatically removes violations whose element.name (or the rule name for flow-level issues) is listed in the suppressions array. |
| 10 | +- You only add manual suppression checks when the traversal is expensive (e.g. graph walking in MissingFaultPath, UnconnectedElement, DuplicateDMLOperation). |
| 11 | + |
| 12 | +## The Flow Model – What You Actually Work With |
| 13 | + |
| 14 | +The Flow object gives you three main collections (already parsed and typed): |
| 15 | + |
| 16 | +- flow.elements → every node, variable, constant, formula, choice, etc. |
| 17 | + Each item is a FlowNode, FlowVariable, FlowMetadata or FlowResource and always has a .name property. |
| 18 | +- flow.xmldata → raw JSON version of the Flow XML (useful for processMetadataValues, description, apiVersion, CanvasMode, etc.). |
| 19 | +- flow.start / flow.startElementReference / flow.startReference → entry point of the flow. |
| 20 | + |
| 21 | +Common filters you will use: |
| 22 | + |
| 23 | +- flow.elements?.filter(e => e.subtype === "recordLookups") |
| 24 | +- flow.elements?.filter(e => e.subtype === "loops") |
| 25 | +- flow.elements?.filter(e => e.subtype === "decisions") |
| 26 | + |
| 27 | +## The Compiler |
| 28 | + |
| 29 | +Compiler.traverseFlow(flow, startName, callback, optionalEndName) walks the flow exactly like the runtime does (iterative DFS, respects fault connectors, loop “noMoreValuesConnector”, etc.). |
| 30 | +Most complex rules (fault paths, unconnected elements, DML-in-loop, etc.) use this helper. |
| 31 | + |
| 32 | +```ts |
| 33 | +new Compiler().traverseFlow( |
| 34 | + flow, // your Flow instance |
| 35 | + flow.startReference, // name of the start element (or startElementReference) |
| 36 | + (element: FlowNode) => { |
| 37 | + // callback executed on every reachable element |
| 38 | + // your logic here – element is a FlowNode with .name, .subtype, .element, .connectors |
| 39 | + }, |
| 40 | + optionalEndName // (optional) stop traversal when this name is reached (used for loops) |
| 41 | +); |
| 42 | +``` |
| 43 | + |
| 44 | +## Example – Simple Element Rule (without manual suppressions) |
| 45 | + |
| 46 | +```ts |
| 47 | +import * as core from "../internals/internals"; |
| 48 | +import { RuleCommon } from "../models/RuleCommon"; |
| 49 | +import { IRuleDefinition } from "../interfaces/IRuleDefinition"; |
| 50 | + |
| 51 | +export class HardcodedReferences extends RuleCommon implements IRuleDefinition { |
| 52 | + constructor() { |
| 53 | + super({ |
| 54 | + name: "HardcodedReferences", |
| 55 | + label: "Hard-coded Record References", |
| 56 | + description: |
| 57 | + "Detects Get Records or other elements that use hard-coded Ids instead of variables.", |
| 58 | + supportedTypes: core.FlowType.allTypes(), |
| 59 | + docRefs: [], |
| 60 | + isConfigurable: false, |
| 61 | + autoFixable: false, |
| 62 | + }); |
| 63 | + } |
| 64 | + |
| 65 | + protected check( |
| 66 | + flow: core.Flow, |
| 67 | + _options: object | undefined, |
| 68 | + _suppressions: Set<string> |
| 69 | + ): core.Violation[] { |
| 70 | + const violations: core.Violation[] = []; |
| 71 | + |
| 72 | + const lookups = flow.elements?.filter((e) => e.subtype === "recordLookups") ?? []; |
| 73 | + |
| 74 | + for (const node of lookups) { |
| 75 | + const filterLogic = node.element.filterLogic; |
| 76 | + const conditions = node.element.conditions ?? node.element.objectConditions; |
| 77 | + |
| 78 | + // naive check – real rule would parse the condition properly |
| 79 | + if (JSON.stringify(conditions).match(/[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18}/)) { |
| 80 | + violations.push(new core.Violation(node)); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + return violations; // suppression handled automatically by base class |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +## Writing a New Rule – The Recipe |
| 90 | + |
| 91 | +1. Create a file src/main/rules/YourRuleName.ts |
| 92 | +2. Extend RuleCommon (or LoopRuleCommon for loop-only rules). |
| 93 | +3. In the constructor call super({ …RuleInfo… }) – all metadata goes here. |
| 94 | +4. Implement protected check(flow, options, suppressions) → Violation[] |
| 95 | + - Do your analysis. |
| 96 | + - Return new Violation(element) for element-level issues. |
| 97 | + - Return new Violation(new FlowAttribute(value, "property", "expected")) for flow-level issues. |
| 98 | + - No suppression code needed unless performance demands it. |
| 99 | +5. Add the rule to DefaultRuleStore.ts |
0 commit comments