Skip to content

Commit 6b0a724

Browse files
committed
automatic suppression handling and rule simplification
1 parent 4ab8df1 commit 6b0a724

33 files changed

+723
-778
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
<p>📌<strong>Tip:</strong> To link directly to a specific rule, use the full GitHub anchor link format. Example:</p>
3131
<p><em><a href="https://github.com/Flow-Scanner/lightning-flow-scanner-core#unsafe-running-context">https://github.com/Flow-Scanner/lightning-flow-scanner-core#unsafe-running-context</a></em></p>
3232

33+
> Want to code a new rule? → See [How to Write a Rule](docs/write-a-new-rule.md)
34+
3335
### Action Calls In Loop(Beta)
3436

3537
_[ActionCallsInLoop](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/rules/ActionCallsInLoop.ts)_ - To prevent exceeding Apex governor limits, it is advisable to consolidate and bulkify your apex calls, utilizing a single action call containing a collection variable at the end of the loop.

docs/write-a-rule.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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

jest.env-setup.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ const fs = require("fs");
22
const path = require("path");
33

44
module.exports = () => {
5-
console.log("UMD Setup: Running, UMD_PATH:", process.env.UMD_PATH || "undefined");
6-
75
if (!process.env.UMD_PATH) {
86
console.log("UMD Setup: No UMD_PATH—skipping (Node-only mode)");
97
return;
@@ -32,10 +30,6 @@ module.exports = () => {
3230
const exportsParam = viteUmdMatch[2];
3331
const depParam = viteUmdMatch[3];
3432

35-
console.log("UMD Setup: Vite UMD pattern matched!");
36-
console.log("UMD Setup: Factory body length:", factoryBody.length);
37-
console.log("UMD Setup: Exports param:", exportsParam, "Dependency param:", depParam);
38-
3933
try {
4034
// Create the factory function with proper parameters
4135
const factoryFn = depParam
@@ -53,9 +47,6 @@ module.exports = () => {
5347
// Assign to global
5448
global.lightningflowscanner = exports;
5549

56-
console.log("UMD Setup: Factory invoked successfully");
57-
console.log("UMD Setup: Exported keys:", Object.keys(exports).slice(0, 10));
58-
5950
if (Object.keys(exports).length === 0) {
6051
console.error("UMD Setup: WARNING - exports object is empty!");
6152
}
@@ -65,7 +56,6 @@ module.exports = () => {
6556
}
6657
} else {
6758
// Fallback: Try direct execution approach
68-
console.log("UMD Setup: Trying direct execution approach...");
6959

7060
try {
7161
// Create a mock global/window context
@@ -108,12 +98,4 @@ module.exports = () => {
10898
console.error("Stack:", e.stack.slice(0, 400));
10999
}
110100
}
111-
112-
// Final verification
113-
if (global.lightningflowscanner && Object.keys(global.lightningflowscanner).length > 0) {
114-
console.log("✓ UMD Setup: SUCCESS - lightningflowscanner loaded on global");
115-
console.log(" Available exports:", Object.keys(global.lightningflowscanner).join(", "));
116-
} else {
117-
console.error("✗ UMD Setup: FAILED - lightningflowscanner not properly loaded");
118-
}
119101
};

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@flow-scanner/lightning-flow-scanner-core",
33
"description": "A lightweight engine for Flow metadata in Node.js, and browser environments. Assess and enhance Salesforce Flow automations for best practices, security, governor limits, and performance issues.",
4-
"version": "6.4.3",
4+
"version": "6.5.0",
55
"main": "index.js",
66
"types": "index.d.ts",
77
"engines": {

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { IRuleDefinition } from "./main/interfaces/IRuleDefinition";
22
import type { IRulesConfig } from "./main/interfaces/IRulesConfig";
3+
import type { FlatViolation } from "./main/models/FlatViolation";
34

45
import { Compiler } from "./main/libs/Compiler";
56
import { exportDetails } from "./main/libs/exportAsDetails";
@@ -41,4 +42,4 @@ export {
4142
scan,
4243
ScanResult,
4344
};
44-
export type { IRuleDefinition, IRulesConfig };
45+
export type { FlatViolation, IRuleDefinition, IRulesConfig };

src/main/internals/internals.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IRuleDefinition } from "../interfaces/IRuleDefinition";
22
import type { IRulesConfig } from "../interfaces/IRulesConfig";
33

44
import { Compiler } from "../libs/Compiler";
5+
import { FlatViolation } from "../models/FlatViolation";
56
import { Flow } from "../models/Flow";
67
import { FlowAttribute } from "../models/FlowAttribute";
78
import { FlowElement } from "../models/FlowElement";
@@ -17,6 +18,7 @@ import { Violation } from "../models/Violation";
1718

1819
// Used to prevent circular dependencies in Common JS
1920
export {
21+
FlatViolation,
2022
FlowAttribute,
2123
FlowElement,
2224
FlowNode,

src/main/libs/ParseFlows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export async function parse(selectedUris: string[]): Promise<ParsedFlow[]> {
1313
ignoreAttributes: false,
1414
// @ts-expect-error type issue
1515
ignoreNameSpace: false,
16-
parseTagValue: false, // ADD THIS: Keeps tag/text values as strings (no auto boolean/number conversion)
16+
parseTagValue: false,
1717
textNodeName: "#text"
1818
});
1919

src/main/libs/exportAsDetails.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1+
import { FlatViolation } from "../models/FlatViolation";
12
import { ScanResult } from "../models/ScanResult";
23
import { Violation } from "../models/Violation";
34

4-
export interface FlatViolation extends Omit<Violation, 'details'> {
5-
flowFile: string;
6-
flowName: string;
7-
ruleName: string;
8-
severity: string;
9-
}
10-
115
export function exportDetails(results: ScanResult[], includeDetails = false): FlatViolation[] {
126
return results.flatMap(result => {
137
const flow = result.flow;

src/main/models/FlatViolation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Violation } from "./Violation";
2+
3+
export interface FlatViolation extends Omit<Violation, 'details'> {
4+
flowFile: string;
5+
flowName: string;
6+
ruleName: string;
7+
severity: string;
8+
}

0 commit comments

Comments
 (0)