diff --git a/.vscode/launch.json b/.vscode/launch.json index 445a4629..bc9f7341 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,30 @@ "name": "Run npm test", "request": "launch", "type": "node-terminal" + }, + { + "type": "node", + "name": "vscode-jest-tests.v2.lightning-flow-scanner-core", + "request": "launch", + "args": [ + "test", + "--", + "--runInBand", + "--watchAll=false", + "--testNamePattern", + "${jest.testNamePattern}", + "--runTestsByPath", + "${jest.testFile}" + ], + "cwd": "/Users/jun/Development/github/lightning-flow-scanner-core", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "runtimeExecutable": "npm", + "env": { + "OVERRIDE_CONFIG": "true", + "IS_NEW_SCAN_ENABLED": "true" + } } ] } diff --git a/src/main/interfaces/AdvancedRuleConfig.ts b/src/main/interfaces/AdvancedRuleConfig.ts new file mode 100644 index 00000000..33d5c27a --- /dev/null +++ b/src/main/interfaces/AdvancedRuleConfig.ts @@ -0,0 +1,10 @@ +export type AdvancedConfig = { + disabled?: boolean; + path?: string; + severity?: string; + suppressions?: string[]; +}; + +export type AdvancedRuleConfig = { + [ruleName: string]: AdvancedConfig; +}; diff --git a/src/main/interfaces/AdvancedRuleDefintion.ts b/src/main/interfaces/AdvancedRuleDefintion.ts new file mode 100644 index 00000000..bf9fa738 --- /dev/null +++ b/src/main/interfaces/AdvancedRuleDefintion.ts @@ -0,0 +1,7 @@ +import type { AdvancedRuleConfig } from "./AdvancedRuleConfig"; + +import { Flow, RuleResult } from "../internals/internals"; + +export interface AdvancedRuleDefinition { + execute(flow: Flow, ruleConfiguration?: AdvancedRuleConfig): RuleResult; +} diff --git a/src/main/interfaces/AdvancedSuppression.ts b/src/main/interfaces/AdvancedSuppression.ts new file mode 100644 index 00000000..ab39d54d --- /dev/null +++ b/src/main/interfaces/AdvancedSuppression.ts @@ -0,0 +1,6 @@ +import { RuleResult } from "../internals/internals"; +import { AdvancedRuleConfig } from "./AdvancedRuleConfig"; + +export interface AdvancedSuppression { + suppress(scanResult: RuleResult, ruleConfiguration?: AdvancedRuleConfig): RuleResult; +} diff --git a/src/main/interfaces/IRulesConfig.ts b/src/main/interfaces/IRulesConfig.ts index 5f08e0e2..c02d45f8 100644 --- a/src/main/interfaces/IRulesConfig.ts +++ b/src/main/interfaces/IRulesConfig.ts @@ -1,7 +1,8 @@ +import { AdvancedRuleConfig } from "./AdvancedRuleConfig"; import { IExceptions } from "./IExceptions"; import { IRuleOptions } from "./IRuleOptions"; export interface IRulesConfig { - rules?: IRuleOptions; exceptions?: IExceptions; + rules?: AdvancedRuleConfig | IRuleOptions; } diff --git a/src/main/libs/DynamicRule.ts b/src/main/libs/DynamicRule.ts index 7052eb67..e6b9f942 100644 --- a/src/main/libs/DynamicRule.ts +++ b/src/main/libs/DynamicRule.ts @@ -1,11 +1,12 @@ import { IRuleDefinition } from "../interfaces/IRuleDefinition"; +import { AdvancedRule } from "../models/AdvancedRule"; import { BetaRuleStore, DefaultRuleStore } from "../store/DefaultRuleStore"; -export class DynamicRule { +export class DynamicRule { constructor(className: string) { if (!DefaultRuleStore.hasOwnProperty(className) && BetaRuleStore.hasOwnProperty(className)) { - return new BetaRuleStore[className]() as IRuleDefinition; + return new BetaRuleStore[className]() as T; } - return new DefaultRuleStore[className]() as IRuleDefinition; + return new DefaultRuleStore[className]() as T; } } diff --git a/src/main/libs/ScanFlows.ts b/src/main/libs/ScanFlows.ts index e770c56e..7d27684d 100644 --- a/src/main/libs/ScanFlows.ts +++ b/src/main/libs/ScanFlows.ts @@ -1,18 +1,34 @@ -import { GetRuleDefinitions } from "./GetRuleDefinitions"; -import * as core from "../../main/internals/internals"; +import type { IRuleDefinition } from "../interfaces/IRuleDefinition"; + +import { + Flow, + IRulesConfig, + ResultDetails, + RuleResult, + ScanResult, +} from "../../main/internals/internals"; +import { AdvancedRuleConfig } from "../interfaces/AdvancedRuleConfig"; +import { AdvancedRule } from "../models/AdvancedRule"; import { ParsedFlow } from "../models/ParsedFlow"; +import { BetaRuleStore, DefaultRuleStore } from "../store/DefaultRuleStore"; +import { DynamicRule } from "./DynamicRule"; +import { GetRuleDefinitions } from "./GetRuleDefinitions"; -export function scan( - parsedFlows: ParsedFlow[], - ruleOptions?: core.IRulesConfig -): core.ScanResult[] { - const flows: core.Flow[] = []; +const { IS_NEW_SCAN_ENABLED: isNewScanEnabled, OVERRIDE_CONFIG: overrideConfig } = process.env; + +// Will be replaced by scanInternal in the future +// eslint-disable-next-line sonarjs/cognitive-complexity +export function scan(parsedFlows: ParsedFlow[], ruleOptions?: IRulesConfig): ScanResult[] { + if (isNewScanEnabled === "true") { + return scanInternal(parsedFlows, ruleOptions); + } + const flows: Flow[] = []; for (const flow of parsedFlows) { if (!flow.errorMessage && flow.flow) { flows.push(flow.flow); } } - let scanResults: core.ScanResult[]; + let scanResults: ScanResult[]; if (ruleOptions?.rules && Object.entries(ruleOptions.rules).length > 0) { scanResults = ScanFlows(flows, ruleOptions); } else { @@ -23,14 +39,12 @@ export function scan( for (const [exceptionName, exceptionElements] of Object.entries(ruleOptions.exceptions)) { for (const scanResult of scanResults) { if (scanResult.flow.name === exceptionName) { - for (const ruleResult of scanResult.ruleResults as core.RuleResult[]) { + for (const ruleResult of scanResult.ruleResults as RuleResult[]) { if (exceptionElements[ruleResult.ruleName]) { const exceptions = exceptionElements[ruleResult.ruleName]; - const filteredDetails = (ruleResult.details as core.ResultDetails[]).filter( - (detail) => { - return !exceptions.includes(detail.name); - } - ); + const filteredDetails = (ruleResult.details as ResultDetails[]).filter((detail) => { + return !exceptions.includes(detail.name); + }); ruleResult.details = filteredDetails; ruleResult.occurs = filteredDetails.length > 0; } @@ -43,10 +57,12 @@ export function scan( return scanResults; } -export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig): core.ScanResult[] { - const flowResults: core.ScanResult[] = []; +// Will be removed once scanInternal is fully enabled +// eslint-disable-next-line sonarjs/cognitive-complexity +export function ScanFlows(flows: Flow[], ruleOptions?: IRulesConfig): ScanResult[] { + const flowResults: ScanResult[] = []; - let selectedRules: core.IRuleDefinition[] = []; + let selectedRules: IRuleDefinition[] = []; if (ruleOptions && ruleOptions.rules) { const ruleMap = new Map(); for (const [ruleName, rule] of Object.entries(ruleOptions.rules)) { @@ -58,7 +74,7 @@ export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig): } for (const flow of flows) { - const ruleResults: core.RuleResult[] = []; + const ruleResults: RuleResult[] = []; for (const rule of selectedRules) { try { if (rule.supportedTypes.includes(flow.type)) { @@ -75,17 +91,75 @@ export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig): } ruleResults.push(result); } else { - ruleResults.push(new core.RuleResult(rule, [])); + ruleResults.push(new RuleResult(rule, [])); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { - const message = - "Something went wrong while executing " + rule.name + " in the Flow: '" + flow.name; - ruleResults.push(new core.RuleResult(rule, [], message)); + const message = `Something went wrong while executing ${rule.name} in the Flow: ${flow.name} with error ${error}`; + ruleResults.push(new RuleResult(rule, [], message)); } } - flowResults.push(new core.ScanResult(flow, ruleResults)); + flowResults.push(new ScanResult(flow, ruleResults)); } return flowResults; } + +export function scanInternal(parsedFlows: ParsedFlow[], ruleOptions?: IRulesConfig): ScanResult[] { + const flows: Flow[] = parsedFlows.map((parsedFlow) => parsedFlow.flow as Flow); + const scanResults: ScanResult[] = []; + for (const flow of flows) { + scanResults.push(scanFlowWithConfig(flow, ruleOptions)); + } + return scanResults; +} + +function ruleAndConfig( + ruleOptions?: IRulesConfig +): [Record, Record] { + // for unit tests, use a small set of rules + const ruleConfiguration = unifiedRuleConfig(ruleOptions); + let allRules: Record = { ...DefaultRuleStore, ...BetaRuleStore }; + if (overrideConfig === "true" && ruleOptions?.rules) { + allRules = Object.entries(allRules).reduce>( + (accumulator, [ruleName, rule]) => { + if (ruleOptions?.rules?.[ruleName]) { + accumulator[ruleName] = rule; + } + return accumulator; + }, + {} + ); + } + return [allRules, ruleConfiguration]; +} + +function scanFlowWithConfig(flow: Flow, ruleOptions?: IRulesConfig): ScanResult { + const [allRules, ruleConfiguration] = ruleAndConfig(ruleOptions); + const ruleResults: RuleResult[] = []; + for (const [ruleName] of Object.entries(allRules)) { + const advancedRule = new DynamicRule(ruleName); + ruleResults.push( + (advancedRule as AdvancedRule).execute(flow, ruleConfiguration[ruleName] ?? {}) + ); + } + return new ScanResult(flow, ruleResults); +} + +function unifiedRuleConfig( + ruleOptions: IRulesConfig | undefined +): Record { + const configuredRules: AdvancedRuleConfig = ruleOptions?.rules ?? {}; + const activeConfiguredRules: Record = Object.entries(configuredRules) + .filter(([, configuration]) => { + if (!("disabled" in configuration)) { + return true; + } + + return configuration.disabled !== true; + }) + .reduce>((accumulator, [ruleName, config]) => { + return { ...accumulator, [ruleName]: config as AdvancedRuleConfig }; + }, {}); + + return activeConfiguredRules; +} diff --git a/src/main/models/AdvancedRule.ts b/src/main/models/AdvancedRule.ts new file mode 100644 index 00000000..455dd176 --- /dev/null +++ b/src/main/models/AdvancedRule.ts @@ -0,0 +1,44 @@ +import { AdvancedRuleConfig } from "../interfaces/AdvancedRuleConfig"; +import { AdvancedRuleDefinition } from "../interfaces/AdvancedRuleDefintion"; +import { AdvancedSuppression } from "../interfaces/AdvancedSuppression"; +import { IRuleDefinition } from "../internals/internals"; +import { Flow } from "./Flow"; +import { RuleCommon } from "./RuleCommon"; +import { RuleInfo } from "./RuleInfo"; +import { RuleResult } from "./RuleResult"; + +export abstract class AdvancedRule extends RuleCommon { + constructor( + info: RuleInfo, + optional?: { + severity?: string; + } + ) { + super(info, optional); + } + + public execute(flow: Flow, ruleConfiguration?: AdvancedRuleConfig): RuleResult { + if (!hasAdvancedRuleDefinition(this)) { + return new RuleResult(this as unknown as IRuleDefinition, []); + } + + let ruleResult = (this as AdvancedRuleDefinition).execute(flow, ruleConfiguration); + + if (hasAdvancedSuppression(this)) { + ruleResult = this.suppress(ruleResult, ruleConfiguration); + } + + return ruleResult; + } +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +const isFunction = (val: unknown): val is Function => typeof val === "function"; + +function hasAdvancedRuleDefinition(instance: unknown): instance is AdvancedRuleDefinition { + return isFunction((instance as AdvancedRuleDefinition).execute); +} + +function hasAdvancedSuppression(instance: unknown): instance is AdvancedSuppression { + return isFunction((instance as AdvancedSuppression).suppress); +} diff --git a/src/main/models/RuleCommon.ts b/src/main/models/RuleCommon.ts index 598b506d..faf4b9e6 100644 --- a/src/main/models/RuleCommon.ts +++ b/src/main/models/RuleCommon.ts @@ -1,15 +1,15 @@ import { RuleInfo } from "./RuleInfo"; export class RuleCommon { + public autoFixable: boolean; + public description: string; + public docRefs: Array<{ label: string; path: string }> = []; + public isConfigurable: boolean; public label; public name; public severity?; - public uri; - public docRefs: { label: string; path: string }[] = []; - public description: string; public supportedTypes: string[]; - public isConfigurable: boolean; - public autoFixable: boolean; + public uri; constructor( info: RuleInfo, diff --git a/src/main/rules/MissingFaultPath.ts b/src/main/rules/MissingFaultPath.ts index a88249d8..e8adf9ba 100644 --- a/src/main/rules/MissingFaultPath.ts +++ b/src/main/rules/MissingFaultPath.ts @@ -1,7 +1,12 @@ +import { AdvancedConfig } from "../interfaces/AdvancedRuleConfig"; +import { AdvancedSuppression } from "../interfaces/AdvancedSuppression"; import * as core from "../internals/internals"; import { RuleCommon } from "../models/RuleCommon"; -export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition { +export class MissingFaultPath + extends RuleCommon + implements AdvancedSuppression, core.IRuleDefinition +{ protected applicableElements: string[] = [ "recordLookups", "recordDeletes", @@ -28,7 +33,6 @@ export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition supportedTypes: [...core.FlowType.backEndTypes, ...core.FlowType.visualTypes], }); } - public execute(flow: core.Flow): core.RuleResult { const compiler = new core.Compiler(); const results: core.ResultDetails[] = []; @@ -63,6 +67,20 @@ export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition return new core.RuleResult(this, results); } + public suppress( + scanResult: core.RuleResult, + ruleConfiguration?: AdvancedConfig + ): core.RuleResult { + const suppressedResults: core.ResultDetails[] = []; + for (const resultDetails of scanResult.details) { + if (ruleConfiguration?.suppressions?.includes(resultDetails.name)) { + continue; + } + suppressedResults.push(resultDetails); + } + return new core.RuleResult(this, suppressedResults); + } + private isPartOfFaultHandlingFlow(element: core.FlowNode, flow: core.Flow): boolean { const flowelements = flow.elements?.filter( (el) => el instanceof core.FlowNode