Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/code-analyzer-core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/code-analyzer-flow-engine/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 4 additions & 3 deletions packages/code-analyzer-flow-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
31 changes: 4 additions & 27 deletions packages/code-analyzer-flow-engine/src/hardcoded-catalog.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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}`);
}
}
}
64 changes: 64 additions & 0 deletions packages/code-analyzer-flow-engine/test/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down