From a7e6a55c9e4946c872f73ca8816238f944682af6 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Mon, 13 Jul 2026 01:02:20 +0200 Subject: [PATCH] Add 6 new Flow Scanner rules (Batch A: lexical) Adds six lexical Flow rules inspired by the community lightning-flow-scanner rule set, filling gaps in the current 15-rule catalog: - HardcodedUrl (Moderate, BestPractices) - hardcoded http(s) URLs - GetRecordAllFields (Low, Performance) - Get Records storing all fields - ProcessBuilder (Moderate, BestPractices) - retired Process Builder / Workflow - InactiveFlow (Low, BestPractices) - non-Active flow status - InvalidApiVersion (Low, BestPractices) - missing/outdated apiVersion (< 50) - MissingTriggerOrder (Low, BestPractices) - record-triggered flow w/o triggerOrder Each rule is a Python Query class in optional_query.py, registered in its QUERIES map, mirrored in the TypeScript catalog (hardcoded-catalog.ts) and messages.ts, and reflected in the all-rules goldfile. Adds a contains-new-rule-violations test workspace plus a jest block asserting each rule flags its fixture, and updates the end-to-end rule count (15 -> 21) and per-rule violation counts. Bumps the engine to 0.39.0-SNAPSHOT. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../FlowScanner/queries/optional_query.py | 260 +++++++++++++++++- .../code-analyzer-flow-engine/package.json | 2 +- .../src/hardcoded-catalog.ts | 50 +++- .../code-analyzer-flow-engine/src/messages.ts | 20 +- .../test/engine.test.ts | 34 ++- .../get_all_fields.flow-meta.xml | 24 ++ .../hardcoded_url.flow-meta.xml | 28 ++ .../missing_trigger_order.flow-meta.xml | 35 +++ .../old_api.flow-meta.xml | 20 ++ .../process_builder.flow-meta.xml | 32 +++ .../goldfiles/all_rules.goldfile.json | 66 +++++ 11 files changed, 566 insertions(+), 5 deletions(-) create mode 100644 packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/get_all_fields.flow-meta.xml create mode 100644 packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/hardcoded_url.flow-meta.xml create mode 100644 packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/missing_trigger_order.flow-meta.xml create mode 100644 packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/old_api.flow-meta.xml create mode 100644 packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/process_builder.flow-meta.xml diff --git a/packages/code-analyzer-flow-engine/FlowScanner/queries/optional_query.py b/packages/code-analyzer-flow-engine/FlowScanner/queries/optional_query.py index 8c5eab73..a37ba693 100644 --- a/packages/code-analyzer-flow-engine/FlowScanner/queries/optional_query.py +++ b/packages/code-analyzer-flow-engine/FlowScanner/queries/optional_query.py @@ -27,6 +27,11 @@ id_pattern = re.compile(r"^(?=.*[0-9][0-9])[a-zA-Z0-9]+[a-zA-Z0-9]{15}(?:[0-5][A-Z0-5]{3})?$") copy_name_pattern = re.compile(r"^Copy_\d+_of_[a-zA-Z]+") copy_label_pattern = re.compile(r"^Copy \d+ of [a-zA-Z]+") +# Matches an http(s) URL anywhere inside a string value. +url_pattern = re.compile(r"https?://[^\s\"'<>]+", re.IGNORECASE) +# Minimum Flow API version considered current. Flows below this (or with no +# apiVersion) are flagged by InvalidApiVersion. +MIN_API_VERSION = 50.0 ns = parse_utils.ns @@ -48,7 +53,13 @@ "CyclicSubflow": "Chain of subflow calls forms a cycle", "TriggerWaitEvent": "Wait Event in Trigger", "TriggerCallout": "Trigger Flow Callout in Synchronous Path", - "MissingDescription": "Missing Description" + "MissingDescription": "Missing Description", + "HardcodedUrl": "Hardcoded URL", + "GetRecordAllFields": "Get Records Retrieves All Fields", + "ProcessBuilder": "Deprecated Process Builder or Workflow", + "InactiveFlow": "Inactive Flow", + "InvalidApiVersion": "Invalid or Outdated API Version", + "MissingTriggerOrder": "Record Trigger Without Trigger Order" } """ @@ -1160,3 +1171,250 @@ def is_copy_label(label: str) -> bool: def is_copy_name(name: str) -> bool: return bool(copy_name_pattern.match(name)) + + +def flow_level_result(query_id: str, parser: FlowParser) -> QueryResult: + """Build a single QueryResult attributed to the flow as a whole. + + Used by rules that report a property of the entire flow (e.g. its status + or API version) rather than a specific inner element. + """ + # We don't want to print out the whole flow, so use a placeholder (see MissingDescription). + return QueryResult( + query_id=query_id, + flow_type=parser.get_flow_type(), + elem_code='', + elem_name='Flow', + elem_line_no=2, + field="Flow", + filename=parser.get_filename() + ) + + +class HardcodedUrl(LexicalQuery): + + query_id = "HardcodedUrl" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=("The flow has a hardcoded URL. Hardcoded URLs are a bad practice as they break " + "when moving between environments. Use a Named Credential, custom setting, or " + "custom label instead."), + query_version="1.0", + severity=Severity.Flow_Moderate_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + if parser is None: + return None + results = [] + root = parser.get_root() + top_level = [x for x in root if parse_utils.get_tag(x) != "processMetadataValues"] + for top in top_level: + for el in top.iter(tag=f"{ns}stringValue"): + msg = el.text + if msg is not None and url_pattern.search(msg) is not None and (top, el) not in results: + results.append((top, el)) + return self.report(results, parser) + + def report(self, results: list[tuple], parser: FlowParser) -> list[QueryResult] | None: + accum = [] + for top, el in results: + el_name = parse_utils.get_name(top) + if el_name is None: + el_name = parse_utils.get_tag(top) + accum.append(QueryResult( + query_id=self.query_id, + flow_type=parser.get_flow_type(), + influence_statement=None, + paths=None, + elem_code=parse_utils.get_elem_string(el), + elem_line_no=parse_utils.get_line_no(el), + elem_name=el_name, + filename=parser.get_filename(), + field=None + )) + return accum if len(accum) > 0 else None + + +class GetRecordAllFields(LexicalQuery): + + query_id = "GetRecordAllFields" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=("A Get Records element retrieves all fields automatically instead of selecting " + "only the fields it needs. Storing all fields is inefficient. Set the element to " + "manually assign only the fields that are used."), + query_version="1.0", + severity=Severity.Flow_Low_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + root = parser.get_root() + accum = [] + for el in parse_utils.get_by_tag(root, 'recordLookups'): + stores_all = parse_utils.get_text_of_tag(el, 'storeOutputAutomatically') == 'true' + has_queried_fields = len(parse_utils.get_by_tag(el, 'queriedFields')) > 0 + if stores_all and not has_queried_fields: + accum.append(QueryResult( + query_id=self.query_id, + flow_type=parser.get_flow_type(), + elem_code=parse_utils.get_elem_string(el), + elem_line_no=parse_utils.get_line_no(el), + elem_name=parse_utils.get_name(el), + field=parse_utils.get_name(el), + filename=parser.get_filename() + )) + return accum if len(accum) > 0 else None + + +class ProcessBuilder(LexicalQuery): + + query_id = "ProcessBuilder" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=("The flow is a Process Builder or Workflow Rule. These automation tools are retired. " + "Migrate the automation to a Flow."), + query_version="1.0", + severity=Severity.Flow_Moderate_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + if parser.get_flow_type() in (FlowType.ProcessBuilder, FlowType.Workflow, FlowType.InvocableProcess): + return [flow_level_result(self.query_id, parser)] + return None + + +class InactiveFlow(LexicalQuery): + + query_id = "InactiveFlow" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=("The flow is not active. Inactive flows should be activated if they are needed, " + "or deleted if they are not, to avoid clutter and confusion."), + query_version="1.0", + severity=Severity.Flow_Low_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + status = parse_utils.get_text_of_tag(parser.get_root(), 'status') + if status is not None and status != 'Active': + return [flow_level_result(self.query_id, parser)] + return None + + +class InvalidApiVersion(LexicalQuery): + + query_id = "InvalidApiVersion" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=(f"The flow has no API version or an API version below {MIN_API_VERSION:g}. " + "Outdated API versions can cause compatibility issues. Set the flow to a " + "recent API version."), + query_version="1.0", + severity=Severity.Flow_Low_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + api_version = parse_utils.get_text_of_tag(parser.get_root(), 'apiVersion') + is_invalid = api_version is None + if not is_invalid: + try: + is_invalid = float(api_version) < MIN_API_VERSION + except ValueError: + is_invalid = True + if is_invalid: + return [flow_level_result(self.query_id, parser)] + return None + + +class MissingTriggerOrder(LexicalQuery): + + query_id = "MissingTriggerOrder" + query_name = QUERIES[query_id] + + @classmethod + def get_query_description(cls) -> QueryDescription: + return QueryDescription( + query_id=cls.query_id, + query_name=cls.query_name, + query_description=("A record-triggered flow does not specify a trigger order. When multiple " + "record-triggered flows run on the same object and event, set a trigger order " + "to make the execution sequence deterministic."), + query_version="1.0", + severity=Severity.Flow_Low_Severity, + help_url=DEFAULT_HELP_URL, + is_security=False + ) + + def when_to_run(self) -> list[QueryAction]: + return [QueryAction.lexical] + + def execute(self, parser: FlowParser = None, **kwargs) -> list[QueryResult] | None: + root = parser.get_root() + starts = parse_utils.get_by_tag(root, tag_name='start') + if len(starts) != 1: + return None + start = starts[0] + # Only record-triggered flows have a recordTriggerType and support triggerOrder. + if len(parse_utils.get_by_tag(start, tag_name='recordTriggerType')) != 1: + return None + if len(parse_utils.get_by_tag(start, tag_name='triggerOrder')) == 0: + return [QueryResult( + query_id=self.query_id, + flow_type=parser.get_flow_type(), + elem_name='start', + elem_line_no=parse_utils.get_line_no(start), + elem_code=parse_utils.get_elem_string(start), + filename=parser.get_filename() + )] + return None diff --git a/packages/code-analyzer-flow-engine/package.json b/packages/code-analyzer-flow-engine/package.json index 94cf8c2c..30dede3f 100644 --- a/packages/code-analyzer-flow-engine/package.json +++ b/packages/code-analyzer-flow-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-flow-engine", "description": "Plugin package that adds 'Flow Scanner' as an engine into Salesforce Code Analyzer", - "version": "0.38.0", + "version": "0.39.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", diff --git a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts index fe22e7da..20cb0250 100644 --- a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts +++ b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts @@ -17,7 +17,13 @@ enum RuleName { TriggerEntryCriteria = 'TriggerEntryCriteria', TriggerWaitEvent = 'TriggerWaitEvent', UnreachableElement = 'UnreachableElement', - UnusedResource = 'UnusedResource' + UnusedResource = 'UnusedResource', + HardcodedUrl = 'HardcodedUrl', + GetRecordAllFields = 'GetRecordAllFields', + ProcessBuilder = 'ProcessBuilder', + InactiveFlow = 'InactiveFlow', + InvalidApiVersion = 'InvalidApiVersion', + MissingTriggerOrder = 'MissingTriggerOrder' } const RULE_DESCRIPTIONS: RuleDescription[] = [ @@ -125,6 +131,48 @@ const RULE_DESCRIPTIONS: RuleDescription[] = [ severityLevel: SeverityLevel.Moderate, tags: [/* NOT RECOMMENDED */ COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], resourceUrls: [] + }, + { + name: RuleName.HardcodedUrl, + description: getMessage('HardcodedUrlRuleDescription'), + severityLevel: SeverityLevel.Moderate, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] + }, + { + name: RuleName.GetRecordAllFields, + description: getMessage('GetRecordAllFieldsRuleDescription'), + severityLevel: SeverityLevel.Low, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] + }, + { + name: RuleName.ProcessBuilder, + description: getMessage('ProcessBuilderRuleDescription'), + severityLevel: SeverityLevel.Moderate, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] + }, + { + name: RuleName.InactiveFlow, + description: getMessage('InactiveFlowRuleDescription'), + severityLevel: SeverityLevel.Low, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] + }, + { + name: RuleName.InvalidApiVersion, + description: getMessage('InvalidApiVersionRuleDescription'), + severityLevel: SeverityLevel.Low, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] + }, + { + name: RuleName.MissingTriggerOrder, + description: getMessage('MissingTriggerOrderRuleDescription'), + severityLevel: SeverityLevel.Low, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], + resourceUrls: [] } ]; diff --git a/packages/code-analyzer-flow-engine/src/messages.ts b/packages/code-analyzer-flow-engine/src/messages.ts index 0dba4231..4deb9c9f 100644 --- a/packages/code-analyzer-flow-engine/src/messages.ts +++ b/packages/code-analyzer-flow-engine/src/messages.ts @@ -93,7 +93,25 @@ const MESSAGE_CATALOG: {[key: string]: string} = { `This rule identifies elements that have not been connected to the start element of the flow. Unreachable elements are usually due to incomplete flows or developer error.`, UnusedResourceRuleDescription: - `This rule detects redundant variables that are not used in the flow. This can be a sign of developer error.` + `This rule detects redundant variables that are not used in the flow. This can be a sign of developer error.`, + + HardcodedUrlRuleDescription: + `This rule detects hardcoded URLs within a flow. Hardcoded URLs are a bad practice as they break when moving between environments. Use a Named Credential, custom setting, or custom label instead.`, + + GetRecordAllFieldsRuleDescription: + `This rule detects Get Records elements that automatically store all fields instead of selecting only the fields that are needed. Storing all fields is inefficient; manually assign only the fields that are used.`, + + ProcessBuilderRuleDescription: + `This rule detects Process Builder processes and Workflow Rules. These automation tools are retired and should be migrated to Flows.`, + + InactiveFlowRuleDescription: + `This rule detects flows that are not active. Inactive flows should be activated if they are needed, or deleted if they are not, to reduce clutter and confusion.`, + + InvalidApiVersionRuleDescription: + `This rule detects flows that have no API version or an API version below the supported minimum. Outdated API versions can cause compatibility issues; set the flow to a recent API version.`, + + MissingTriggerOrderRuleDescription: + `This rule detects record-triggered flows that do not specify a trigger order. When multiple record-triggered flows run on the same object and event, a trigger order makes the execution sequence deterministic.` }; export function getMessage(msgId: string, ...args: (string | number)[]): string { diff --git a/packages/code-analyzer-flow-engine/test/engine.test.ts b/packages/code-analyzer-flow-engine/test/engine.test.ts index ddbc08e4..27419cd9 100644 --- a/packages/code-analyzer-flow-engine/test/engine.test.ts +++ b/packages/code-analyzer-flow-engine/test/engine.test.ts @@ -65,6 +65,7 @@ const TEST_DATA_FOLDER: string = path.resolve(__dirname, 'test-data'); const PATH_TO_NO_FLOWS_WORKSPACE = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'contains-no-flows'); const PATH_TO_MULTIPLE_FLOWS_WORKSPACE = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'contains-multiple-flows'); const PATH_TO_ONE_FLOW_NO_VIOLATIONS_WORKSPACE = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'contains-one-flow-no-violations'); +const PATH_TO_NEW_RULE_VIOLATIONS_WORKSPACE = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'contains-new-rule-violations'); const PARENT_WITH_SOURCE_CALLS_SUB_WITH_SINK_WORKSPACE: string = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'parent-with-source-calls-sub-with-sink'); const PARENT_WITH_SINK_CALLS_SUB_WITH_SOURCE_WORKSPACE: string = path.resolve(TEST_DATA_FOLDER, 'example workspaces', 'parent-with-sink-calls-sub-with-source'); const PATH_TO_EXAMPLE1: string = path.join(PATH_TO_MULTIPLE_FLOWS_WORKSPACE, 'example1_containsWithoutSharingViolations.flow-meta.xml'); @@ -106,7 +107,7 @@ describe('Tests for the FlowScannerEngine', () => { const ruleDescriptors: RuleDescription[] = await engine.describeRules(createDescribeOptions(workspace)); // No need to do in-depth examination of the rules, since other tests already do that. Just make sure we got // the right number of rules. - expect(ruleDescriptors).toHaveLength(15); + expect(ruleDescriptors).toHaveLength(21); expect(describeProgressEvents.map(e => e.percentComplete)).toEqual([0, 75, 100]); // Part 2: Running production rules. @@ -124,6 +125,8 @@ describe('Tests for the FlowScannerEngine', () => { return acc; }, {}); expect(countsPerRule).toEqual({ + GetRecordAllFields: 3, + InactiveFlow: 4, MissingDescription: 56, MissingFaultHandler: 9, PreventPassingUserDataIntoElementWithoutSharing: 5, @@ -726,6 +729,35 @@ describe('Tests for the FlowScannerEngine', () => { expect(version).toMatch(/\d+\.\d+\.\d+.*/); }); }); + + describe('Batch A lexical rules each detect their own violation', () => { + const engine: FlowScannerEngine = new FlowScannerEngine(flowScannerCommandWrapper); + const workspace: Workspace = new Workspace('id', [PATH_TO_NEW_RULE_VIOLATIONS_WORKSPACE]); + + it.each([ + {ruleName: 'HardcodedUrl', file: 'hardcoded_url.flow-meta.xml'}, + {ruleName: 'GetRecordAllFields', file: 'get_all_fields.flow-meta.xml'}, + {ruleName: 'ProcessBuilder', file: 'process_builder.flow-meta.xml'}, + {ruleName: 'InvalidApiVersion', file: 'old_api.flow-meta.xml'}, + {ruleName: 'MissingTriggerOrder', file: 'missing_trigger_order.flow-meta.xml'}, + ])('When running $ruleName, then it flags its violating fixture', async ({ruleName, file}) => { + const engineResults: EngineRunResults = await engine.runRules([ruleName], createRunOptions(workspace)); + + expect(engineResults.violations.length).toBeGreaterThan(0); + // Only the selected rule fires, and it flags the expected fixture. + for (const violation of engineResults.violations) { + expect(violation.ruleName).toEqual(ruleName); + } + expect(engineResults.violations.some(v => + v.codeLocations[v.primaryLocationIndex].file.endsWith(file))).toEqual(true); + }); + + it('When running InactiveFlow on an all-active workspace, then it reports no violations', async () => { + // Every fixture in this workspace is Active, so InactiveFlow must stay silent here. + const engineResults: EngineRunResults = await engine.runRules(['InactiveFlow'], createRunOptions(workspace)); + expect(engineResults.violations).toHaveLength(0); + }); + }); }); }); diff --git a/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/get_all_fields.flow-meta.xml b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/get_all_fields.flow-meta.xml new file mode 100644 index 00000000..abf117eb --- /dev/null +++ b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/get_all_fields.flow-meta.xml @@ -0,0 +1,24 @@ + + + 60.0 + d + + AutoLaunchedFlow + + getAccount + + d + 100 + 100 + false + true + Account + true + + + 50 + 48 + getAccount + + Active + diff --git a/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/hardcoded_url.flow-meta.xml b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/hardcoded_url.flow-meta.xml new file mode 100644 index 00000000..9dc3bba4 --- /dev/null +++ b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/hardcoded_url.flow-meta.xml @@ -0,0 +1,28 @@ + + + 60.0 + d + + Flow + + hello + + d + 494 + 202 + + + 336 + 48 + hello + + Active + + myEndpoint + String + false + false + false + https://example.com/api/v1/data + + diff --git a/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/missing_trigger_order.flow-meta.xml b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/missing_trigger_order.flow-meta.xml new file mode 100644 index 00000000..0e63e71f --- /dev/null +++ b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/missing_trigger_order.flow-meta.xml @@ -0,0 +1,35 @@ + + + 60.0 + d + + AutoLaunchedFlow + + a1 + + d + 100 + 100 + + var1 + Assign + x + + + + 50 + 48 + a1 + Account + Create + RecordAfterSave + + Active + + var1 + String + false + false + false + + diff --git a/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/old_api.flow-meta.xml b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/old_api.flow-meta.xml new file mode 100644 index 00000000..7ce55a60 --- /dev/null +++ b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/old_api.flow-meta.xml @@ -0,0 +1,20 @@ + + + 45.0 + d + + Flow + + hello + + d + 494 + 202 + + + 336 + 48 + hello + + Active + diff --git a/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/process_builder.flow-meta.xml b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/process_builder.flow-meta.xml new file mode 100644 index 00000000..1766a349 --- /dev/null +++ b/packages/code-analyzer-flow-engine/test/test-data/example workspaces/contains-new-rule-violations/process_builder.flow-meta.xml @@ -0,0 +1,32 @@ + + + 60.0 + d + + Workflow + + a1 + + d + 100 + 100 + + var1 + Assign + x + + + + 50 + 48 + a1 + + Active + + var1 + String + false + false + false + + diff --git a/packages/code-analyzer-flow-engine/test/test-data/goldfiles/all_rules.goldfile.json b/packages/code-analyzer-flow-engine/test/test-data/goldfiles/all_rules.goldfile.json index 28c2e792..6a3258d8 100644 --- a/packages/code-analyzer-flow-engine/test/test-data/goldfiles/all_rules.goldfile.json +++ b/packages/code-analyzer-flow-engine/test/test-data/goldfiles/all_rules.goldfile.json @@ -163,5 +163,71 @@ "XML" ], "resourceUrls": [] + }, + { + "name": "HardcodedUrl", + "description": "This rule detects hardcoded URLs within a flow. Hardcoded URLs are a bad practice as they break when moving between environments. Use a Named Credential, custom setting, or custom label instead.", + "severityLevel": 3, + "tags": [ + "Recommended", + "BestPractices", + "XML" + ], + "resourceUrls": [] + }, + { + "name": "GetRecordAllFields", + "description": "This rule detects Get Records elements that automatically store all fields instead of selecting only the fields that are needed. Storing all fields is inefficient; manually assign only the fields that are used.", + "severityLevel": 4, + "tags": [ + "Recommended", + "Performance", + "XML" + ], + "resourceUrls": [] + }, + { + "name": "ProcessBuilder", + "description": "This rule detects Process Builder processes and Workflow Rules. These automation tools are retired and should be migrated to Flows.", + "severityLevel": 3, + "tags": [ + "Recommended", + "BestPractices", + "XML" + ], + "resourceUrls": [] + }, + { + "name": "InactiveFlow", + "description": "This rule detects flows that are not active. Inactive flows should be activated if they are needed, or deleted if they are not, to reduce clutter and confusion.", + "severityLevel": 4, + "tags": [ + "Recommended", + "BestPractices", + "XML" + ], + "resourceUrls": [] + }, + { + "name": "InvalidApiVersion", + "description": "This rule detects flows that have no API version or an API version below the supported minimum. Outdated API versions can cause compatibility issues; set the flow to a recent API version.", + "severityLevel": 4, + "tags": [ + "Recommended", + "BestPractices", + "XML" + ], + "resourceUrls": [] + }, + { + "name": "MissingTriggerOrder", + "description": "This rule detects record-triggered flows that do not specify a trigger order. When multiple record-triggered flows run on the same object and event, a trigger order makes the execution sequence deterministic.", + "severityLevel": 4, + "tags": [ + "Recommended", + "BestPractices", + "XML" + ], + "resourceUrls": [] } ] \ No newline at end of file