@flow-scanner/lightning-flow-scanner-core 6.17.2 → 6.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@
39
39
  - [Configure Rules](#configure-rules)
40
40
  - [Define Exceptions](#define-exceptions)
41
41
  - [Exclude Flows](#exclude-flows)
42
- - [Scan Modes](#scan-modes)
42
+ - [Scan Options](#scan-options)
43
43
  - **[Installation](#installation)**
44
44
  - [Distributions](#distributions)
45
45
  - [CICD Templates](#cicd-templates)
@@ -270,6 +270,10 @@ Auto-Layout automatically arranges and aligns Flow elements, keeping the canvas
270
270
  **Rule ID:** `missing-auto-layout`
271
271
  **Class Name:** _[AutoLayout](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/AutoLayout.ts)_
272
272
  **Severity:** 🔵 *Note*
273
+
274
+ #### System (subcategory)
275
+
276
+ System rules are a subset of Layout rules that detect structural issues normally prevented by the Flow Builder UI. See [System Rules Documentation](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/docs/system-rules.md) for the full list.
273
277
  <!-- END GENERATED_RULES -->
274
278
 
275
279
  ---
@@ -281,20 +285,26 @@ It is recommend to configure and define:
281
285
  - The severity of violating any specific rule.
282
286
  - Expressions used for rules, such as REGEX patterns and comparison operators.
283
287
  - Any known exceptions that should be ignored during scanning.
288
+ - (Optional) Implement filters based on a severity **threshold** or **rule categories**.
289
+
290
+ Most distributions automatically load configuration from:
291
+ - `.flow-scanner.yml`
292
+ - `.flow-scanner.json`
293
+ - `package.json` → `"flowScanner"` key
284
294
 
285
295
  ```json
286
296
  {
287
297
  "rules": {
288
- // Your rule configurations
298
+ // rule customizations (severity, expression, enabled, ...)
289
299
  },
290
300
  "exceptions": {
291
- // Your defined exceptions
292
- }
301
+ // flow rule → result suppressions
302
+ },
303
+ "threshold": "error", // only consider errors
304
+ "categories": ["problem", "layout"] // only run rules from these categories
293
305
  }
294
306
  ```
295
307
 
296
- Most Lightning Flow Scanner distributions automatically resolve configurations from `.flow-scanner.yml`, `.flow-scanner.json`, or `package.json` → `flowScanner`.
297
-
298
308
  ### Configure Rules
299
309
 
300
310
  By default, all default rules are executed. You can customize individual rules and override the rules to be executed without having to specify every rule. Below is a breakdown of the available attributes of rule configuration:
@@ -315,7 +325,7 @@ By default, all default rules are executed. You can customize individual rules a
315
325
 
316
326
  #### Configure Severity
317
327
 
318
- When the severity is not provided it will be `warning` by default. Other available values for severity are `error` and `note`. Configure the severity per rule as demonstrated below:
328
+ Available values for severity are `error`, `warning` and `note`. If no severity is provided, a default value is applied. Configure the severity per rule as demonstrated below:
319
329
 
320
330
  ```json
321
331
  {
@@ -444,7 +454,19 @@ Exclude specific flows by their unique API names, regardless of their location.
444
454
 
445
455
  **Environment compatibility**: works in **all environments** including Node.js and browser/web distributions, as it operates on parsed flow data rather than file system paths.
446
456
 
447
- ### Scan Modes
457
+ ### Scan Options
458
+
459
+ #### Severity Threshold
460
+ Only report on violations at or above a chosen severity level:
461
+ ```json
462
+ { "threshold": "error" }
463
+ ```
464
+
465
+ #### Filter by category
466
+ Restrict the scan to specific categories of rules:
467
+ ```json
468
+ { "categories": ["problem", "layout"] }
469
+ ```
448
470
 
449
471
  #### Beta Mode
450
472
 
@@ -469,9 +491,10 @@ By default, Lightning Flow Scanner runs **all** default rules and merges any cus
469
491
  |----------------------------------------------------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------|
470
492
  | **[Salesforce CLI Plugin](https://www.npmjs.com/package/lightning-flow-scanner)** | Local development, scratch orgs, CI/CD | `sf plugins install lightning-flow-scanner` |
471
493
  | **[VS Code Extension](https://open-vsx.org/extension/ForceConfigControl/lightning-flow-scanner-vsx)** | Real-time scanning inside VS Code | `code --install-extension ForceConfigControl.lightning-flow-scanner-vsx` |
472
- | **[Salesforce App (Managed Package)](https://github.com/Flow-Scanner/lightning-flow-scanner-app)** | Run scans directly inside a Salesforce org | `sf package install --package 04tgK0000008CLlQAM` |
494
+ | **[Salesforce App](https://github.com/Flow-Scanner/lightning-flow-scanner-app)** | Run scans directly inside a Salesforce org | `sf package install --package 04tgK0000008CLlQAM` |
473
495
  | **[GitHub Action](https://github.com/marketplace/actions/lightning-flow-scan)** | Native PR checks | `uses: Flow-Scanner/lightning-flow-scanner@main` |
474
496
  | **[Core Library](https://www.npmjs.com/package/@flow-scanner/lightning-flow-scanner-core)** (Node.js + Browser) | Custom tools, scripts, extensions, web apps | `npm install -g @flow-scanner/lightning-flow-scanner-core` |
497
+ | **[Regex Scanner](https://www.npmjs.com/package/@flow-scanner/regex-scanner)** | Regex-based scanning | `npm install -g @flow-scanner/regex-scanner`
475
498
 
476
499
  **Privacy:** Zero user data collected. All processing is client-side. → See our [Security Policy](https://github.com/Flow-Scanner/lightning-flow-scanner?tab=security-ov-file).
477
500
 
@@ -628,7 +651,7 @@ For more on Programmatic API, types, and advanced usage of `@flow-scanner/lightn
628
651
  6. Deploy Demo Flows (Optional):
629
652
 
630
653
  ```bash
631
- cd example-flows && sf project deploy start
654
+ sf project deploy start
632
655
  ```
633
656
 
634
657
  Navigate to the [Demo Readme](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/example-flows/README.md) for full details
package/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { IRuleDefinition } from "./main/interfaces/IRuleDefinition";
2
- import type { IRulesConfig } from "./main/interfaces/IRulesConfig";
2
+ import type { IRulesConfig, RuleCategory, Severity, Threshold } from "./main/interfaces/IRulesConfig";
3
+ import { SEVERITY_ORDER, meetsThreshold, countThresholdViolations, filterByThreshold } from "./main/interfaces/IRulesConfig";
3
4
  import type { FlatViolation } from "./main/models/FlatViolation";
4
5
  import { Compiler } from "./main/libs/Compiler";
5
6
  import { exportDetails } from "./main/libs/ExportDetails";
@@ -22,5 +23,5 @@ import { Violation } from "./main/models/Violation";
22
23
  import { DEFAULT_ICONS, ASCII_ICONS, type NodeIconConfig } from "./main/config/NodeIcons";
23
24
  import { DEFAULT_VARIABLE_ICONS, ASCII_VARIABLE_ICONS, type VariableIconConfig } from "./main/config/VariableIcons";
24
25
  import { exportDiagram, type DiagramOptions } from "./main/libs/ExportDiagram";
25
- export { Compiler, exportDetails, exportDiagram, exportSarif, fix, Flow, FlowAttribute, FlowElement, FlowNode, FlowResource, FlowType, FlowVariable, getRules, parse, ParsedFlow, Violation, RuleResult, scan, ScanResult, DEFAULT_ICONS, ASCII_ICONS, DEFAULT_VARIABLE_ICONS, ASCII_VARIABLE_ICONS, };
26
- export type { FlatViolation, IRuleDefinition, IRulesConfig, NodeIconConfig, DiagramOptions, VariableIconConfig };
26
+ export { Compiler, exportDetails, exportDiagram, exportSarif, fix, Flow, FlowAttribute, FlowElement, FlowNode, FlowResource, FlowType, FlowVariable, getRules, parse, ParsedFlow, Violation, RuleResult, scan, ScanResult, DEFAULT_ICONS, ASCII_ICONS, DEFAULT_VARIABLE_ICONS, ASCII_VARIABLE_ICONS, SEVERITY_ORDER, meetsThreshold, countThresholdViolations, filterByThreshold, };
27
+ export type { FlatViolation, IRuleDefinition, IRulesConfig, RuleCategory, Severity, Threshold, NodeIconConfig, DiagramOptions, VariableIconConfig };
package/index.js CHANGED
@@ -52,12 +52,19 @@ _export(exports, {
52
52
  get RuleResult () {
53
53
  return _RuleResult.RuleResult;
54
54
  },
55
+ get // Threshold utilities
56
+ SEVERITY_ORDER () {
57
+ return _IRulesConfig.SEVERITY_ORDER;
58
+ },
55
59
  get ScanResult () {
56
60
  return _ScanResult.ScanResult;
57
61
  },
58
62
  get Violation () {
59
63
  return _Violation.Violation;
60
64
  },
65
+ get countThresholdViolations () {
66
+ return _IRulesConfig.countThresholdViolations;
67
+ },
61
68
  get exportDetails () {
62
69
  return _ExportDetails.exportDetails;
63
70
  },
@@ -67,12 +74,18 @@ _export(exports, {
67
74
  get exportSarif () {
68
75
  return _ExportSarif.exportSarif;
69
76
  },
77
+ get filterByThreshold () {
78
+ return _IRulesConfig.filterByThreshold;
79
+ },
70
80
  get fix () {
71
81
  return _FixFlows.fix;
72
82
  },
73
83
  get getRules () {
74
84
  return _GetRuleDefinitions.getRules;
75
85
  },
86
+ get meetsThreshold () {
87
+ return _IRulesConfig.meetsThreshold;
88
+ },
76
89
  get parse () {
77
90
  return _ParseFlows.parse;
78
91
  },
@@ -80,6 +93,7 @@ _export(exports, {
80
93
  return _ScanFlows.scan;
81
94
  }
82
95
  });
96
+ const _IRulesConfig = require("./main/interfaces/IRulesConfig");
83
97
  const _Compiler = require("./main/libs/Compiler");
84
98
  const _ExportDetails = require("./main/libs/ExportDetails");
85
99
  const _ExportSarif = require("./main/libs/ExportSarif");
@@ -34,6 +34,7 @@ const _UnsafeRunningContext = require("../rules/UnsafeRunningContext");
34
34
  const _UnusedVariable = require("../rules/UnusedVariable");
35
35
  const _MissingMetadataDescription = require("../rules/MissingMetadataDescription");
36
36
  const _MissingRecordTriggerFilter = require("../rules/MissingRecordTriggerFilter");
37
+ const _MissingStartReference = require("../rules/MissingStartReference");
37
38
  const _TransformInsteadOfLoop = require("../rules/TransformInsteadOfLoop");
38
39
  const _RecordIdAsString = require("../rules/RecordIdAsString");
39
40
  function _define_property(obj, key, value) {
@@ -201,6 +202,7 @@ registry.register("unsafe-running-context", _UnsafeRunningContext.UnsafeRunningC
201
202
  registry.register("unused-variable", _UnusedVariable.UnusedVariable, "UnusedVariable");
202
203
  registry.register("missing-metadata-description", _MissingMetadataDescription.MissingMetadataDescription, "MissingMetadataDescription", true);
203
204
  registry.register("missing-record-trigger-filter", _MissingRecordTriggerFilter.MissingRecordTriggerFilter, "MissingFilterRecordTrigger", true);
205
+ registry.register("missing-start-reference", _MissingStartReference.MissingStartReference, "MissingStartReference", true);
204
206
  registry.register("transform-instead-of-loop", _TransformInsteadOfLoop.TransformInsteadOfLoop, "TransformInsteadOfLoop", true);
205
207
  registry.register("record-id-as-string", _RecordIdAsString.RecordIdAsString, "RecordIdAsString", true);
206
208
  registry.register("hardcoded-secret", _HardcodedSecret.HardcodedSecret, "HardcodedSecret", true);
@@ -1,6 +1,7 @@
1
1
  import { Flow, RuleResult } from "../internals/internals";
2
2
  export interface IRuleDefinition {
3
3
  ruleId: string;
4
+ category?: 'problem' | 'suggestion' | 'layout' | 'system';
4
5
  description: string;
5
6
  summary: string;
6
7
  docRefs: Array<{
@@ -4,12 +4,45 @@ export declare enum DetailLevel {
4
4
  ENRICHED = "enriched",
5
5
  SIMPLE = "simple"
6
6
  }
7
+ export type RuleCategory = 'problem' | 'suggestion' | 'layout';
8
+ export type Severity = 'error' | 'warning' | 'note';
9
+ export type Threshold = Severity | 'never';
10
+ /** Severity levels ordered from most to least severe */
11
+ export declare const SEVERITY_ORDER: Severity[];
7
12
  export interface IRulesConfig {
8
13
  betaMode?: boolean;
9
14
  betamode?: boolean;
15
+ systemRules?: boolean;
16
+ categories?: RuleCategory[];
17
+ threshold?: Threshold;
10
18
  detailLevel?: 'enriched' | 'simple' | DetailLevel;
11
19
  exceptions?: IExceptions;
12
20
  rules?: IRuleOptions;
13
21
  ruleMode?: "merged" | "isolated";
14
22
  ignoreFlows?: string[];
15
23
  }
24
+ /**
25
+ * Check if a severity meets or exceeds the threshold.
26
+ * @param severity - The severity to check
27
+ * @param threshold - The threshold to compare against
28
+ * @returns true if severity >= threshold (more severe or equal)
29
+ */
30
+ export declare function meetsThreshold(severity: string | undefined, threshold: Threshold): boolean;
31
+ /**
32
+ * Count violations that meet or exceed the threshold.
33
+ * @param results - Array of results with severity property
34
+ * @param threshold - The threshold to compare against
35
+ * @returns Number of violations meeting the threshold
36
+ */
37
+ export declare function countThresholdViolations(results: Array<{
38
+ severity?: string;
39
+ }>, threshold: Threshold): number;
40
+ /**
41
+ * Filter results to only include those meeting the threshold.
42
+ * @param results - Array of results with severity property
43
+ * @param threshold - The threshold to filter by
44
+ * @returns Filtered array of results meeting the threshold ('never' returns all)
45
+ */
46
+ export declare function filterByThreshold<T extends {
47
+ severity?: string;
48
+ }>(results: T[], threshold: Threshold): T[];
@@ -2,10 +2,27 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- Object.defineProperty(exports, "DetailLevel", {
6
- enumerable: true,
7
- get: function() {
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get DetailLevel () {
8
13
  return DetailLevel;
14
+ },
15
+ get SEVERITY_ORDER () {
16
+ return SEVERITY_ORDER;
17
+ },
18
+ get countThresholdViolations () {
19
+ return countThresholdViolations;
20
+ },
21
+ get filterByThreshold () {
22
+ return filterByThreshold;
23
+ },
24
+ get meetsThreshold () {
25
+ return meetsThreshold;
9
26
  }
10
27
  });
11
28
  var DetailLevel = /*#__PURE__*/ function(DetailLevel) {
@@ -13,3 +30,24 @@ var DetailLevel = /*#__PURE__*/ function(DetailLevel) {
13
30
  DetailLevel["SIMPLE"] = "simple";
14
31
  return DetailLevel;
15
32
  }({});
33
+ const SEVERITY_ORDER = [
34
+ 'error',
35
+ 'warning',
36
+ 'note'
37
+ ];
38
+ function meetsThreshold(severity, threshold) {
39
+ if (threshold === 'never') return false;
40
+ const sev = severity || 'warning';
41
+ const sevIndex = SEVERITY_ORDER.indexOf(sev);
42
+ const thresholdIndex = SEVERITY_ORDER.indexOf(threshold);
43
+ // Lower index = more severe, so severity meets threshold if sevIndex <= thresholdIndex
44
+ return sevIndex >= 0 && sevIndex <= thresholdIndex;
45
+ }
46
+ function countThresholdViolations(results, threshold) {
47
+ if (threshold === 'never') return 0;
48
+ return results.filter((r)=>meetsThreshold(r.severity, threshold)).length;
49
+ }
50
+ function filterByThreshold(results, threshold) {
51
+ if (threshold === 'never') return results;
52
+ return results.filter((r)=>meetsThreshold(r.severity, threshold));
53
+ }
@@ -1,3 +1,7 @@
1
1
  import * as core from "../internals/internals";
2
2
  export declare function fix(results: core.ScanResult[]): core.ScanResult[];
3
+ /**
4
+ * @deprecated Use fix() instead which modifies flows in place.
5
+ * Kept for backward compatibility.
6
+ */
3
7
  export declare function FixFlows(flow: core.Flow, ruleResults: core.RuleResult[]): core.Flow;
@@ -17,7 +17,6 @@ _export(exports, {
17
17
  }
18
18
  });
19
19
  const _internals = /*#__PURE__*/ _interop_require_wildcard(require("../internals/internals"));
20
- const _BuildFlow = require("./BuildFlow");
21
20
  function _getRequireWildcardCache(nodeInterop) {
22
21
  if (typeof WeakMap !== "function") return null;
23
22
  var cacheBabelInterop = new WeakMap();
@@ -63,45 +62,105 @@ function fix(results) {
63
62
  const newResults = [];
64
63
  for (const result of results){
65
64
  if (!result.ruleResults || result.ruleResults.length === 0) continue;
66
- const fixables = result.ruleResults.filter((r)=>r.ruleName === "UnusedVariable" && r.occurs || r.ruleName === "UnconnectedElement" && r.occurs);
65
+ const fixables = result.ruleResults.filter((r)=>r.ruleName === "UnusedVariable" && r.occurs || r.ruleName === "UnconnectedElement" && r.occurs || r.ruleName === "AutoLayout" && r.occurs);
67
66
  if (fixables.length === 0) continue;
68
- const newFlow = FixFlows(result.flow, fixables);
69
- const hasRemainingElements = newFlow.elements && newFlow.elements.length > 0;
70
- if (hasRemainingElements) {
71
- result.flow = newFlow;
72
- newResults.push(result);
67
+ // Handle AutoLayout fix separately (modifies metadata, not elements)
68
+ const autoLayoutFix = fixables.find((r)=>r.ruleName === "AutoLayout");
69
+ if (autoLayoutFix) {
70
+ applyAutoLayoutFix(result.flow);
73
71
  }
72
+ // Handle element-based fixes (UnusedVariable, UnconnectedElement)
73
+ // These modify xmldata in place to preserve element order and formatting
74
+ const elementFixables = fixables.filter((r)=>r.ruleName !== "AutoLayout");
75
+ if (elementFixables.length > 0) {
76
+ applyElementFixes(result.flow, elementFixables);
77
+ }
78
+ newResults.push(result);
74
79
  }
75
80
  return newResults;
76
81
  }
77
- function FixFlows(flow, ruleResults) {
78
- var _unusedVariableRes_details, _unconnectedElementsRes_details, _flow_elements;
82
+ function applyAutoLayoutFix(flow) {
83
+ if (!flow.xmldata) return;
84
+ // Ensure processMetadataValues is an array
85
+ if (!flow.xmldata.processMetadataValues) {
86
+ flow.xmldata.processMetadataValues = [];
87
+ } else if (!Array.isArray(flow.xmldata.processMetadataValues)) {
88
+ flow.xmldata.processMetadataValues = [
89
+ flow.xmldata.processMetadataValues
90
+ ];
91
+ }
92
+ // Find existing CanvasMode entry
93
+ const canvasModeIndex = flow.xmldata.processMetadataValues.findIndex((mdv)=>mdv.name === "CanvasMode");
94
+ const autoLayoutValue = {
95
+ name: "CanvasMode",
96
+ value: {
97
+ stringValue: "AUTO_LAYOUT_CANVAS"
98
+ }
99
+ };
100
+ if (canvasModeIndex >= 0) {
101
+ // Update existing entry
102
+ flow.xmldata.processMetadataValues[canvasModeIndex] = autoLayoutValue;
103
+ } else {
104
+ // Add new entry
105
+ flow.xmldata.processMetadataValues.push(autoLayoutValue);
106
+ }
107
+ // Update the flow's processMetadataValues property
108
+ flow.processMetadataValues = flow.xmldata.processMetadataValues;
109
+ }
110
+ /**
111
+ * Apply element-based fixes (UnusedVariable, UnconnectedElement) by modifying xmldata in place.
112
+ * This preserves element order and formatting from the original file.
113
+ */ function applyElementFixes(flow, ruleResults) {
114
+ var _unusedVariableRes_details, _unconnectedElementsRes_details;
115
+ if (!flow.xmldata) return;
79
116
  const unusedVariableRes = ruleResults.find((r)=>r.ruleName === "UnusedVariable");
80
117
  var _unusedVariableRes_details_map;
81
118
  const unusedVariableNames = new Set((_unusedVariableRes_details_map = unusedVariableRes === null || unusedVariableRes === void 0 ? void 0 : (_unusedVariableRes_details = unusedVariableRes.details) === null || _unusedVariableRes_details === void 0 ? void 0 : _unusedVariableRes_details.map((d)=>d.name)) !== null && _unusedVariableRes_details_map !== void 0 ? _unusedVariableRes_details_map : []);
82
119
  const unconnectedElementsRes = ruleResults.find((r)=>r.ruleName === "UnconnectedElement");
83
120
  var _unconnectedElementsRes_details_map;
84
121
  const unconnectedElementNames = new Set((_unconnectedElementsRes_details_map = unconnectedElementsRes === null || unconnectedElementsRes === void 0 ? void 0 : (_unconnectedElementsRes_details = unconnectedElementsRes.details) === null || _unconnectedElementsRes_details === void 0 ? void 0 : _unconnectedElementsRes_details.map((d)=>d.name)) !== null && _unconnectedElementsRes_details_map !== void 0 ? _unconnectedElementsRes_details_map : []);
85
- var _flow_elements_filter;
86
- const nodesToKeep = (_flow_elements_filter = (_flow_elements = flow.elements) === null || _flow_elements === void 0 ? void 0 : _flow_elements.filter((node)=>{
87
- switch(node.metaType){
88
- case "attribute":
89
- case "resource":
90
- return true;
91
- case "node":
92
- {
93
- const nodeElement = node;
94
- return !unconnectedElementNames.has(nodeElement.name);
95
- }
96
- case "variable":
97
- {
98
- const nodeVar = node;
99
- return !unusedVariableNames.has(nodeVar.name);
100
- }
101
- default:
102
- return false;
122
+ // Remove unused variables from xmldata
123
+ if (unusedVariableNames.size > 0) {
124
+ for (const varTag of _internals.Flow.VARIABLE_TAGS){
125
+ removeElementsByName(flow.xmldata, varTag, unusedVariableNames);
126
+ }
127
+ }
128
+ // Remove unconnected elements from xmldata
129
+ if (unconnectedElementNames.size > 0) {
130
+ for (const nodeTag of _internals.Flow.NODE_TAGS){
131
+ removeElementsByName(flow.xmldata, nodeTag, unconnectedElementNames);
132
+ }
133
+ }
134
+ // Update the flow's elements array to match the modified xmldata
135
+ flow.preProcessNodes();
136
+ }
137
+ /**
138
+ * Remove elements from xmldata by name.
139
+ * Handles both single element and array cases.
140
+ */ function removeElementsByName(xmldata, tagName, namesToRemove) {
141
+ const elements = xmldata[tagName];
142
+ if (!elements) return;
143
+ if (Array.isArray(elements)) {
144
+ const filtered = elements.filter((el)=>!namesToRemove.has(el === null || el === void 0 ? void 0 : el.name));
145
+ if (filtered.length === 0) {
146
+ delete xmldata[tagName];
147
+ } else if (filtered.length === 1) {
148
+ // Keep as single element if only one remains (matches original format)
149
+ xmldata[tagName] = filtered[0];
150
+ } else {
151
+ xmldata[tagName] = filtered;
103
152
  }
104
- })) !== null && _flow_elements_filter !== void 0 ? _flow_elements_filter : [];
105
- const xmldata = (0, _BuildFlow.BuildFlow)(nodesToKeep);
106
- return new _internals.Flow(flow.fsPath, xmldata);
153
+ } else if (typeof elements === 'object' && elements !== null) {
154
+ // Single element case
155
+ if (namesToRemove.has(elements.name)) {
156
+ delete xmldata[tagName];
157
+ }
158
+ }
159
+ }
160
+ function FixFlows(flow, ruleResults) {
161
+ // Create a shallow clone of xmldata to avoid modifying the original
162
+ const clonedXmldata = JSON.parse(JSON.stringify(flow.xmldata));
163
+ const clonedFlow = new _internals.Flow(flow.fsPath, clonedXmldata);
164
+ applyElementFixes(clonedFlow, ruleResults);
165
+ return clonedFlow;
107
166
  }
@@ -19,6 +19,8 @@ _export(exports, {
19
19
  const _RuleRegistry = require("../config/RuleRegistry");
20
20
  function GetRuleDefinitions(ruleConfig, options) {
21
21
  const includeBeta = (options === null || options === void 0 ? void 0 : options.betaMode) === true || (options === null || options === void 0 ? void 0 : options.betamode) === true;
22
+ const includeSystem = (options === null || options === void 0 ? void 0 : options.systemRules) !== false; // defaults to true
23
+ const categories = options === null || options === void 0 ? void 0 : options.categories; // undefined means all categories
22
24
  const rulesMode = (options === null || options === void 0 ? void 0 : options.ruleMode) || "merged";
23
25
  const selectedRules = [];
24
26
  const ruleIds = _RuleRegistry.ruleRegistry.getAllRuleIds(includeBeta);
@@ -31,6 +33,10 @@ function GetRuleDefinitions(ruleConfig, options) {
31
33
  const config = ruleConfig.get(key);
32
34
  if ((config === null || config === void 0 ? void 0 : config.enabled) === false) continue;
33
35
  const rule = _RuleRegistry.ruleRegistry.createInstance(entry.ruleId); // Always use ruleId to instantiate
36
+ // Skip system rules if disabled
37
+ if (rule.category === 'system' && !includeSystem) continue;
38
+ // Skip rules not in selected categories (if categories filter is specified)
39
+ if (!isCategoryIncluded(rule.category, categories, includeSystem)) continue;
34
40
  if (config === null || config === void 0 ? void 0 : config.severity) {
35
41
  rule.severity = config.severity;
36
42
  }
@@ -41,6 +47,10 @@ function GetRuleDefinitions(ruleConfig, options) {
41
47
  // MERGED MODE (default)
42
48
  for (const ruleId of ruleIds){
43
49
  const rule = _RuleRegistry.ruleRegistry.createInstance(ruleId);
50
+ // Skip system rules if disabled
51
+ if (rule.category === 'system' && !includeSystem) continue;
52
+ // Skip rules not in selected categories (if categories filter is specified)
53
+ if (!isCategoryIncluded(rule.category, categories, includeSystem)) continue;
44
54
  var _ruleConfig_get;
45
55
  // Try to find config by ruleId first, then fall back to legacy name
46
56
  const config = (_ruleConfig_get = ruleConfig === null || ruleConfig === void 0 ? void 0 : ruleConfig.get(rule.ruleId)) !== null && _ruleConfig_get !== void 0 ? _ruleConfig_get : ruleConfig === null || ruleConfig === void 0 ? void 0 : ruleConfig.get(rule.name) // rule.name is the legacy camelCase name (e.g. "ActionCallsInLoop")
@@ -53,6 +63,26 @@ function GetRuleDefinitions(ruleConfig, options) {
53
63
  }
54
64
  return selectedRules;
55
65
  }
66
+ /**
67
+ * Check if a rule's category should be included based on the categories filter.
68
+ * - If no categories filter is specified, all categories are included
69
+ * - System rules are handled separately via includeSystem flag
70
+ * - Rules with matching category are included
71
+ * - Category matching is case-insensitive
72
+ */ function isCategoryIncluded(ruleCategory, categories, includeSystem) {
73
+ // System category is controlled by systemRules flag, not categories filter
74
+ if (ruleCategory === 'system') {
75
+ return includeSystem;
76
+ }
77
+ // If no categories filter specified, include all non-system categories
78
+ if (!categories || categories.length === 0) {
79
+ return true;
80
+ }
81
+ // Normalize categories to lowercase for case-insensitive matching
82
+ const normalizedCategories = categories.map((c)=>c.toLowerCase());
83
+ // Check if rule's category is in the allowed list (case-insensitive)
84
+ return normalizedCategories.includes(ruleCategory === null || ruleCategory === void 0 ? void 0 : ruleCategory.toLowerCase());
85
+ }
56
86
  function getRules(ruleNames, options) {
57
87
  return _RuleRegistry.ruleRegistry.getRulesByNames(ruleNames, options);
58
88
  }
@@ -66,4 +66,5 @@ export declare class Flow {
66
66
  private findStart;
67
67
  toXMLString(): string;
68
68
  private generateDoc;
69
+ private hasXsiAttributes;
69
70
  }
@@ -259,16 +259,42 @@ let Flow = class Flow {
259
259
  };
260
260
  const builder = new _fastxmlparser.XMLBuilder(builderOptions);
261
261
  const xmldataWithNs = _object_spread({}, this.xmldata);
262
+ // Always ensure the base xmlns is present
262
263
  if (!xmldataWithNs["@_xmlns"]) {
263
264
  xmldataWithNs["@_xmlns"] = flowXmlNamespace;
264
265
  }
265
- if (!xmldataWithNs["@_xmlns:xsi"]) {
266
+ // Only add xmlns:xsi if the content actually uses xsi: attributes
267
+ // Don't add it unconditionally to avoid unnecessary diffs
268
+ if (!xmldataWithNs["@_xmlns:xsi"] && this.hasXsiAttributes(xmldataWithNs)) {
266
269
  xmldataWithNs["@_xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance";
267
270
  }
268
271
  const rootObj = {
269
272
  Flow: xmldataWithNs
270
273
  };
271
- return builder.build(rootObj);
274
+ const xmlContent = builder.build(rootObj);
275
+ // Add XML declaration if not present
276
+ const xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>\n';
277
+ if (!xmlContent.startsWith('<?xml')) {
278
+ return xmlDeclaration + xmlContent;
279
+ }
280
+ return xmlContent;
281
+ }
282
+ hasXsiAttributes(obj) {
283
+ if (obj === null || obj === undefined) {
284
+ return false;
285
+ }
286
+ if (typeof obj !== 'object') {
287
+ return false;
288
+ }
289
+ for (const key of Object.keys(obj)){
290
+ if (key.includes(':xsi') || key.includes('xsi:')) {
291
+ return true;
292
+ }
293
+ if (this.hasXsiAttributes(obj[key])) {
294
+ return true;
295
+ }
296
+ }
297
+ return false;
272
298
  }
273
299
  constructor(path, data){
274
300
  // Flow elements (excludes legacy start nodes)
@@ -8,6 +8,7 @@ import { FlowNode } from "./FlowNode";
8
8
  export declare class FlowGraph {
9
9
  private nodeMap;
10
10
  private reachableFromStart;
11
+ private normalReachableFromStart;
11
12
  private elementsInLoop;
12
13
  private faultConnectors;
13
14
  private normalConnectors;
@@ -35,6 +36,11 @@ export declare class FlowGraph {
35
36
  * This reuses the existing IDDFS traversal logic!
36
37
  */
37
38
  private computeReachability;
39
+ /**
40
+ * Compute which elements are reachable from start using ONLY normal connectors (not fault connectors).
41
+ * Elements that are reachable overall but NOT reachable via normal connectors are part of fault handling.
42
+ */
43
+ private computeNormalReachability;
38
44
  /**
39
45
  * Use Compiler to compute which elements are inside loops.
40
46
  * Calls Compiler.traverseFlow() for each loop with endElementName.
@@ -51,6 +57,11 @@ export declare class FlowGraph {
51
57
  getAllNextElements(elementName: string): string[];
52
58
  getPreviousElements(elementName: string): string[];
53
59
  getNode(elementName: string): FlowNode | undefined;
60
+ /**
61
+ * Check if an element is part of fault handling flow.
62
+ * An element is part of fault handling if it's only reachable through fault paths
63
+ * (i.e., reachable overall but NOT reachable via normal connectors from START).
64
+ */
54
65
  isPartOfFaultHandling(elementName: string): boolean;
55
66
  getLoopNodes(): FlowNode[];
56
67
  forEachReachable(callback: (node: FlowNode) => void): void;
@@ -110,6 +110,15 @@ let FlowGraph = class FlowGraph {
110
110
  }, this.nodeMap, this.allConnectors);
111
111
  }
112
112
  /**
113
+ * Compute which elements are reachable from start using ONLY normal connectors (not fault connectors).
114
+ * Elements that are reachable overall but NOT reachable via normal connectors are part of fault handling.
115
+ */ computeNormalReachability(startReference) {
116
+ const compiler = new _Compiler.Compiler();
117
+ compiler.traverseFlow(startReference, (element)=>{
118
+ this.normalReachableFromStart.add(element.name);
119
+ }, this.nodeMap, this.normalConnectors);
120
+ }
121
+ /**
113
122
  * Use Compiler to compute which elements are inside loops.
114
123
  * Calls Compiler.traverseFlow() for each loop with endElementName.
115
124
  */ computeLoopBoundaries() {
@@ -167,13 +176,13 @@ let FlowGraph = class FlowGraph {
167
176
  getNode(elementName) {
168
177
  return this.nodeMap.get(elementName);
169
178
  }
170
- isPartOfFaultHandling(elementName) {
171
- const previous = this.getPreviousElements(elementName);
172
- return previous.some((prev)=>{
173
- const faultTargets = this.faultConnectors.get(prev);
174
- var _faultTargets_has;
175
- return (_faultTargets_has = faultTargets === null || faultTargets === void 0 ? void 0 : faultTargets.has(elementName)) !== null && _faultTargets_has !== void 0 ? _faultTargets_has : false;
176
- });
179
+ /**
180
+ * Check if an element is part of fault handling flow.
181
+ * An element is part of fault handling if it's only reachable through fault paths
182
+ * (i.e., reachable overall but NOT reachable via normal connectors from START).
183
+ */ isPartOfFaultHandling(elementName) {
184
+ // Element is part of fault handling if it's reachable but NOT reachable via normal paths
185
+ return this.reachableFromStart.has(elementName) && !this.normalReachableFromStart.has(elementName);
177
186
  }
178
187
  getLoopNodes() {
179
188
  return Array.from(this.nodeMap.values()).filter((n)=>n.subtype === "loops");
@@ -508,6 +517,7 @@ let FlowGraph = class FlowGraph {
508
517
  _define_property(this, "nodeMap", new Map());
509
518
  // Pre-computed sets for common queries (built using Compiler)
510
519
  _define_property(this, "reachableFromStart", new Set());
520
+ _define_property(this, "normalReachableFromStart", new Set()); // Elements reachable via normal (non-fault) connectors only
511
521
  _define_property(this, "elementsInLoop", new Map()); // element -> loop name
512
522
  // Connector metadata (extracted during node processing)
513
523
  _define_property(this, "faultConnectors", new Map());
@@ -527,6 +537,7 @@ let FlowGraph = class FlowGraph {
527
537
  this.computeLoopBoundaries();
528
538
  if (startReference) {
529
539
  this.computeReachability(startReference);
540
+ this.computeNormalReachability(startReference);
530
541
  }
531
542
  }
532
543
  };
@@ -1,7 +1,7 @@
1
1
  import { RuleInfo } from "./RuleInfo";
2
2
  import * as core from "../internals/internals";
3
3
  export declare abstract class RuleCommon {
4
- category?: 'problem' | 'suggestion' | 'layout';
4
+ category?: 'problem' | 'suggestion' | 'layout' | 'system';
5
5
  description: string;
6
6
  summary: string;
7
7
  docRefs: Array<{
@@ -29,9 +29,8 @@ export declare class RuleInfo {
29
29
  label: string;
30
30
  /**
31
31
  * The category for the rule.
32
- * 'problem' | 'suggestion' | 'layout'
33
32
  */
34
- category: 'problem' | 'suggestion' | 'layout';
33
+ category: 'problem' | 'suggestion' | 'layout' | 'system';
35
34
  /**
36
35
  * Stable public identifier used for config, suppression, and reporting.
37
36
  */
@@ -38,7 +38,6 @@ let RuleInfo = class RuleInfo {
38
38
  */ _define_property(this, "label", void 0);
39
39
  /**
40
40
  * The category for the rule.
41
- * 'problem' | 'suggestion' | 'layout'
42
41
  */ _define_property(this, "category", void 0);
43
42
  /**
44
43
  * Stable public identifier used for config, suppression, and reporting.
@@ -66,11 +66,13 @@ let MissingRecordTriggerFilter = class MissingRecordTriggerFilter extends _RuleC
66
66
  }
67
67
  // Check if the flow has filters or entry conditions at the flow level
68
68
  const filters = this.getStartProperty(flow, 'filters');
69
+ const filterFormula = this.getStartProperty(flow, 'filterFormula');
69
70
  const hasFilters = !!filters;
71
+ const hasFilterFormula = !!filterFormula;
70
72
  const scheduledPaths = (_flow_xmldata = flow.xmldata) === null || _flow_xmldata === void 0 ? void 0 : (_flow_xmldata_start = _flow_xmldata.start) === null || _flow_xmldata_start === void 0 ? void 0 : _flow_xmldata_start.scheduledPaths;
71
73
  const hasScheduledPaths = !!scheduledPaths;
72
- // If no filters or scheduled paths (which have their own conditions), flag as violation
73
- if (!hasFilters && !hasScheduledPaths) {
74
+ // If no filters, formula conditions, or scheduled paths (which have their own conditions), flag as violation
75
+ if (!hasFilters && !hasFilterFormula && !hasScheduledPaths) {
74
76
  violations.push(new _internals.Violation(new _internals.FlowAttribute(triggerType, "triggerType", "autolaunched && triggerType")));
75
77
  }
76
78
  return violations;
@@ -0,0 +1,7 @@
1
+ import * as core from "../internals/internals";
2
+ import { RuleCommon } from "../models/RuleCommon";
3
+ import { IRuleDefinition } from "../internals/internals";
4
+ export declare class MissingStartReference extends RuleCommon implements IRuleDefinition {
5
+ constructor();
6
+ protected check(flow: core.Flow, _options: object | undefined, _suppressions: Set<string>): core.Violation[];
7
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "MissingStartReference", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return MissingStartReference;
9
+ }
10
+ });
11
+ const _internals = /*#__PURE__*/ _interop_require_wildcard(require("../internals/internals"));
12
+ const _RuleCommon = require("../models/RuleCommon");
13
+ function _getRequireWildcardCache(nodeInterop) {
14
+ if (typeof WeakMap !== "function") return null;
15
+ var cacheBabelInterop = new WeakMap();
16
+ var cacheNodeInterop = new WeakMap();
17
+ return (_getRequireWildcardCache = function(nodeInterop) {
18
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
19
+ })(nodeInterop);
20
+ }
21
+ function _interop_require_wildcard(obj, nodeInterop) {
22
+ if (!nodeInterop && obj && obj.__esModule) {
23
+ return obj;
24
+ }
25
+ if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
26
+ return {
27
+ default: obj
28
+ };
29
+ }
30
+ var cache = _getRequireWildcardCache(nodeInterop);
31
+ if (cache && cache.has(obj)) {
32
+ return cache.get(obj);
33
+ }
34
+ var newObj = {
35
+ __proto__: null
36
+ };
37
+ var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
38
+ for(var key in obj){
39
+ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
40
+ var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
41
+ if (desc && (desc.get || desc.set)) {
42
+ Object.defineProperty(newObj, key, desc);
43
+ } else {
44
+ newObj[key] = obj[key];
45
+ }
46
+ }
47
+ }
48
+ newObj.default = obj;
49
+ if (cache) {
50
+ cache.set(obj, newObj);
51
+ }
52
+ return newObj;
53
+ }
54
+ let MissingStartReference = class MissingStartReference extends _RuleCommon.RuleCommon {
55
+ check(flow, _options, _suppressions) {
56
+ const violations = [];
57
+ if (!flow.startNode) {
58
+ violations.push(new _internals.Violation(new _internals.FlowAttribute("undefined", "startNode", "startNode")));
59
+ }
60
+ return violations;
61
+ }
62
+ constructor(){
63
+ super({
64
+ ruleId: "missing-start-reference",
65
+ category: "system",
66
+ name: "MissingStartReference",
67
+ label: "Missing Start Reference",
68
+ description: "When a flow has no start reference.",
69
+ summary: "Ensure flow has a start reference node",
70
+ supportedTypes: _internals.FlowType.allTypes(),
71
+ docRefs: []
72
+ }, {
73
+ severity: "error"
74
+ });
75
+ }
76
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flow-scanner/lightning-flow-scanner-core",
3
3
  "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.17.2",
4
+ "version": "6.18.0",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": {