@flow-scanner/lightning-flow-scanner-core 6.18.0 → 6.19.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
@@ -70,20 +70,13 @@ Executing DML operations (insert, update, delete) inside a loop is a high-risk a
70
70
  **Class Name:** _[DMLStatementInLoop](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/DMLStatementInLoop.ts)_
71
71
  **Severity:** 🔴 *Error*
72
72
 
73
- #### Hardcoded Salesforce Id
73
+ #### Hardcoded Id
74
74
  Avoid hard-coding record IDs, as they are unique to a specific org and will not work in other environments. Instead, store IDs in variables—such as merge-field URL parameters or a **Get Records** element—to make the Flow portable, maintainable, and flexible.
75
75
 
76
76
  **Rule ID:** `hardcoded-id`
77
77
  **Class Name:** _[HardcodedId](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/HardcodedId.ts)_
78
78
  **Severity:** 🔴 *Error*
79
79
 
80
- #### Hardcoded Salesforce Url
81
- Avoid hard-coding URLs, as they may change between environments or over time. Instead, store URLs in variables or custom settings to make the Flow adaptable, maintainable, and environment-independent.
82
-
83
- **Rule ID:** `hardcoded-url`
84
- **Class Name:** _[HardcodedUrl](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/HardcodedUrl.ts)_
85
- **Severity:** 🔴 *Error*
86
-
87
80
  #### Hardcoded Secret ![Beta](https://img.shields.io/badge/status-beta-yellow)
88
81
  Avoid hardcoding secrets, API keys, tokens, or credentials in Flows. These should be stored securely in Named Credentials, Custom Settings, Custom Metadata, or external secret management systems.
89
82
 
@@ -91,6 +84,13 @@ Avoid hardcoding secrets, API keys, tokens, or credentials in Flows. These shoul
91
84
  **Class Name:** _[HardcodedSecret](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/HardcodedSecret.ts)_
92
85
  **Severity:** 🔴 *Error*
93
86
 
87
+ #### Hardcoded Url
88
+ Avoid hard-coding URLs, as they may change between environments or over time. Instead, store URLs in variables or custom settings to make the Flow adaptable, maintainable, and environment-independent.
89
+
90
+ **Rule ID:** `hardcoded-url`
91
+ **Class Name:** _[HardcodedUrl](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/HardcodedUrl.ts)_
92
+ **Severity:** 🔴 *Error*
93
+
94
94
  #### Process Builder
95
95
  Process Builder is retired. Continuing to use it increases maintenance overhead and risks future compatibility issues. Migrating automation to Flow reduces risk and improves maintainability.
96
96
 
@@ -167,13 +167,18 @@ Inactive Flows should be deleted or archived to reduce risk. Even when inactive,
167
167
  **Class Name:** _[InactiveFlow](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/InactiveFlow.ts)_
168
168
  **Severity:** 🟡 *Warning*
169
169
 
170
- #### Invalid API Version
170
+ #### Invalid API Version ![Auto-Fix](https://img.shields.io/badge/-auto--fix-green)
171
171
  Flows running on outdated API versions may behave inconsistently when newer platform features or components are used. From API version 50.0 onward, the API Version attribute explicitly controls Flow runtime behavior. Keeping Flows aligned with a supported API version helps prevent compatibility issues and ensures predictable execution.
172
172
 
173
173
  **Rule ID:** `invalid-api-version`
174
174
  **Class Name:** _[APIVersion](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/APIVersion.ts)_
175
175
  **Severity:** 🟡 *Warning*
176
176
 
177
+ | Option | Type | Default | Description |
178
+ |--------|------|---------|-------------|
179
+ | expression | expression | `>= 50` | Comparison expression for API version (e.g., `>= 58`, `< 50`, `=== 60`) |
180
+
181
+
177
182
  #### Missing Filter Record Trigger ![Beta](https://img.shields.io/badge/status-beta-yellow)
178
183
  Record-triggered Flows without filters on changed fields or entry conditions execute on every record change. Adding filters ensures the Flow runs only when needed, improving performance.
179
184
 
@@ -188,6 +193,18 @@ Before-save Flows can safely update the triggering record directly via $Record,
188
193
  **Class Name:** _[SameRecordFieldUpdates](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/SameRecordFieldUpdates.ts)_
189
194
  **Severity:** 🟡 *Warning*
190
195
 
196
+ #### Cognitive Complexity
197
+ Flows with deeply nested loops and decisions are hard to understand. Unlike cyclomatic complexity which counts paths, cognitive complexity penalizes nesting depth. Consider extracting nested logic into subflows.
198
+
199
+ **Rule ID:** `cognitive-complexity`
200
+ **Class Name:** _[CognitiveComplexity](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/CognitiveComplexity.ts)_
201
+ **Severity:** 🔵 *Note*
202
+
203
+ | Option | Type | Default | Description |
204
+ |--------|------|---------|-------------|
205
+ | threshold | number | `15` | Maximum cognitive complexity score before triggering a violation |
206
+
207
+
191
208
  #### Excessive Cyclomatic Complexity
192
209
  High numbers of loops and decision elements increase a Flow's cyclomatic complexity. To maintain simplicity and readability, consider using subflows or splitting a Flow into smaller, ordered Flows.
193
210
 
@@ -195,6 +212,11 @@ High numbers of loops and decision elements increase a Flow's cyclomatic complex
195
212
  **Class Name:** _[CyclomaticComplexity](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/CyclomaticComplexity.ts)_
196
213
  **Severity:** 🔵 *Note*
197
214
 
215
+ | Option | Type | Default | Description |
216
+ |--------|------|---------|-------------|
217
+ | threshold | number | `25` | Maximum cyclomatic complexity score before triggering a violation |
218
+
219
+
198
220
  #### Missing Trigger Order
199
221
  Record-triggered Flows without a specified Trigger Order may execute in an unpredictable sequence. Setting a Trigger Order ensures your Flows run in the intended order.
200
222
 
@@ -229,6 +251,11 @@ Using clear and consistent Flow names improves readability, discoverability, and
229
251
  **Class Name:** _[FlowName](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/FlowName.ts)_
230
252
  **Severity:** 🔴 *Error*
231
253
 
254
+ | Option | Type | Default | Description |
255
+ |--------|------|---------|-------------|
256
+ | expression | expression | `[A-Za-z0-9]+_[A-Za-z0-9]+` | Regex pattern for valid Flow names |
257
+
258
+
232
259
  #### Missing Flow Description
233
260
  Flow descriptions are essential for documentation and maintainability. Include a description for each Flow, explaining its purpose and where it's used.
234
261
 
@@ -250,21 +277,21 @@ Elements with unclear or duplicated API names, like Copy_X_Of_Element, reduce Fl
250
277
  **Class Name:** _[CopyAPIName](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/CopyAPIName.ts)_
251
278
  **Severity:** 🟡 *Warning*
252
279
 
253
- #### Unreachable Element
280
+ #### Unreachable Element ![Auto-Fix](https://img.shields.io/badge/-auto--fix-green)
254
281
  Unconnected elements never execute and add unnecessary clutter. Remove or connect unused Flow elements to keep Flows clean and efficient.
255
282
 
256
283
  **Rule ID:** `unreachable-element`
257
284
  **Class Name:** _[UnconnectedElement](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/UnconnectedElement.ts)_
258
285
  **Severity:** 🟡 *Warning*
259
286
 
260
- #### Unused Variable
287
+ #### Unused Variable ![Auto-Fix](https://img.shields.io/badge/-auto--fix-green)
261
288
  Unused variables are never referenced and add unnecessary clutter. Remove them to keep Flows efficient and easy to maintain.
262
289
 
263
290
  **Rule ID:** `unused-variable`
264
291
  **Class Name:** _[UnusedVariable](https://github.com/Flow-Scanner/lightning-flow-scanner/blob/main/packages/core/src/main/rules/UnusedVariable.ts)_
265
292
  **Severity:** 🟡 *Warning*
266
293
 
267
- #### Missing Auto Layout
294
+ #### Missing Auto Layout ![Auto-Fix](https://img.shields.io/badge/-auto--fix-green)
268
295
  Auto-Layout automatically arranges and aligns Flow elements, keeping the canvas organized and easier to maintain. Enabling it saves time and improves readability.
269
296
 
270
297
  **Rule ID:** `missing-auto-layout`
@@ -340,9 +367,9 @@ Available values for severity are `error`, `warning` and `note`. If no severity
340
367
  }
341
368
  ```
342
369
 
343
- #### Override Expressions
370
+ #### Customize Rules
344
371
 
345
- Some rules are configurable and allow overriding their default expressions. You configure these overrides the same way as severity, as shown in the examples below.
372
+ Some rules are configurable and allow overriding their default expressions, or setting a threshold as shown in the examples below.
346
373
 
347
374
  ```json
348
375
  {
@@ -352,12 +379,15 @@ Some rules are configurable and allow overriding their default expressions. You
352
379
  },
353
380
  "invalid-naming-convention": {
354
381
  "expression": "[A-Za-z0-9]" // regular expression
382
+ },
383
+ "excessive-cyclomatic-complexity": {
384
+ "threshold": 10 // threshold
355
385
  }
356
386
  }
357
387
  }
358
388
  ```
359
389
 
360
- #### Customize Messages
390
+ #### Customize Rule Messages
361
391
 
362
392
  If not provided, `message` shows the standard rule summary and `messageUrl` links to the README; providing either overrides the default behavior.
363
393
 
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "ruleRegistry", {
11
11
  const _ActionCallsInLoop = require("../rules/ActionCallsInLoop");
12
12
  const _APIVersion = require("../rules/APIVersion");
13
13
  const _AutoLayout = require("../rules/AutoLayout");
14
+ const _CognitiveComplexity = require("../rules/CognitiveComplexity");
14
15
  const _CopyAPIName = require("../rules/CopyAPIName");
15
16
  const _CyclomaticComplexity = require("../rules/CyclomaticComplexity");
16
17
  const _DMLStatementInLoop = require("../rules/DMLStatementInLoop");
@@ -181,6 +182,7 @@ registry.register("action-call-in-loop", _ActionCallsInLoop.ActionCallsInLoop, "
181
182
  registry.register("invalid-api-version", _APIVersion.APIVersion, "APIVersion");
182
183
  registry.register("missing-auto-layout", _AutoLayout.AutoLayout, "AutoLayout");
183
184
  registry.register("unclear-api-naming", _CopyAPIName.CopyAPIName, "CopyAPIName");
185
+ registry.register("cognitive-complexity", _CognitiveComplexity.CognitiveComplexity, "CognitiveComplexity");
184
186
  registry.register("excessive-cyclomatic-complexity", _CyclomaticComplexity.CyclomaticComplexity, "CyclomaticComplexity");
185
187
  registry.register("dml-in-loop", _DMLStatementInLoop.DMLStatementInLoop, "DMLStatementInLoop");
186
188
  registry.register("duplicate-dml", _DuplicateDMLOperation.DuplicateDMLOperation, "DuplicateDMLOperation");
@@ -1,4 +1,5 @@
1
1
  import { Flow, RuleResult } from "../internals/internals";
2
+ import { ConfigurableOption } from "../models/RuleInfo";
2
3
  export interface IRuleDefinition {
3
4
  ruleId: string;
4
5
  category?: 'problem' | 'suggestion' | 'layout' | 'system';
@@ -10,6 +11,8 @@ export interface IRuleDefinition {
10
11
  }>;
11
12
  execute(flow: Flow, options?: object, suppressions?: string[]): RuleResult;
12
13
  isConfigurable: boolean;
14
+ configurableOptions?: ConfigurableOption[];
15
+ isFixable: boolean;
13
16
  label: string;
14
17
  name: string;
15
18
  severity?: string;
@@ -5,6 +5,7 @@ import { FlatViolation } from "../models/FlatViolation";
5
5
  import { Flow } from "../models/Flow";
6
6
  import { FlowAttribute } from "../models/FlowAttribute";
7
7
  import { FlowElement } from "../models/FlowElement";
8
+ import { FlowGraph } from "../models/FlowGraph";
8
9
  import { FlowNode } from "../models/FlowNode";
9
10
  import { FlowResource } from "../models/FlowResource";
10
11
  import { FlowType } from "../models/FlowType";
@@ -14,5 +15,5 @@ import { RuleCommon } from "../models/RuleCommon";
14
15
  import { RuleResult } from "../models/RuleResult";
15
16
  import { ScanResult } from "../models/ScanResult";
16
17
  import { Violation } from "../models/Violation";
17
- export { FlowAttribute, FlowElement, FlowNode, FlowType, FlowVariable, FlowResource, Flow, Compiler, ScanResult, RuleResult, Violation, RuleCommon, ParsedFlow, };
18
+ export { FlowAttribute, FlowElement, FlowGraph, FlowNode, FlowType, FlowVariable, FlowResource, Flow, Compiler, ScanResult, RuleResult, Violation, RuleCommon, ParsedFlow, };
18
19
  export type { IRuleDefinition, IRulesConfig, FlatViolation };
@@ -21,6 +21,9 @@ _export(exports, {
21
21
  get FlowElement () {
22
22
  return _FlowElement.FlowElement;
23
23
  },
24
+ get FlowGraph () {
25
+ return _FlowGraph.FlowGraph;
26
+ },
24
27
  get FlowNode () {
25
28
  return _FlowNode.FlowNode;
26
29
  },
@@ -53,6 +56,7 @@ const _Compiler = require("../libs/Compiler");
53
56
  const _Flow = require("../models/Flow");
54
57
  const _FlowAttribute = require("../models/FlowAttribute");
55
58
  const _FlowElement = require("../models/FlowElement");
59
+ const _FlowGraph = require("../models/FlowGraph");
56
60
  const _FlowNode = require("../models/FlowNode");
57
61
  const _FlowResource = require("../models/FlowResource");
58
62
  const _FlowType = require("../models/FlowType");
@@ -1,5 +1,5 @@
1
1
  import * as core from "../internals/internals";
2
- export declare function fix(results: core.ScanResult[]): core.ScanResult[];
2
+ export declare function fix(results: core.ScanResult[], ruleOptions?: Map<string, unknown>): core.ScanResult[];
3
3
  /**
4
4
  * @deprecated Use fix() instead which modifies flows in place.
5
5
  * Kept for backward compatibility.
@@ -58,17 +58,24 @@ function _interop_require_wildcard(obj, nodeInterop) {
58
58
  }
59
59
  return newObj;
60
60
  }
61
- function fix(results) {
61
+ function fix(results, ruleOptions) {
62
62
  const newResults = [];
63
63
  for (const result of results){
64
64
  if (!result.ruleResults || result.ruleResults.length === 0) continue;
65
- const fixables = result.ruleResults.filter((r)=>r.ruleName === "UnusedVariable" && r.occurs || r.ruleName === "UnconnectedElement" && r.occurs || r.ruleName === "AutoLayout" && r.occurs);
65
+ const fixables = result.ruleResults.filter((r)=>r.ruleName === "UnusedVariable" && r.occurs || r.ruleName === "UnconnectedElement" && r.occurs || r.ruleName === "AutoLayout" && r.occurs || r.ruleName === "APIVersion" && r.occurs);
66
66
  if (fixables.length === 0) continue;
67
67
  // Handle AutoLayout fix separately (modifies metadata, not elements)
68
68
  const autoLayoutFix = fixables.find((r)=>r.ruleName === "AutoLayout");
69
69
  if (autoLayoutFix) {
70
70
  applyAutoLayoutFix(result.flow);
71
71
  }
72
+ // Handle APIVersion fix (modifies apiVersion attribute)
73
+ const apiVersionFix = fixables.find((r)=>r.ruleName === "APIVersion");
74
+ if (apiVersionFix) {
75
+ var _ruleOptions_get;
76
+ const options = (_ruleOptions_get = ruleOptions === null || ruleOptions === void 0 ? void 0 : ruleOptions.get("invalid-api-version")) !== null && _ruleOptions_get !== void 0 ? _ruleOptions_get : ruleOptions === null || ruleOptions === void 0 ? void 0 : ruleOptions.get("APIVersion");
77
+ applyAPIVersionFix(result.flow, options);
78
+ }
72
79
  // Handle element-based fixes (UnusedVariable, UnconnectedElement)
73
80
  // These modify xmldata in place to preserve element order and formatting
74
81
  const elementFixables = fixables.filter((r)=>r.ruleName !== "AutoLayout");
@@ -107,6 +114,39 @@ function applyAutoLayoutFix(flow) {
107
114
  // Update the flow's processMetadataValues property
108
115
  flow.processMetadataValues = flow.xmldata.processMetadataValues;
109
116
  }
117
+ /**
118
+ * Parse an API version expression and return the target version for auto-fix.
119
+ * Fixable expressions: >= N, === N, > N
120
+ * Returns undefined for unfixable expressions (<, <=, !==)
121
+ */ function parseAPIVersionExpression(expression) {
122
+ if (!expression) {
123
+ // Default behavior: >= 50
124
+ return 50;
125
+ }
126
+ const match = expression.match(/^\s*(>=|<=|>|<|===|!==)\s*(\d+)\s*$/);
127
+ if (!match) return undefined;
128
+ const [, operator, versionStr] = match;
129
+ const version = parseInt(versionStr, 10);
130
+ switch(operator){
131
+ case '>=':
132
+ case '===':
133
+ return version;
134
+ case '>':
135
+ return version + 1;
136
+ // These don't have a clear target version
137
+ case '<':
138
+ case '<=':
139
+ case '!==':
140
+ default:
141
+ return undefined;
142
+ }
143
+ }
144
+ function applyAPIVersionFix(flow, options) {
145
+ if (!flow.xmldata) return;
146
+ const targetVersion = parseAPIVersionExpression(options === null || options === void 0 ? void 0 : options.expression);
147
+ if (targetVersion === undefined) return;
148
+ flow.xmldata.apiVersion = targetVersion.toString();
149
+ }
110
150
  /**
111
151
  * Apply element-based fixes (UnusedVariable, UnconnectedElement) by modifying xmldata in place.
112
152
  * This preserves element order and formatting from the original file.
@@ -116,5 +116,17 @@ function ScanFlows(flows, ruleOptions) {
116
116
  });
117
117
  });
118
118
  }
119
+ // Apply threshold filtering if specified
120
+ const threshold = ruleOptions === null || ruleOptions === void 0 ? void 0 : ruleOptions.threshold;
121
+ if (threshold && threshold !== 'never') {
122
+ for (const scanResult of flowResults){
123
+ scanResult.ruleResults = scanResult.ruleResults.filter((ruleResult)=>{
124
+ // Get effective severity (config override or rule default)
125
+ const config = getRuleConfigByIdOrName(ruleResult.ruleDefinition, ruleOptions === null || ruleOptions === void 0 ? void 0 : ruleOptions.rules);
126
+ const severity = (config === null || config === void 0 ? void 0 : config.severity) || ruleResult.severity || 'warning';
127
+ return (0, _IRulesConfig.meetsThreshold)(severity, threshold);
128
+ });
129
+ }
130
+ }
119
131
  return flowResults;
120
132
  }
@@ -1,4 +1,4 @@
1
- import { RuleInfo } from "./RuleInfo";
1
+ import { RuleInfo, ConfigurableOption } from "./RuleInfo";
2
2
  import * as core from "../internals/internals";
3
3
  export declare abstract class RuleCommon {
4
4
  category?: 'problem' | 'suggestion' | 'layout' | 'system';
@@ -9,6 +9,8 @@ export declare abstract class RuleCommon {
9
9
  path: string;
10
10
  }>;
11
11
  isConfigurable: boolean;
12
+ configurableOptions?: ConfigurableOption[];
13
+ isFixable: boolean;
12
14
  label: string;
13
15
  name: string;
14
16
  severity?: string;
@@ -132,6 +132,8 @@ let RuleCommon = class RuleCommon {
132
132
  _define_property(this, "summary", void 0);
133
133
  _define_property(this, "docRefs", []);
134
134
  _define_property(this, "isConfigurable", void 0);
135
+ _define_property(this, "configurableOptions", void 0);
136
+ _define_property(this, "isFixable", void 0);
135
137
  _define_property(this, "label", void 0);
136
138
  _define_property(this, "name", void 0);
137
139
  _define_property(this, "severity", void 0);
@@ -147,13 +149,10 @@ let RuleCommon = class RuleCommon {
147
149
  this.summary = info.summary;
148
150
  this.uri = `https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner/tree/main/src/main/rules/${info.name}.ts`;
149
151
  this.docRefs = info.docRefs;
150
- const checkImpl = this.check;
151
- if (typeof checkImpl === "function") {
152
- const source = checkImpl.toString();
153
- this.isConfigurable = /options[.\?]/.test(source);
154
- } else {
155
- this.isConfigurable = false;
156
- }
152
+ this.configurableOptions = info.configurableOptions;
153
+ this.isConfigurable = !!(info.configurableOptions && info.configurableOptions.length > 0);
154
+ var _info_isFixable;
155
+ this.isFixable = (_info_isFixable = info.isFixable) !== null && _info_isFixable !== void 0 ? _info_isFixable : false;
157
156
  var _optional_severity;
158
157
  this.severity = (_optional_severity = optional === null || optional === void 0 ? void 0 : optional.severity) !== null && _optional_severity !== void 0 ? _optional_severity : "warning";
159
158
  }
@@ -3,6 +3,15 @@ export type RuleDefinitionExpression = {
3
3
  expression?: unknown;
4
4
  };
5
5
  };
6
+ /**
7
+ * Describes a configurable option for a rule.
8
+ */
9
+ export interface ConfigurableOption {
10
+ name: string;
11
+ type: 'number' | 'string' | 'boolean' | 'expression';
12
+ description: string;
13
+ defaultValue?: unknown;
14
+ }
6
15
  /**
7
16
  * Represents a rule metadata; this contains properties to describe the rule
8
17
  */
@@ -45,4 +54,13 @@ export declare class RuleInfo {
45
54
  * Use defined types in @see FlowType
46
55
  */
47
56
  supportedTypes: string[];
57
+ /**
58
+ * Configurable options for this rule.
59
+ * Used for documentation generation.
60
+ */
61
+ configurableOptions?: ConfigurableOption[];
62
+ /**
63
+ * Whether this rule supports auto-fix.
64
+ */
65
+ isFixable?: boolean;
48
66
  }
@@ -50,5 +50,12 @@ let RuleInfo = class RuleInfo {
50
50
  * The types supported by this rule (e.g., Flow, Process).
51
51
  * Use defined types in @see FlowType
52
52
  */ _define_property(this, "supportedTypes", void 0);
53
+ /**
54
+ * Configurable options for this rule.
55
+ * Used for documentation generation.
56
+ */ _define_property(this, "configurableOptions", void 0);
57
+ /**
58
+ * Whether this rule supports auto-fix.
59
+ */ _define_property(this, "isFixable", void 0);
53
60
  }
54
61
  };
@@ -120,7 +120,16 @@ let APIVersion = class APIVersion extends _RuleCommon.RuleCommon {
120
120
  description: "Flows running on outdated API versions may behave inconsistently when newer platform features or components are used. From API version 50.0 onward, the API Version attribute explicitly controls Flow runtime behavior. Keeping Flows aligned with a supported API version helps prevent compatibility issues and ensures predictable execution.",
121
121
  summary: "Outdated API versions risk compatibility issues",
122
122
  supportedTypes: _internals.FlowType.allTypes(),
123
- docRefs: []
123
+ docRefs: [],
124
+ configurableOptions: [
125
+ {
126
+ name: "expression",
127
+ type: "expression",
128
+ description: "Comparison expression for API version (e.g., `>= 58`, `< 50`, `=== 60`)",
129
+ defaultValue: ">= 50"
130
+ }
131
+ ],
132
+ isFixable: true
124
133
  });
125
134
  }
126
135
  };
@@ -72,7 +72,8 @@ let AutoLayout = class AutoLayout extends _RuleCommon.RuleCommon {
72
72
  description: "Auto-Layout automatically arranges and aligns Flow elements, keeping the canvas organized and easier to maintain. Enabling it saves time and improves readability.",
73
73
  summary: "Auto-Layout improves canvas organization and readability",
74
74
  supportedTypes: _internals.FlowType.allTypes(),
75
- docRefs: []
75
+ docRefs: [],
76
+ isFixable: true
76
77
  }, {
77
78
  severity: "note"
78
79
  });
@@ -0,0 +1,33 @@
1
+ import * as core from "../internals/internals";
2
+ import { RuleCommon } from "../models/RuleCommon";
3
+ import { IRuleDefinition } from "../interfaces/IRuleDefinition";
4
+ /**
5
+ * Cognitive Complexity Rule
6
+ *
7
+ * Unlike cyclomatic complexity which counts paths, cognitive complexity
8
+ * measures how hard a flow is to understand by penalizing:
9
+ * - Control flow structures (loops, decisions) +1 each
10
+ * - Nesting depth: each level of nesting adds extra penalty
11
+ *
12
+ * A decision inside a loop is harder to understand than a flat sequence.
13
+ */
14
+ export declare class CognitiveComplexity extends RuleCommon implements IRuleDefinition {
15
+ private defaultThreshold;
16
+ constructor();
17
+ protected check(flow: core.Flow, options: {
18
+ threshold?: number;
19
+ } | undefined): core.Violation[];
20
+ /**
21
+ * Calculate cognitive complexity for a flow.
22
+ *
23
+ * Algorithm:
24
+ * 1. Find all loops and decisions
25
+ * 2. Calculate nesting depth for each (how many loops/decisions contain it)
26
+ * 3. Add 1 + nesting_depth for each control structure
27
+ */
28
+ private calculateCognitiveComplexity;
29
+ /**
30
+ * Count how many parent loops contain this loop
31
+ */
32
+ private countParentLoops;
33
+ }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "CognitiveComplexity", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return CognitiveComplexity;
9
+ }
10
+ });
11
+ const _internals = /*#__PURE__*/ _interop_require_wildcard(require("../internals/internals"));
12
+ const _RuleCommon = require("../models/RuleCommon");
13
+ function _define_property(obj, key, value) {
14
+ if (key in obj) {
15
+ Object.defineProperty(obj, key, {
16
+ value: value,
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true
20
+ });
21
+ } else {
22
+ obj[key] = value;
23
+ }
24
+ return obj;
25
+ }
26
+ function _getRequireWildcardCache(nodeInterop) {
27
+ if (typeof WeakMap !== "function") return null;
28
+ var cacheBabelInterop = new WeakMap();
29
+ var cacheNodeInterop = new WeakMap();
30
+ return (_getRequireWildcardCache = function(nodeInterop) {
31
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
32
+ })(nodeInterop);
33
+ }
34
+ function _interop_require_wildcard(obj, nodeInterop) {
35
+ if (!nodeInterop && obj && obj.__esModule) {
36
+ return obj;
37
+ }
38
+ if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
39
+ return {
40
+ default: obj
41
+ };
42
+ }
43
+ var cache = _getRequireWildcardCache(nodeInterop);
44
+ if (cache && cache.has(obj)) {
45
+ return cache.get(obj);
46
+ }
47
+ var newObj = {
48
+ __proto__: null
49
+ };
50
+ var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
51
+ for(var key in obj){
52
+ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
53
+ var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
54
+ if (desc && (desc.get || desc.set)) {
55
+ Object.defineProperty(newObj, key, desc);
56
+ } else {
57
+ newObj[key] = obj[key];
58
+ }
59
+ }
60
+ }
61
+ newObj.default = obj;
62
+ if (cache) {
63
+ cache.set(obj, newObj);
64
+ }
65
+ return newObj;
66
+ }
67
+ let CognitiveComplexity = class CognitiveComplexity extends _RuleCommon.RuleCommon {
68
+ check(flow, options) {
69
+ var _options_threshold;
70
+ const threshold = (_options_threshold = options === null || options === void 0 ? void 0 : options.threshold) !== null && _options_threshold !== void 0 ? _options_threshold : this.defaultThreshold;
71
+ const complexity = this.calculateCognitiveComplexity(flow);
72
+ if (complexity > threshold) {
73
+ return [
74
+ new _internals.Violation(new _internals.FlowAttribute(`${complexity}`, "CognitiveComplexity", `>${threshold}`))
75
+ ];
76
+ }
77
+ return [];
78
+ }
79
+ /**
80
+ * Calculate cognitive complexity for a flow.
81
+ *
82
+ * Algorithm:
83
+ * 1. Find all loops and decisions
84
+ * 2. Calculate nesting depth for each (how many loops/decisions contain it)
85
+ * 3. Add 1 + nesting_depth for each control structure
86
+ */ calculateCognitiveComplexity(flow) {
87
+ let complexity = 0;
88
+ const graph = flow.graph;
89
+ // Get all loops and decisions
90
+ const loops = flow.elements.filter((e)=>e.subtype === "loops");
91
+ const decisions = flow.elements.filter((e)=>e.subtype === "decisions");
92
+ // Build a map of which elements are contained within which control structures
93
+ const nestingDepth = new Map();
94
+ // Calculate nesting depth for each element
95
+ for (const element of flow.elements){
96
+ if (!(element instanceof _internals.FlowNode)) continue;
97
+ let depth = 0;
98
+ // Check if inside any loop
99
+ if (graph.isInLoop(element.name)) {
100
+ depth++;
101
+ // Check for nested loops (loop inside loop)
102
+ const containingLoop = graph.getContainingLoop(element.name);
103
+ if (containingLoop && containingLoop !== element.name) {
104
+ // Check if the containing loop is itself inside another loop
105
+ depth += this.countParentLoops(containingLoop, graph, loops);
106
+ }
107
+ }
108
+ nestingDepth.set(element.name, depth);
109
+ }
110
+ // Add complexity for each loop: 1 + nesting depth
111
+ for (const loop of loops){
112
+ var _nestingDepth_get;
113
+ const depth = (_nestingDepth_get = nestingDepth.get(loop.name)) !== null && _nestingDepth_get !== void 0 ? _nestingDepth_get : 0;
114
+ complexity += 1 + depth;
115
+ }
116
+ // Add complexity for each decision: 1 + nesting depth
117
+ // Also count additional branches beyond 2 (if-else is base, more adds complexity)
118
+ for (const decision of decisions){
119
+ var _decision_rules;
120
+ var _nestingDepth_get1;
121
+ const depth = (_nestingDepth_get1 = nestingDepth.get(decision.name)) !== null && _nestingDepth_get1 !== void 0 ? _nestingDepth_get1 : 0;
122
+ var _decision_rules_length;
123
+ const rulesCount = (_decision_rules_length = (_decision_rules = decision.rules) === null || _decision_rules === void 0 ? void 0 : _decision_rules.length) !== null && _decision_rules_length !== void 0 ? _decision_rules_length : 0;
124
+ // Base complexity: 1 + nesting depth
125
+ complexity += 1 + depth;
126
+ // Additional complexity for multiple branches (beyond binary decision)
127
+ // Each additional rule beyond the first adds complexity
128
+ if (rulesCount > 1) {
129
+ complexity += rulesCount - 1;
130
+ }
131
+ }
132
+ return complexity;
133
+ }
134
+ /**
135
+ * Count how many parent loops contain this loop
136
+ */ countParentLoops(loopName, graph, allLoops) {
137
+ let count = 0;
138
+ for (const parentLoop of allLoops){
139
+ if (parentLoop.name === loopName) continue;
140
+ // Check if loopName is within this parent loop's body
141
+ const loopElements = graph.getLoopElements(parentLoop.name);
142
+ if (loopElements.has(loopName)) {
143
+ count++;
144
+ }
145
+ }
146
+ return count;
147
+ }
148
+ constructor(){
149
+ super({
150
+ ruleId: "cognitive-complexity",
151
+ category: "suggestion",
152
+ name: "CognitiveComplexity",
153
+ label: "Cognitive Complexity",
154
+ description: "Flows with deeply nested loops and decisions are hard to understand. Unlike cyclomatic complexity which counts paths, cognitive complexity penalizes nesting depth. Consider extracting nested logic into subflows.",
155
+ summary: "Deeply nested logic harms readability",
156
+ supportedTypes: _internals.FlowType.backEndTypes,
157
+ docRefs: [
158
+ {
159
+ label: "Cognitive Complexity is a measure of how difficult code is to understand, as opposed to Cyclomatic Complexity which measures testability.",
160
+ path: "https://www.sonarsource.com/docs/CognitiveComplexity.pdf"
161
+ }
162
+ ],
163
+ configurableOptions: [
164
+ {
165
+ name: "threshold",
166
+ type: "number",
167
+ description: "Maximum cognitive complexity score before triggering a violation",
168
+ defaultValue: 15
169
+ }
170
+ ]
171
+ }, {
172
+ severity: "note"
173
+ }), _define_property(this, "defaultThreshold", 15);
174
+ }
175
+ };
@@ -67,7 +67,8 @@ function _interop_require_wildcard(obj, nodeInterop) {
67
67
  let CyclomaticComplexity = class CyclomaticComplexity extends _RuleCommon.RuleCommon {
68
68
  check(flow, options) {
69
69
  var _flow_elements, _flow_elements1;
70
- const threshold = (options === null || options === void 0 ? void 0 : options.threshold) || this.defaultThreshold;
70
+ var _options_threshold;
71
+ const threshold = (_options_threshold = options === null || options === void 0 ? void 0 : options.threshold) !== null && _options_threshold !== void 0 ? _options_threshold : this.defaultThreshold;
71
72
  let cyclomaticComplexity = 1;
72
73
  const flowDecisions = flow === null || flow === void 0 ? void 0 : (_flow_elements = flow.elements) === null || _flow_elements === void 0 ? void 0 : _flow_elements.filter((node)=>node.subtype === "decisions");
73
74
  const flowLoops = flow === null || flow === void 0 ? void 0 : (_flow_elements1 = flow.elements) === null || _flow_elements1 === void 0 ? void 0 : _flow_elements1.filter((node)=>node.subtype === "loops");
@@ -99,6 +100,14 @@ let CyclomaticComplexity = class CyclomaticComplexity extends _RuleCommon.RuleCo
99
100
  label: `Cyclomatic complexity is a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code.`,
100
101
  path: "https://en.wikipedia.org/wiki/Cyclomatic_complexity"
101
102
  }
103
+ ],
104
+ configurableOptions: [
105
+ {
106
+ name: "threshold",
107
+ type: "number",
108
+ description: "Maximum cyclomatic complexity score before triggering a violation",
109
+ defaultValue: 25
110
+ }
102
111
  ]
103
112
  }, {
104
113
  severity: "note"
@@ -91,7 +91,15 @@ let FlowName = class FlowName extends _RuleCommon.RuleCommon {
91
91
  ],
92
92
  label: "Flow Naming Convention",
93
93
  name: "FlowName",
94
- supportedTypes: _internals.FlowType.allTypes()
94
+ supportedTypes: _internals.FlowType.allTypes(),
95
+ configurableOptions: [
96
+ {
97
+ name: "expression",
98
+ type: "expression",
99
+ description: "Regex pattern for valid Flow names",
100
+ defaultValue: "[A-Za-z0-9]+_[A-Za-z0-9]+"
101
+ }
102
+ ]
95
103
  }, {
96
104
  severity: "error"
97
105
  }), _define_property(this, "regexRule", new _regexscanner.NamingConvention());
@@ -80,7 +80,7 @@ let HardcodedId = class HardcodedId extends _RuleCommon.RuleCommon {
80
80
  ruleId: "hardcoded-id",
81
81
  name: "HardcodedId",
82
82
  category: "problem",
83
- label: "Hardcoded Salesforce Id",
83
+ label: "Hardcoded Id",
84
84
  description: "Avoid hard-coding record IDs, as they are unique to a specific org and will not work in other environments. Instead, store IDs in variables—such as merge-field URL parameters or a **Get Records** element—to make the Flow portable, maintainable, and flexible.",
85
85
  summary: "Hardcoded IDs break portability across environments",
86
86
  supportedTypes: _internals.FlowType.allTypes(),
@@ -50,7 +50,7 @@ let HardcodedUrl = class HardcodedUrl extends _RuleCommon.RuleCommon {
50
50
  path: "https://admin.salesforce.com/blog/2021/why-you-should-avoid-hard-coding-and-three-alternative-solutions"
51
51
  }
52
52
  ],
53
- label: "Hardcoded Salesforce Url",
53
+ label: "Hardcoded Url",
54
54
  name: "HardcodedUrl",
55
55
  supportedTypes: _internals.FlowType.allTypes()
56
56
  }, {
@@ -71,7 +71,8 @@ let UnconnectedElement = class UnconnectedElement extends _RuleCommon.RuleCommon
71
71
  supportedTypes: [
72
72
  ..._internals.FlowType.backEndTypes,
73
73
  ..._internals.FlowType.visualTypes
74
- ]
74
+ ],
75
+ isFixable: true
75
76
  });
76
77
  }
77
78
  };
@@ -89,7 +89,8 @@ let UnusedVariable = class UnusedVariable extends _RuleCommon.RuleCommon {
89
89
  ..._internals.FlowType.backEndTypes,
90
90
  ..._internals.FlowType.visualTypes
91
91
  ],
92
- docRefs: []
92
+ docRefs: [],
93
+ isFixable: true
93
94
  });
94
95
  }
95
96
  };
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.18.0",
4
+ "version": "6.19.0",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": {
@@ -20,7 +20,7 @@
20
20
  "directory": "packages/core"
21
21
  },
22
22
  "dependencies": {
23
- "@flow-scanner/regex-scanner": "1.0.0",
23
+ "@flow-scanner/regex-scanner": "1.1.0",
24
24
  "fast-xml-parser": "^5.3.0"
25
25
  },
26
26
  "homepage": "https://flow-scanner.github.io/lightning-flow-scanner/",