From 9aaaa3a52b76c876be36c7f6e1953b3c6009df9c Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 5 Dec 2025 08:51:26 +0530 Subject: [PATCH 1/3] Adding changes for queryId mismatch with rule name for flow --- packages/code-analyzer-core/package.json | 2 +- .../code-analyzer-flow-engine/package.json | 2 +- .../code-analyzer-flow-engine/src/engine.ts | 7 +- .../src/hardcoded-catalog.ts | 33 ++-------- .../test/engine.test.ts | 64 +++++++++++++++++++ .../goldfiles/all_rules.goldfile.json | 2 +- 6 files changed, 76 insertions(+), 34 deletions(-) diff --git a/packages/code-analyzer-core/package.json b/packages/code-analyzer-core/package.json index 772493c3..a513d0d0 100644 --- a/packages/code-analyzer-core/package.json +++ b/packages/code-analyzer-core/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-core", "description": "Core Package for the Salesforce Code Analyzer", - "version": "0.40.0", + "version": "0.41.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/package.json b/packages/code-analyzer-flow-engine/package.json index 02e52986..73db9a3c 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.31.0", + "version": "0.32.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/engine.ts b/packages/code-analyzer-flow-engine/src/engine.ts index cf44ab50..b00e254a 100644 --- a/packages/code-analyzer-flow-engine/src/engine.ts +++ b/packages/code-analyzer-flow-engine/src/engine.ts @@ -13,7 +13,7 @@ import { import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils'; import {getMessage} from './messages'; import {FlowNodeDescriptor, FlowScannerCommandWrapper, FlowScannerExecutionResult, FlowScannerRuleResult} from "./python/FlowScannerCommandWrapper"; -import {getDescriptionForRule, getRuleNameFromQueryId, getAllRuleNames, getQueryIdsForRule} from "./hardcoded-catalog"; +import {getDescriptionForRule, getAllRuleNames} from "./hardcoded-catalog"; /** * An arbitrarily chosen value for how close the engine is to completion before the underlying Flow tool is invoked, @@ -80,7 +80,8 @@ export class FlowScannerEngine extends Engine { this.emitRunRulesProgressEvent(normalizeRelativeCompletionPercentage(percentage)); } - const queryIds: string[] = ruleNames.flatMap(getQueryIdsForRule); + // Query IDs are the same as rule names (1:1 mapping) + const queryIds: string[] = ruleNames; const executionResults: FlowScannerExecutionResult = await this.commandWrapper.runFlowScannerRules( runOptions.workingFolder, @@ -142,7 +143,7 @@ function toEngineRunResults(flowScannerExecutionResult: FlowScannerExecutionResu for (const queryId of Object.keys(flowScannerExecutionResult.results)) { const flowScannerRuleResults: FlowScannerRuleResult[] = flowScannerExecutionResult.results[queryId]; for (const flowScannerRuleResult of flowScannerRuleResults) { - const ruleName = getRuleNameFromQueryId(flowScannerRuleResult.query_id); + const ruleName = flowScannerRuleResult.query_id; // Query IDs are the same as rule names const flowNodes: FlowNodeDescriptor[] | undefined = flowScannerRuleResult.flow; if (flowNodes) { // If flow based violation results.violations.push({ diff --git a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts index 40be7779..3f96ca91 100644 --- a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts +++ b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts @@ -1,14 +1,12 @@ import {COMMON_TAGS, RuleDescription, SeverityLevel} from '@salesforce/code-analyzer-engine-api'; import {getMessage} from './messages'; -// Code Analyzer rule names -// Good news: The python flow scanner query ids now happen to be the exact same names as our code analyzer rule names -// so we no longer need to keep a map between the two. +// Code Analyzer rule names (these match the Python flow scanner query IDs 1:1) enum RuleName { CyclicSubflow = 'CyclicSubflow', DbInLoop = 'DbInLoop', DefaultCopy = 'DefaultCopy', - HardcodedId = 'HardCodedId', + HardcodedId = 'HardcodedId', MissingDescription = 'MissingDescription', MissingFaultHandler = 'MissingFaultHandler', MissingNextValueConnector = 'MissingNextValueConnector', @@ -45,7 +43,7 @@ const RULE_DESCRIPTIONS: RuleDescription[] = [ resourceUrls: [] }, { - name: RuleName.HardcodedId, + name: RuleName.HardCodedId, description: getMessage('HardcodedIdRuleDescription'), severityLevel: SeverityLevel.Moderate, tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], @@ -136,32 +134,11 @@ export function getAllRuleNames(): string[] { return Object.values(RuleName); } -export function getRuleNameFromQueryId(queryId: string): string { - // Good news: The python flow scanner query ids now happen to be the exact same names as our code analyzer rule names - // so we no longer need to keep a map between the two. But leaving this helper just in case we need it again in the - // future. - - // istanbul ignore else - if (Object.values(RuleName).includes(queryId as RuleName)) { - return queryId; - } else { - throw new Error(`Developer error: invalid query id ${queryId}`); - } -} - -export function getQueryIdsForRule(ruleName: string): string[] { - // It used to be that a single Code Analyzer rule could map to multiple flow scanner query ids. But now - // they are mapped 1-to-1 and happen to be the exact same names. But keeping the output as a string array - // just in case things change in the future. - const queryIds: string[] = [ruleName]; - return queryIds; -} - export function getDescriptionForRule(ruleName: string): RuleDescription { // istanbul ignore else if (RULE_DESCRIPTIONS_BY_NAME.has(ruleName)) { return RULE_DESCRIPTIONS_BY_NAME.get(ruleName)!; } else { - throw new Error(`Developer rule: No rule with name ${ruleName}`); + throw new Error(`Developer error: No rule with name ${ruleName}`); } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-flow-engine/test/engine.test.ts b/packages/code-analyzer-flow-engine/test/engine.test.ts index 250d661f..97662f0d 100644 --- a/packages/code-analyzer-flow-engine/test/engine.test.ts +++ b/packages/code-analyzer-flow-engine/test/engine.test.ts @@ -17,10 +17,74 @@ import {FlowScannerEngine} from "../src/engine"; import {RunTimeFlowScannerCommandWrapper} from "../src/python/FlowScannerCommandWrapper"; import {changeWorkingDirectoryToPackageRoot, createDescribeOptions, createRunOptions} from "./test-helpers"; import {getMessage} from "../src/messages"; +import {getAllRuleNames} from "../src/hardcoded-catalog"; import * as fs from "node:fs"; changeWorkingDirectoryToPackageRoot(); +/** + * Extracts query IDs from a Python file by parsing the QUERIES dictionary. + * Looks for patterns like: QUERIES = { "QueryId": "Description", ... } + */ +function extractQueryIdsFromPythonFile(filePath: string): string[] { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Match QUERIES = { ... } block + const queriesMatch = content.match(/QUERIES\s*=\s*\{([^}]+)\}/s); + if (!queriesMatch) { + return []; + } + + // Extract keys from the dictionary (supports both single and double quotes) + const queryIds: string[] = []; + const keyPattern = /['"]([^'"]+)['"]\s*:/g; + let match; + while ((match = keyPattern.exec(queriesMatch[1])) !== null) { + queryIds.push(match[1]); + } + return queryIds; +} + +/** + * Gets all Python query IDs by reading the actual Python source files. + * This ensures TypeScript stays in sync with Python automatically. + */ +function getPythonQueryIds(): string[] { + const flowScannerPath = path.resolve(__dirname, '..', 'FlowScanner', 'queries'); + const defaultQueryPath = path.join(flowScannerPath, 'default_query.py'); + const optionalQueryPath = path.join(flowScannerPath, 'optional_query.py'); + + const defaultQueryIds = extractQueryIdsFromPythonFile(defaultQueryPath); + const optionalQueryIds = extractQueryIdsFromPythonFile(optionalQueryPath); + + return [...defaultQueryIds, ...optionalQueryIds].sort(); +} + +describe('TypeScript and Python rule name validation', () => { + it('All TypeScript rule names must match Python query IDs exactly (including case)', () => { + const tsRuleNames = getAllRuleNames().sort(); + const pythonQueryIds = getPythonQueryIds(); + + // Validate same count + expect(tsRuleNames).toHaveLength(pythonQueryIds.length); + + // Validate exact match (case-sensitive) + expect(tsRuleNames).toEqual(pythonQueryIds); + }); + + it('No duplicate rule names in TypeScript', () => { + const tsRuleNames = getAllRuleNames(); + const uniqueNames = new Set(tsRuleNames); + expect(tsRuleNames.length).toEqual(uniqueNames.size); + }); + + it('No duplicate query IDs in Python', () => { + const pythonQueryIds = getPythonQueryIds(); + const uniqueIds = new Set(pythonQueryIds); + expect(pythonQueryIds.length).toEqual(uniqueIds.size); + }); +}); + //the space in the "example workspaces" path is important for testing purposes. do not remove. 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'); 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 5d4961f7..28c2e792 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 @@ -33,7 +33,7 @@ "resourceUrls": [] }, { - "name": "HardCodedId", + "name": "HardcodedId", "description": "This rule detects hardcoded IDs within a flow. Hardcoded Ids are a bad practice, and such flows are not appropriate for distribution.", "severityLevel": 3, "tags": [ From 196fc15f9bc55650fcd90ef519be6724decaf6c4 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 5 Dec 2025 12:50:42 +0530 Subject: [PATCH 2/3] Adding changes for queryId mismatch with rule name for flow --- packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts index 3f96ca91..fe22e7da 100644 --- a/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts +++ b/packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts @@ -43,7 +43,7 @@ const RULE_DESCRIPTIONS: RuleDescription[] = [ resourceUrls: [] }, { - name: RuleName.HardCodedId, + name: RuleName.HardcodedId, description: getMessage('HardcodedIdRuleDescription'), severityLevel: SeverityLevel.Moderate, tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.XML], From c3cac7d361a2e4203cf6448ec59e3290faf9ef8c Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 5 Dec 2025 13:04:14 +0530 Subject: [PATCH 3/3] correcting UT --- .../test/engine.test.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/code-analyzer-flow-engine/test/engine.test.ts b/packages/code-analyzer-flow-engine/test/engine.test.ts index 97662f0d..ddbc08e4 100644 --- a/packages/code-analyzer-flow-engine/test/engine.test.ts +++ b/packages/code-analyzer-flow-engine/test/engine.test.ts @@ -60,31 +60,6 @@ function getPythonQueryIds(): string[] { return [...defaultQueryIds, ...optionalQueryIds].sort(); } -describe('TypeScript and Python rule name validation', () => { - it('All TypeScript rule names must match Python query IDs exactly (including case)', () => { - const tsRuleNames = getAllRuleNames().sort(); - const pythonQueryIds = getPythonQueryIds(); - - // Validate same count - expect(tsRuleNames).toHaveLength(pythonQueryIds.length); - - // Validate exact match (case-sensitive) - expect(tsRuleNames).toEqual(pythonQueryIds); - }); - - it('No duplicate rule names in TypeScript', () => { - const tsRuleNames = getAllRuleNames(); - const uniqueNames = new Set(tsRuleNames); - expect(tsRuleNames.length).toEqual(uniqueNames.size); - }); - - it('No duplicate query IDs in Python', () => { - const pythonQueryIds = getPythonQueryIds(); - const uniqueIds = new Set(pythonQueryIds); - expect(pythonQueryIds.length).toEqual(uniqueIds.size); - }); -}); - //the space in the "example workspaces" path is important for testing purposes. do not remove. 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'); @@ -752,4 +727,29 @@ describe('Tests for the FlowScannerEngine', () => { }); }); }); +}); + +describe('TypeScript and Python rule name validation', () => { + it('All TypeScript rule names must match Python query IDs exactly (including case)', () => { + const tsRuleNames = getAllRuleNames().sort(); + const pythonQueryIds = getPythonQueryIds(); + + // Validate same count + expect(tsRuleNames).toHaveLength(pythonQueryIds.length); + + // Validate exact match (case-sensitive) + expect(tsRuleNames).toEqual(pythonQueryIds); + }); + + it('No duplicate rule names in TypeScript', () => { + const tsRuleNames = getAllRuleNames(); + const uniqueNames = new Set(tsRuleNames); + expect(tsRuleNames.length).toEqual(uniqueNames.size); + }); + + it('No duplicate query IDs in Python', () => { + const pythonQueryIds = getPythonQueryIds(); + const uniqueIds = new Set(pythonQueryIds); + expect(pythonQueryIds.length).toEqual(uniqueIds.size); + }); }); \ No newline at end of file