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..fe22e7da 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', @@ -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..ddbc08e4 100644 --- a/packages/code-analyzer-flow-engine/test/engine.test.ts +++ b/packages/code-analyzer-flow-engine/test/engine.test.ts @@ -17,10 +17,49 @@ 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(); +} + //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'); @@ -688,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 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": [