From 003f3aef4bff977b0ec1a2523b58209e640c4341 Mon Sep 17 00:00:00 2001 From: Tom Carman Date: Sun, 30 Jun 2024 23:36:46 +0100 Subject: [PATCH] refactor: i learnt a lot --- src/common/experimental/FlowElementWrapper.ts | 11 ++ src/common/experimental/FlowNodeTypes.ts | 37 +++--- src/common/experimental/FlowNodeWrapper.ts | 109 ++++++++---------- src/common/experimental/FlowWalker.ts | 28 +++++ src/common/experimental/FlowWrapper.ts | 39 ++++++- src/common/util.ts | 6 + src/rules/no-dml-in-flow-for-loop.ts | 71 +----------- 7 files changed, 147 insertions(+), 154 deletions(-) create mode 100644 src/common/experimental/FlowElementWrapper.ts create mode 100644 src/common/experimental/FlowWalker.ts diff --git a/src/common/experimental/FlowElementWrapper.ts b/src/common/experimental/FlowElementWrapper.ts new file mode 100644 index 0000000..8c6181b --- /dev/null +++ b/src/common/experimental/FlowElementWrapper.ts @@ -0,0 +1,11 @@ +import type { AnyFlowElement } from './FlowNodeTypes.js'; + +export class FlowElementWrapper { + public type: string; + public element: AnyFlowElement; + + public constructor(typeOfElement: string, element: AnyFlowElement) { + this.type = typeOfElement; + this.element = element; + } +} diff --git a/src/common/experimental/FlowNodeTypes.ts b/src/common/experimental/FlowNodeTypes.ts index 5d39525..3ceaa16 100644 --- a/src/common/experimental/FlowNodeTypes.ts +++ b/src/common/experimental/FlowNodeTypes.ts @@ -18,11 +18,16 @@ import { FlowLoop, FlowStep, FlowStart, + FlowChoice, + FlowConstant, + FlowDynamicChoiceSet, + FlowFormula, + FlowStage, + FlowTextTemplate, + FlowVariable, } from '@salesforce/types/metadata'; -export type NodeType = NodeWithConnector | NodeWithFaultConnector | FlowWait | FlowDecision | FlowLoop | FlowStep; - -export type NodeWithConnector = +export type AnyFlowNode = | FlowAssignment | FlowCollectionProcessor | FlowCustomError @@ -30,27 +35,19 @@ export type NodeWithConnector = | FlowScreen | FlowSubflow | FlowTransform - | FlowStart - | FlowActionCall - | FlowApexPluginCall - | FlowOrchestratedStage - | FlowRecordCreate - | FlowRecordDelete - | FlowRecordLookup - | FlowRecordUpdate; - -export type NodeWithFaultConnector = | FlowActionCall | FlowApexPluginCall | FlowOrchestratedStage | FlowRecordCreate | FlowRecordDelete | FlowRecordLookup - | FlowRecordUpdate; + | FlowRecordUpdate + | FlowWait + | FlowDecision + | FlowLoop + | FlowStep + | FlowStart; -export type NodeWithDefaultConnector = FlowWait | FlowDecision; -export type NodeWithNextValueConnector = FlowLoop; -export type NodeWithNoMoreValuesConnector = FlowLoop; -export type NodeWithWaitEvents = FlowWait; -export type NodeWithRules = FlowDecision; -export type NodeWithScheduledPaths = FlowStart; +export type AnyFlowElement = [ + FlowChoice | FlowConstant | FlowDynamicChoiceSet | FlowFormula | FlowStage | FlowTextTemplate | FlowVariable +]; diff --git a/src/common/experimental/FlowNodeWrapper.ts b/src/common/experimental/FlowNodeWrapper.ts index 55bf1e9..1fa64d8 100644 --- a/src/common/experimental/FlowNodeWrapper.ts +++ b/src/common/experimental/FlowNodeWrapper.ts @@ -1,77 +1,75 @@ -import { FlowConnector } from '@salesforce/types/metadata'; -import { - NodeType, - NodeWithConnector, - NodeWithFaultConnector, - NodeWithDefaultConnector, - NodeWithNextValueConnector, - NodeWithNoMoreValuesConnector, - NodeWithWaitEvents, - NodeWithRules, - NodeWithScheduledPaths, -} from './FlowNodeTypes.js'; +import type { FlowConnector } from '@salesforce/types/metadata'; +import { arrayify } from '../util.js'; +import type { AnyFlowNode } from './FlowNodeTypes.js'; type Connector = FlowConnector & { type: string; }; export class FlowNodeWrapper { + public type: string; public name: string; + public node: AnyFlowNode; public connectors: Connector[] = []; - public constructor(nodeType: NodeType) { - this.name = nodeType.name ?? ''; - this.handleConnectors(nodeType); + public constructor(typeOfNode: string, node: AnyFlowNode) { + this.type = typeOfNode; + this.name = node.name ?? ''; + this.node = node; + this.handleConnectors(node); } - private handleConnectors(nodeType: NodeType): void { - this.addStandardConnectors(nodeType); - this.addFaultConnector(nodeType); - this.addDefaultConnector(nodeType); - this.addNextValueConnector(nodeType); - this.addNoMoreValuesConnector(nodeType); - this.addWaitEventConnectors(nodeType); - this.addRuleConnectors(nodeType); - this.addScheduledPaths(nodeType); + private handleConnectors(node: AnyFlowNode): void { + this.addStandardConnector(node); + this.addFaultConnector(node); + this.addDefaultConnector(node); + this.addNextValueConnector(node); + this.addNoMoreValuesConnector(node); + this.addWaitEventConnectors(node); + this.addRuleConnectors(node); + this.addScheduledPaths(node); } private addConnector(connectorType: string, connector: FlowConnector): void { this.connectors.push({ type: connectorType, ...connector }); } - private addStandardConnectors(nodeType: NodeType): void { - if (hasConnector(nodeType) && nodeType.connector) { - const connectors: FlowConnector[] = Array.isArray(nodeType.connector) ? nodeType.connector : [nodeType.connector]; - connectors.forEach((connector) => this.addConnector('connector', connector)); + private addStandardConnector(node: AnyFlowNode): void { + if ('connector' in node && node.connector) { + this.addConnector('connector', node.connector); + } + if ('connectors' in node && node.connectors) { + arrayify(node.connectors).forEach((connector) => this.addConnector('connector', connector)); } } - private addFaultConnector(nodeType: NodeType): void { - if (hasFaultConnector(nodeType) && nodeType.faultConnector) { - this.addConnector('faultConnector', nodeType.faultConnector); + private addFaultConnector(node: AnyFlowNode): void { + if ('faultConnector' in node && node.faultConnector) { + this.addConnector('faultConnector', node.faultConnector); } } - private addDefaultConnector(nodeType: NodeType): void { - if (hasDefaultConnector(nodeType) && nodeType.defaultConnector) { - this.addConnector('defaultConnector', nodeType.defaultConnector); + private addDefaultConnector(node: AnyFlowNode): void { + if ('defaultConnector' in node && node.defaultConnector) { + this.addConnector('defaultConnector', node.defaultConnector); } } - private addNextValueConnector(nodeType: NodeType): void { - if (hasNextValueConnector(nodeType) && nodeType.nextValueConnector) { - this.addConnector('nextValueConnector', nodeType.nextValueConnector); + private addNextValueConnector(node: AnyFlowNode): void { + if ('nextValueConnector' in node && node.nextValueConnector) { + this.addConnector('nextValueConnector', node.nextValueConnector); } } - private addNoMoreValuesConnector(nodeType: NodeType): void { - if (hasNoMoreValuesConnector(nodeType) && nodeType.noMoreValuesConnector) { - this.addConnector('noMoreValuesConnector', nodeType.noMoreValuesConnector); + private addNoMoreValuesConnector(node: AnyFlowNode): void { + if ('noMoreValuesConnector' in node && node.noMoreValuesConnector) { + this.addConnector('noMoreValuesConnector', node.noMoreValuesConnector); } } - private addWaitEventConnectors(nodeType: NodeType): void { - if (hasWaitEvents(nodeType)) { - nodeType.waitEvents.forEach((waitEvent) => { + + private addWaitEventConnectors(node: AnyFlowNode): void { + if ('waitEvents' in node && node.waitEvents) { + arrayify(node.waitEvents).forEach((waitEvent) => { if (waitEvent.connector) { this.addConnector('connector', waitEvent.connector); } @@ -79,19 +77,20 @@ export class FlowNodeWrapper { } } - private addRuleConnectors(nodeType: NodeType): void { - if (hasRules(nodeType)) { - nodeType.rules.forEach((rule) => { - if (rule.connector) { + private addRuleConnectors(node: AnyFlowNode): void { + // Narrow type with 'fields', as both FlowRule and FlowScreenRule can be assigned to node.rules + if ('rules' in node && node.rules && !('fields' in node)) { + arrayify(node.rules).forEach((rule) => { + if ('connector' in rule && rule.connector) { this.addConnector('connector', rule.connector); } }); } } - private addScheduledPaths(nodeType: NodeType): void { - if (hasScheduledPaths(nodeType)) { - nodeType.scheduledPaths.forEach((path) => { + private addScheduledPaths(node: AnyFlowNode): void { + if ('scheduledPaths' in node && node.scheduledPaths) { + arrayify(node.scheduledPaths).forEach((path) => { if (path.connector) { this.addConnector('connector', path.connector); } @@ -99,13 +98,3 @@ export class FlowNodeWrapper { } } } - -const hasConnector = (node: NodeType): node is NodeWithConnector => 'connector' in node; -const hasFaultConnector = (node: NodeWithConnector): node is NodeWithFaultConnector => 'faultConnector' in node; -const hasDefaultConnector = (node: NodeType): node is NodeWithDefaultConnector => 'defaultConnector' in node; -const hasNextValueConnector = (node: NodeType): node is NodeWithNextValueConnector => 'nextValueConnector' in node; -const hasNoMoreValuesConnector = (node: NodeType): node is NodeWithNoMoreValuesConnector => - 'noMoreValuesConnector' in node; -const hasWaitEvents = (node: NodeType): node is NodeWithWaitEvents => 'waitEvents' in node; -const hasRules = (node: NodeType): node is NodeWithRules => 'rules' in node; -const hasScheduledPaths = (node: NodeType): node is NodeWithScheduledPaths => 'scheduledPaths' in node; diff --git a/src/common/experimental/FlowWalker.ts b/src/common/experimental/FlowWalker.ts new file mode 100644 index 0000000..e9e165b --- /dev/null +++ b/src/common/experimental/FlowWalker.ts @@ -0,0 +1,28 @@ +import { FlowWrapper } from './FlowWrapper.js'; +import { FlowNodeWrapper } from './FlowNodeWrapper.js'; + +export function walk(flow: FlowWrapper): void { + const startNode = flow.nodes.find((node: FlowNodeWrapper) => node.type === 'start'); + + if (startNode) { + const visitedNodes = new Set(); + const stack: FlowNodeWrapper[] = [startNode]; + + while (stack.length > 0) { + const currentNode = stack.pop(); + console.debug('currentNode: ', currentNode?.name); + + if (currentNode && !visitedNodes.has(currentNode.name)) { + visitedNodes.add(currentNode.name); + const connectors = currentNode.connectors; + + connectors.forEach((connector) => { + const targetNode = flow.nodes.find((node) => node.name === connector.targetReference); + if (targetNode) { + stack.push(targetNode); + } + }); + } + } + } +} diff --git a/src/common/experimental/FlowWrapper.ts b/src/common/experimental/FlowWrapper.ts index 660a931..63ee935 100644 --- a/src/common/experimental/FlowWrapper.ts +++ b/src/common/experimental/FlowWrapper.ts @@ -1,15 +1,17 @@ import { Flow } from '@salesforce/types/metadata'; import { FlowNodeWrapper } from './FlowNodeWrapper.js'; -import { NodeType } from './FlowNodeTypes.js'; +import { FlowElementWrapper } from './FlowElementWrapper.js'; +import { AnyFlowNode, AnyFlowElement } from './FlowNodeTypes.js'; export class FlowWrapper { public flowName: string; public nodes: FlowNodeWrapper[] = []; + public elements: FlowElementWrapper[] = []; public constructor(flow: Flow) { this.flowName = flow.fullName ?? ''; - const nodeProperties: Array = [ + const flowNodes: Array = [ 'assignments', 'collectionProcessors', 'customErrors', @@ -30,14 +32,39 @@ export class FlowWrapper { 'steps', 'start', ]; - nodeProperties.forEach((property) => { + + const flowElements: Array = [ + 'choices', + 'constants', + 'dynamicChoiceSets', + 'formulas', + 'stages', + 'textTemplates', + 'variables', + ]; + + flowNodes.forEach((property) => { + console.log('prop ', property); + if (flow[property] !== undefined) { + if (Array.isArray(flow[property])) { + (flow[property] as AnyFlowNode[]).forEach((node) => { + this.nodes.push(new FlowNodeWrapper(property, node)); + }); + } else { + this.nodes.push(new FlowNodeWrapper(property, flow[property] as AnyFlowNode)); + } + } + }); + + flowElements.forEach((property) => { + console.log('prop elem ', property); if (flow[property] !== undefined) { if (Array.isArray(flow[property])) { - (flow[property] as NodeType[]).forEach((node) => { - this.nodes.push(new FlowNodeWrapper(node)); + (flow[property] as unknown as AnyFlowElement[]).forEach((node) => { + this.elements.push(new FlowElementWrapper(property, node)); }); } else { - this.nodes.push(new FlowNodeWrapper(flow[property] as NodeType)); + this.elements.push(new FlowElementWrapper(property, flow[property] as AnyFlowElement)); } } }); diff --git a/src/common/util.ts b/src/common/util.ts index 0cfbd82..352bed0 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -31,6 +31,12 @@ export function normaliseNewlines(fileText: string): string { return fileText.replace(/\r\n/g, '\n'); } +// Many of the @salesforce/types/metadata types (correctly) assign metadata values to arrays, +// but when parsing xml, if there is only one element, the parser does not know it should be an array. +export function arrayify(input: T[] | T): T[] { + return Array.isArray(input) ? input : [input]; +} + export function getLineAndColNumber(ruleId: string, file: string, fileText: string, value: string): Location { let location: Location; diff --git a/src/rules/no-dml-in-flow-for-loop.ts b/src/rules/no-dml-in-flow-for-loop.ts index 3db678f..bd8db2b 100644 --- a/src/rules/no-dml-in-flow-for-loop.ts +++ b/src/rules/no-dml-in-flow-for-loop.ts @@ -3,6 +3,7 @@ import type { Flow } from '@salesforce/types/metadata'; import { RuleClass } from '../common/types.js'; import { parseMetadataXml } from '../common/util.js'; import { FlowWrapper } from '../common/experimental/FlowWrapper.js'; +import { walk } from '../common/experimental/FlowWalker.js'; export default class NoDmlInFlowForLoop extends RuleClass { public ruleId: string = 'no-missing-description-on-fields'; @@ -18,74 +19,8 @@ export default class NoDmlInFlowForLoop extends RuleClass { const fileText = fs.readFileSync(file, 'utf-8'); const flow = parseMetadataXml(fileText, 'Flow'); const flowWrapper = new FlowWrapper(flow); - - console.log('\n\n Flow: ' + flow.label); - console.log('\n'); - for (const node of flowWrapper.nodes) { - console.log('\n- Node: ' + node.name); - for (const connector of node.connectors) { - console.log(' Connector Type: ' + connector.type); - console.log(' Connects to :' + connector.targetReference); - } - } + console.log(flowWrapper); + walk(flowWrapper); } } } - -// Flow: Sample PMD flow - -// - Node: cascadeCity1 -// Connector Type: connector -// Connects to :badUpdate1 - -// - Node: cascadeCity2 -// Connector Type: connector -// Connects to :goodLoop1 - -// - Node: call_Apex_code -// Connector Type: connector -// Connects to :suspiciousLoop1 - -// - Node: getContacts -// Connector Type: connector -// Connects to :whichCity - -// - Node: badUpdate1 -// Connector Type: connector -// Connects to :badLoop1 - -// - Node: goodUpdate1 -// Connector Type: connector -// Connects to :X1_more_update - -// - Node: X1_more_update - -// - Node: whichCity -// Connector Type: defaultConnector -// Connects to :suspiciousLoop1 -// Connector Type: connector -// Connects to :badLoop1 -// Connector Type: connector -// Connects to :goodLoop1 - -// - Node: badLoop1 -// Connector Type: nextValueConnector -// Connects to :cascadeCity1 -// Connector Type: noMoreValuesConnector -// Connects to :X1_more_update - -// - Node: goodLoop1 -// Connector Type: nextValueConnector -// Connects to :cascadeCity2 -// Connector Type: noMoreValuesConnector -// Connects to :goodUpdate1 - -// - Node: suspiciousLoop1 -// Connector Type: nextValueConnector -// Connects to :call_Apex_code -// Connector Type: noMoreValuesConnector -// Connects to :X1_more_update - -// - Node: -// Connector Type: connector -// Connects to :getContacts