Skip to content
Merged
24 changes: 1 addition & 23 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,30 +172,8 @@ const config: Config = {
"^.+\\.(t|j)sx?$": [
"@swc/jest",
{
$schema: "https://swc.rs/schema.json",
sourceMaps: "inline",
module: {
type: "es6",
strictMode: true,
noInterop: false,
resolveFully: false,
},
jsc: {
externalHelpers: false,
target: "es2015",
parser: {
syntax: "typescript",
tsx: true,
decorators: true,
dynamicImport: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: false,
},
keepClassNames: true,
baseUrl: ".",
},
minify: false,
},
],
},
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ _An Extensible Rule Engine capable of conducting static analysis on the metadata
| **Unsafe Running Context** ([`UnsafeRunningContext`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/UnsafeRunningContext.ts)) | This flow is configured to run in System Mode without Sharing. This system context grants all running users the permission to view and edit all data in your org. Running a flow in System Mode without Sharing can lead to unsafe data access. |
| **Same Record Field Updates** ([`SameRecordFieldUpdates`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/SameRecordFieldUpdates.ts)) | Much like triggers, before contexts can update the same record by accessing the trigger variables `$Record` without needing to invoke a DML. |
| **Trigger Order** ([`TriggerOrder`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/TriggerOrder.ts)) | Guarantee your flow execution order with the Trigger Order property introduced in Spring '22 |
| **Cyclomatic Complexity** ([`CyclomaticComplexity`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/TriggerOrder.ts)) | The number of loops and decision rules, plus the number of decisions. Use a combination of 1) subflows and 2) breaking flows into multiple concise trigger ordered flows, to reduce the cyclomatic complexity within a single flow, ensuring maintainability and simplicity. |

## Core Functions

Expand Down
6 changes: 1 addition & 5 deletions src/main/libs/ParseFlows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from "fs";
import { convert } from "xmlbuilder2";
import { ParsedFlow } from "../models/ParsedFlow";

export async function ParseFlows(selectedUris: string[]): Promise<ParsedFlow[]> {
export async function parse(selectedUris: string[]): Promise<ParsedFlow[]> {
const parseResults: ParsedFlow[] = [];
for (const uri of selectedUris) {
try {
Expand All @@ -19,7 +19,3 @@ export async function ParseFlows(selectedUris: string[]): Promise<ParsedFlow[]>
}
return parseResults;
}

export function parse(selectedUris: string[]): Promise<ParsedFlow[]> {
return ParseFlows(selectedUris);
}
2 changes: 1 addition & 1 deletion src/main/models/FlowElement.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export class FlowElement {
public subtype: string;
public metaType: string;
public element: string | object = {};
public element: string | object[] | object = {};
public connectors?: object[];
public name?: string;
public locationX?: string;
Expand Down
66 changes: 66 additions & 0 deletions src/main/rules/CyclomaticComplexity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { RuleCommon } from "../models/RuleCommon";
import * as core from "../internals/internals";

export class CyclomaticComplexity extends RuleCommon implements core.IRuleDefinition {
constructor() {
super(
{
name: "CyclomaticComplexity",
label: "Cyclomatic Complexity",
description: `The number of loops and decision rules, plus the number of decisions.
Use a combination of 1) subflows and 2) breaking flows into multiple concise trigger ordered flows,
to reduce the cyclomatic complexity within a single flow, ensuring maintainability and simplicity.`,
supportedTypes: core.FlowType.backEndTypes,
docRefs: [
{
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.`,
path: "https://en.wikipedia.org/wiki/Cyclomatic_complexity",
},
],
isConfigurable: true,
autoFixable: false,
},
{ severity: "note" }
);
}

private defaultThreshold: number = 25;

private cyclomaticComplexityUnit: number = 0;

public execute(flow: core.Flow, options?: { threshold: number }): core.RuleResult {
// Set Threshold
const threshold = options?.threshold || this.defaultThreshold;

// Calculate Cyclomatic Complexity based on the number of decision rules and loops, adding the number of decisions plus 1.
let cyclomaticComplexity = 1;

const flowDecisions = flow?.elements?.filter(
(node) => node.subtype === "decisions"
) as core.FlowElement[];
const flowLoops = flow?.elements?.filter((node) => node.subtype === "loops");

for (const decision of flowDecisions || []) {
const rules = decision.element["rules"];
if (Array.isArray(rules)) {
cyclomaticComplexity += rules.length + 1;
} else {
cyclomaticComplexity += 1;
}
}
cyclomaticComplexity += flowLoops?.length ?? 0;

this.cyclomaticComplexityUnit = cyclomaticComplexity; // for unit testing

const results: core.ResultDetails[] = [];
if (cyclomaticComplexity > threshold) {
results.push(
new core.ResultDetails(
new core.FlowAttribute(`${cyclomaticComplexity}`, "CyclomaticComplexity", `>${threshold}`)
)
);
}
return new core.RuleResult(this, results);
}
}
2 changes: 2 additions & 0 deletions src/main/store/DefaultRuleStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { APIVersion } from "../rules/APIVersion";
import { AutoLayout } from "../rules/AutoLayout";
import { CopyAPIName } from "../rules/CopyAPIName";
import { CyclomaticComplexity } from "../rules/CyclomaticComplexity";
import { DMLStatementInLoop } from "../rules/DMLStatementInLoop";
import { DuplicateDMLOperation } from "../rules/DuplicateDMLOperation";
import { FlowDescription } from "../rules/FlowDescription";
Expand All @@ -20,6 +21,7 @@ export const DefaultRuleStore: object = {
APIVersion,
AutoLayout,
CopyAPIName,
CyclomaticComplexity,
DMLStatementInLoop,
DuplicateDMLOperation,
FlowDescription,
Expand Down
123 changes: 123 additions & 0 deletions tests/CyclomaticComplexity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as path from "path";
import { describe, it, expect } from "@jest/globals";

import * as core from "../src";
import { CyclomaticComplexity } from "../src/main/rules/CyclomaticComplexity";

describe("CyclomaticComplexity ", () => {
const example_uri = path.join(__dirname, "./xmlfiles/Cyclomatic_Complexity.flow-meta.xml");
const other_uri = path.join(__dirname, "./xmlfiles/SOQL_Query_In_A_Loop.flow-meta.xml");
const defaultConfig = {
rules: {
CyclomaticComplexity: {
severity: "error",
},
},
};

it("should have a result when there are more than 25 decision options", async () => {
const flows = await core.parse([example_uri]);
debugger;
const results: core.ScanResult[] = core.scan(flows, defaultConfig);
const occurringResults = results[0].ruleResults.filter((rule) => rule.occurs);
expect(occurringResults).toHaveLength(1);
expect(occurringResults[0].ruleName).toBe("CyclomaticComplexity");
});

it("should have no result when value is below threshold", async () => {
const flows = await core.parse([other_uri]);

const results: core.ScanResult[] = core.scan(flows, defaultConfig);
const occurringResults = results[0].ruleResults.filter((rule) => rule.occurs);
expect(occurringResults).toHaveLength(0);
});

it("should have a result when value surpasses a configured threshold", async () => {
const flows = await core.parse([other_uri]);
const ruleConfig = {
rules: {
CyclomaticComplexity: {
threshold: 1,
severity: "error",
},
},
};

const results: core.ScanResult[] = core.scan(flows, ruleConfig);
const occurringResults = results[0].ruleResults.filter((rule) => rule.occurs);
expect(occurringResults).toHaveLength(1);
expect(occurringResults[0].ruleName).toBe("CyclomaticComplexity");
});

it("should correctly count the number of decisions and underlying rules one level", () => {
const sut = new CyclomaticComplexity();
const raw = {
elements: [
{
subtype: "decisions",
element: {
rules: [{}, {}, {}],
},
},
],
} as Partial<core.Flow>;
const given = raw as core.Flow;
sut.execute(given);
expect(sut["cyclomaticComplexityUnit"]).toBe(5);
});

it("should correctly count the number of decisions and underlying rules multi level", () => {
const sut = new CyclomaticComplexity();
const raw = {
elements: [
{
subtype: "decisions",
element: {
rules: [{}, {}, {}],
},
},
{ subtype: "decisions", element: { rules: [{}] } },
],
} as Partial<core.Flow>;
const given = raw as core.Flow;
sut.execute(given);
expect(sut["cyclomaticComplexityUnit"]).toBe(7);
});

it("should not throw an exception when theres no elements at all", () => {
const sut = new CyclomaticComplexity();
const raw = {
elements: [],
} as Partial<core.Flow>;
const given = raw as core.Flow;
expect(() => {
sut.execute(given);
}).not.toThrow();
});

it("should not throw an exception when element isn't present", () => {
const sut = new CyclomaticComplexity();
const raw = {} as Partial<core.Flow>;
const given = raw as core.Flow;
expect(() => {
sut.execute(given);
}).not.toThrow();
});

it("should correctly count the number of loops", () => {
const sut = new CyclomaticComplexity();
const raw = {
elements: [
{
subtype: "loops",
},
{
subtype: "loops",
},
],
} as Partial<core.Flow>;
const given = raw as core.Flow;
sut.execute(given);
expect(sut["cyclomaticComplexityUnit"]).toBe(3);
});
});
6 changes: 3 additions & 3 deletions tests/UnconnectedElement.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from "../src";
import * as path from "path";

import { ParseFlows } from "../src/main/libs/ParseFlows";
import { parse } from "../src/main/libs/ParseFlows";
import { ParsedFlow } from "../src/main/models/ParsedFlow";

import { UnconnectedElement } from "../src/main/rules/UnconnectedElement";
Expand All @@ -16,7 +16,7 @@ describe("UnconnectedElement", () => {
__dirname,
"./xmlfiles/Unconnected_Element.flow-meta.xml"
);
const parsed: ParsedFlow = (await ParseFlows([connectedElementTestFile])).pop() as ParsedFlow;
const parsed: ParsedFlow = (await parse([connectedElementTestFile])).pop() as ParsedFlow;
const ruleResult: core.RuleResult = unconnectedElementRule.execute(parsed.flow as core.Flow);
expect(ruleResult.occurs).toBe(true);
expect(ruleResult.details).not.toHaveLength(0);
Expand All @@ -30,7 +30,7 @@ describe("UnconnectedElement", () => {
__dirname,
"./xmlfiles/Unconnected_Element_Async.flow-meta.xml"
);
const parsed: ParsedFlow = (await ParseFlows([connectedElementTestFile])).pop() as ParsedFlow;
const parsed: ParsedFlow = (await parse([connectedElementTestFile])).pop() as ParsedFlow;
const ruleResult: core.RuleResult = unconnectedElementRule.execute(parsed.flow as core.Flow);
expect(ruleResult.occurs).toBe(true);
ruleResult.details.forEach((ruleDetail) => {
Expand Down
8 changes: 4 additions & 4 deletions tests/UnsafeRunningContext.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from "../src";
import * as path from "path";

import { ParseFlows } from "../src/main/libs/ParseFlows";
import { parse } from "../src/main/libs/ParseFlows";
import { ParsedFlow } from "../src/main/models/ParsedFlow";

import { describe, it, expect } from "@jest/globals";
Expand All @@ -15,7 +15,7 @@ describe("UnsafeRunningContext", () => {
__dirname,
"./xmlfiles/Unsafe_Running_Context.flow-meta.xml"
);
const parsed: ParsedFlow = (await ParseFlows([unsafeContextTestFile])).pop() as ParsedFlow;
const parsed: ParsedFlow = (await parse([unsafeContextTestFile])).pop() as ParsedFlow;
const ruleResult: core.RuleResult = unsafeRunningContext.execute(parsed.flow as core.Flow);
expect(ruleResult.occurs).toBe(true);
expect(ruleResult.details).not.toHaveLength(0);
Expand All @@ -27,7 +27,7 @@ describe("UnsafeRunningContext", () => {
__dirname,
"./xmlfiles/Unsafe_Running_Context_WithSharing.flow-meta.xml"
);
const parsed: ParsedFlow = (await ParseFlows([unsafeContextTestFile])).pop() as ParsedFlow;
const parsed: ParsedFlow = (await parse([unsafeContextTestFile])).pop() as ParsedFlow;
const ruleResult: core.RuleResult = unsafeRunningContext.execute(parsed.flow as core.Flow);
expect(ruleResult.occurs).toBe(false);
expect(ruleResult.details).toHaveLength(0);
Expand All @@ -38,7 +38,7 @@ describe("UnsafeRunningContext", () => {
__dirname,
"./xmlfiles/Unsafe_Running_Context_Default.flow-meta.xml"
);
const parsed: ParsedFlow = (await ParseFlows([unsafeContextTestFile])).pop() as ParsedFlow;
const parsed: ParsedFlow = (await parse([unsafeContextTestFile])).pop() as ParsedFlow;
const ruleResult: core.RuleResult = unsafeRunningContext.execute(parsed.flow as core.Flow);
expect(ruleResult.occurs).toBe(false);
expect(ruleResult.details).toHaveLength(0);
Expand Down
Loading
Loading