diff --git a/messages/rules-command.md b/messages/rules-command.md index cd0e4e1b6..97c4e1fc3 100644 --- a/messages/rules-command.md +++ b/messages/rules-command.md @@ -24,6 +24,10 @@ We're continually improving Salesforce Code Analyzer. Tell us what you think! Gi <%= config.bin %> <%= command.id %> --rule-selector eslint:Recommended +- List Flow Analyzer rules from an authenticated org (required for that engine): + + <%= config.bin %> <%= command.id %> --rule-selector flowAnalyzer --target-org my-org + - List all the rules for the "eslint" engine: <%= config.bin %> <%= command.id %> --rule-selector eslint @@ -92,6 +96,16 @@ Each targeted file must live within the workspace specified by the –-workspace If you specify `--workspace` but not `--target`, then all the files within your workspace are targeted. +# flags.target-org.summary + +Target org username or alias for remote analysis engines. + +# flags.target-org.description + +Specify the username or alias of a Salesforce org when listing rules from engines that require remote connectivity, such as Flow Analyzer. The org must be authenticated with the Salesforce CLI. + +Flow Analyzer requires this flag (or `engines.flowAnalyzer.target_org` in your `code-analyzer.yml`). Other engines ignore it. + # flags.rule-selector.summary Selection of rules, based on engine name, severity level, rule name, tag, or a combination of criteria separated by colons. diff --git a/messages/run-command.md b/messages/run-command.md index 12c0687e8..d947c5ac4 100644 --- a/messages/run-command.md +++ b/messages/run-command.md @@ -44,6 +44,10 @@ We're continually improving Salesforce Code Analyzer. Tell us what you think! Gi <%= config.bin %> <%= command.id %> --rule-selector retire-js:Recommended --target ./other-source --target ./force-app/**/*.cls +- Analyze only specific flows with the "flowAnalyzer" engine; target one flow by path, plus every flow whose file name starts with "Account_" in any folder (quote globs so your shell doesn't expand them). Only the targeted flows are sent to your org: + + <%= config.bin %> <%= command.id %> --rule-selector flowAnalyzer --target-org my-org --target ./force-app/main/default/flows/My_Flow.flow-meta.xml --target "./force-app/**/Account_*.flow-meta.xml" + - Specify a custom configuration file and output the results to the "results.csv" file in CSV format; the commands fails if it finds a violation that exceeds the moderate severity level (3): <%= config.bin %> <%= command.id %> --config-file ./code-analyzer2.yml --output-file results.csv --severity-threshold 3 @@ -92,7 +96,9 @@ Target org username or alias for remote analysis engines. # flags.target-org.description -Specify the username or alias of a Salesforce org when using engines that require remote connectivity, such as ApexGuru. The org must be authenticated with the Salesforce CLI. +Specify the username or alias of a Salesforce org when using engines that require remote connectivity, such as ApexGuru or Flow Analyzer. The org must be authenticated with the Salesforce CLI. + +Flow Analyzer requires this flag (or `engines.flowAnalyzer.target_org` in your `code-analyzer.yml`). # flags.rule-selector.summary diff --git a/package-lock.json b/package-lock.json index aee2a8277..5d56f8cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@salesforce/code-analyzer-engine-api": "0.42.0", "@salesforce/code-analyzer-eslint-engine": "0.47.0", "@salesforce/code-analyzer-flow-engine": "0.41.0", + "@salesforce/code-analyzer-flowanalyzer-engine": "file:../code-analyzer-core/packages/code-analyzer-flowanalyzer-engine", "@salesforce/code-analyzer-pmd-engine": "0.46.0", "@salesforce/code-analyzer-regex-engine": "0.40.0", "@salesforce/code-analyzer-retirejs-engine": "0.39.0", @@ -53,6 +54,32 @@ "node": ">=20.0.0" } }, + "../code-analyzer-core/packages/code-analyzer-flowanalyzer-engine": { + "name": "@salesforce/code-analyzer-flowanalyzer-engine", + "version": "0.1.0-SNAPSHOT", + "license": "BSD-3-Clause", + "dependencies": { + "@salesforce/code-analyzer-engine-api": "0.42.0", + "@salesforce/core": "^8.31.2", + "archiver": "^7.0.1", + "form-data": "^4.0.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.5", + "@types/archiver": "^6.0.4", + "@types/jest": "^30.0.0", + "@types/node": "^20.0.0", + "eslint": "^9.39.5", + "jest": "^30.4.2", + "rimraf": "^6.1.3", + "ts-jest": "^29.4.12", + "typescript": "^5.9.3", + "typescript-eslint": "^8.67.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", @@ -5780,6 +5807,10 @@ "node": ">=20.0.0" } }, + "node_modules/@salesforce/code-analyzer-flowanalyzer-engine": { + "resolved": "../code-analyzer-core/packages/code-analyzer-flowanalyzer-engine", + "link": true + }, "node_modules/@salesforce/code-analyzer-pmd-engine": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-pmd-engine/-/code-analyzer-pmd-engine-0.46.0.tgz", diff --git a/package.json b/package.json index c4dfe08be..dba3a6537 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@salesforce/code-analyzer-engine-api": "0.42.0", "@salesforce/code-analyzer-eslint-engine": "0.47.0", "@salesforce/code-analyzer-flow-engine": "0.41.0", + "@salesforce/code-analyzer-flowanalyzer-engine": "file:../code-analyzer-core/packages/code-analyzer-flowanalyzer-engine", "@salesforce/code-analyzer-pmd-engine": "0.46.0", "@salesforce/code-analyzer-regex-engine": "0.40.0", "@salesforce/code-analyzer-retirejs-engine": "0.39.0", diff --git a/src/commands/code-analyzer/rules.ts b/src/commands/code-analyzer/rules.ts index f7acba2b4..b8102bc71 100644 --- a/src/commands/code-analyzer/rules.ts +++ b/src/commands/code-analyzer/rules.ts @@ -34,6 +34,12 @@ export default class RulesCommand extends SfCommand implements Displayable multiple: true, delimiter: ',' }), + 'target-org': Flags.string({ + summary: getMessage(BundleName.RulesCommand, 'flags.target-org.summary'), + description: getMessage(BundleName.RulesCommand, 'flags.target-org.description'), + // eslint-disable-next-line sf-plugin/dash-o + char: 'o' + }), 'rule-selector': Flags.string({ summary: getMessage(BundleName.RulesCommand, 'flags.rule-selector.summary'), description: getMessage(BundleName.RulesCommand, 'flags.rule-selector.description'), @@ -75,7 +81,8 @@ export default class RulesCommand extends SfCommand implements Displayable 'output-file': outputFiles, 'rule-selector': parsedFlags['rule-selector'].flatMap(s => s.replace(/\s+/g, ' ').trim().split(' ')), 'workspace': parsedFlags['workspace'], - 'target': parsedFlags['target'] + 'target': parsedFlags['target'], + 'target-org': parsedFlags['target-org'] }; await action.execute(rulesInput); diff --git a/src/lib/actions/RulesAction.ts b/src/lib/actions/RulesAction.ts index da4341d2c..e1312713f 100644 --- a/src/lib/actions/RulesAction.ts +++ b/src/lib/actions/RulesAction.ts @@ -29,6 +29,7 @@ export type RulesInput = { 'output-file'?: string[]; workspace?: string[]; target?: string[]; + 'target-org'?: string; view?: string; } @@ -40,7 +41,10 @@ export class RulesAction { } public async execute(input: RulesInput): Promise { - const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file']); + const cliOverrides = input['target-org'] !== undefined + ? {targetOrg: input['target-org']} + : undefined; + const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file'], cliOverrides); const logWriter: LogFileWriter = await LogFileWriter.fromConfig(config); this.dependencies.actionSummaryViewer.viewPreExecutionSummary(logWriter.getLogDestination()); // We always add a Logger Listener to the appropriate listeners list, because we should Always Be Logging. diff --git a/src/lib/factories/CodeAnalyzerConfigFactory.ts b/src/lib/factories/CodeAnalyzerConfigFactory.ts index fcb28504d..1f44d373b 100644 --- a/src/lib/factories/CodeAnalyzerConfigFactory.ts +++ b/src/lib/factories/CodeAnalyzerConfigFactory.ts @@ -92,7 +92,7 @@ export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory }; } - // Apply target-org override to apexguru engine config + // Apply target-org override to engines that require an explicit org if (cliOverrides.targetOrg) { const enginesSection = mergedConfig.engines as Record> | undefined; mergedConfig = { @@ -102,6 +102,10 @@ export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory apexguru: { ...(enginesSection?.apexguru || {}), target_org: cliOverrides.targetOrg + }, + flowAnalyzer: { + ...(enginesSection?.flowAnalyzer || {}), + target_org: cliOverrides.targetOrg } } }; @@ -122,6 +126,9 @@ export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory configObject.engines = { apexguru: { target_org: cliOverrides.targetOrg + }, + flowAnalyzer: { + target_org: cliOverrides.targetOrg } }; } diff --git a/src/lib/factories/EnginePluginsFactory.ts b/src/lib/factories/EnginePluginsFactory.ts index b36cdba7a..0c346f610 100644 --- a/src/lib/factories/EnginePluginsFactory.ts +++ b/src/lib/factories/EnginePluginsFactory.ts @@ -7,6 +7,7 @@ import * as FlowEngineModule from '@salesforce/code-analyzer-flow-engine'; import * as SfgeEngineModule from '@salesforce/code-analyzer-sfge-engine'; import * as ApexGuruEngineModule from '@salesforce/code-analyzer-apexguru-engine'; import * as UIBundleEngineModule from '@salesforce/code-analyzer-uibundle-engine'; +import * as FlowAnalyzerEngineModule from '@salesforce/code-analyzer-flowanalyzer-engine'; export interface EnginePluginsFactory { create(): EnginePlugin[]; @@ -22,7 +23,8 @@ export class EnginePluginsFactoryImpl implements EnginePluginsFactory { FlowEngineModule.createEnginePlugin(), SfgeEngineModule.createEnginePlugin(), ApexGuruEngineModule.createEnginePlugin(), - UIBundleEngineModule.createEnginePlugin() + UIBundleEngineModule.createEnginePlugin(), + FlowAnalyzerEngineModule.createEnginePlugin() ]; } } diff --git a/test/commands/code-analyzer/rules.test.ts b/test/commands/code-analyzer/rules.test.ts index 17a66815f..7c7b9f38f 100644 --- a/test/commands/code-analyzer/rules.test.ts +++ b/test/commands/code-analyzer/rules.test.ts @@ -275,6 +275,22 @@ describe('`code-analyzer rules` unit tests', () => { }); }); + describe('--target-org', () => { + it('Can be supplied with a value', async () => { + const inputValue = 'test-org'; + await runRulesCommand(['--target-org', inputValue]); + expect(executeSpy).toHaveBeenCalled(); + expect(receivedActionInput).toHaveProperty('target-org', inputValue); + }); + + it('Can be referenced by its shortname, -o', async () => { + const inputValue = 'my-org'; + await runRulesCommand(['-o', inputValue]); + expect(executeSpy).toHaveBeenCalled(); + expect(receivedActionInput).toHaveProperty('target-org', inputValue); + }); + }); + describe.each([ {flag: '--workspace', shortflag: '-w', property: 'workspace'}, {flag: '--target', shortflag: '-t', property: 'target'}, diff --git a/test/lib/actions/RulesAction.test.ts b/test/lib/actions/RulesAction.test.ts index 1a278e126..687b21e50 100644 --- a/test/lib/actions/RulesAction.test.ts +++ b/test/lib/actions/RulesAction.test.ts @@ -174,6 +174,39 @@ describe('RulesAction tests', () => { expect(viewerCallHistory[0]).toEqual([]); }); + describe('target-org', () => { + it('passes target-org through to engine config', async () => { + const targetOrg = 'my-test-org'; + const configFactorySpy = vi.spyOn(defaultDependencies.configFactory, 'create'); + const action = RulesAction.createAction(defaultDependencies); + const input: RulesInput = { + 'rule-selector': ['all'], + 'target-org': targetOrg + }; + + await action.execute(input); + + expect(configFactorySpy).toHaveBeenCalledWith( + undefined, + expect.objectContaining({ + targetOrg: targetOrg + }) + ); + }); + + it('does not include a target-org override when target-org is not provided', async () => { + const configFactorySpy = vi.spyOn(defaultDependencies.configFactory, 'create'); + const action = RulesAction.createAction(defaultDependencies); + const input: RulesInput = { + 'rule-selector': ['all'] + }; + + await action.execute(input); + + expect(configFactorySpy).toHaveBeenCalledWith(undefined, undefined); + }); + }); + it('Throws an error when an engine throws an error', async () => { const dependencies: RulesDependencies = { ...defaultDependencies, diff --git a/test/lib/factories/CodeAnalyzerConfigFactory.test.ts b/test/lib/factories/CodeAnalyzerConfigFactory.test.ts index ba0d0bc18..baef53d0a 100644 --- a/test/lib/factories/CodeAnalyzerConfigFactory.test.ts +++ b/test/lib/factories/CodeAnalyzerConfigFactory.test.ts @@ -290,6 +290,9 @@ describe('CodeAnalyzerConfigFactoryImpl', () => { expect(testedConfig.getEngineOverridesFor('apexguru')).toEqual({ target_org: 'my-org@example.com' }); + expect(testedConfig.getEngineOverridesFor('flowAnalyzer')).toEqual({ + target_org: 'my-org@example.com' + }); }); it('When config file exists (without engines) and targetOrg override is provided, applies it to apexguru engine', () => { @@ -301,6 +304,9 @@ describe('CodeAnalyzerConfigFactoryImpl', () => { expect(testedConfig.getEngineOverridesFor('apexguru')).toEqual({ target_org: 'my-org@example.com' }); + expect(testedConfig.getEngineOverridesFor('flowAnalyzer')).toEqual({ + target_org: 'my-org@example.com' + }); // Original config values should still be loaded expect(testedConfig.getRuleOverridesFor('stubEngine1')).toEqual({ stub1RuleC: { @@ -321,6 +327,9 @@ describe('CodeAnalyzerConfigFactoryImpl', () => { expect(testedConfig.getEngineOverridesFor('apexguru')).toEqual({ target_org: 'my-org@example.com' }); + expect(testedConfig.getEngineOverridesFor('flowAnalyzer')).toEqual({ + target_org: 'my-org@example.com' + }); }); it('When both noSuppressions and targetOrg overrides are provided, both are applied', () => { @@ -334,6 +343,9 @@ describe('CodeAnalyzerConfigFactoryImpl', () => { expect(testedConfig.getEngineOverridesFor('apexguru')).toEqual({ target_org: 'my-org@example.com' }); + expect(testedConfig.getEngineOverridesFor('flowAnalyzer')).toEqual({ + target_org: 'my-org@example.com' + }); }); }); }); diff --git a/test/lib/factories/EnginePluginsFactory.test.ts b/test/lib/factories/EnginePluginsFactory.test.ts index 7c59561b2..09ca596a3 100644 --- a/test/lib/factories/EnginePluginsFactory.test.ts +++ b/test/lib/factories/EnginePluginsFactory.test.ts @@ -6,7 +6,7 @@ describe('EnginePluginsFactoryImpl', () => { const pluginsFactory = new EnginePluginsFactoryImpl(); const enginePlugins = pluginsFactory.create(); - expect(enginePlugins).toHaveLength(8); + expect(enginePlugins).toHaveLength(9); expect(enginePlugins[0].getAvailableEngineNames()).toEqual(['eslint']); expect(enginePlugins[1].getAvailableEngineNames()).toEqual(['pmd', 'cpd']); expect(enginePlugins[2].getAvailableEngineNames()).toEqual(['retire-js']); @@ -15,5 +15,6 @@ describe('EnginePluginsFactoryImpl', () => { expect(enginePlugins[5].getAvailableEngineNames()).toEqual(['sfge']); expect(enginePlugins[6].getAvailableEngineNames()).toEqual(['apexguru']); expect(enginePlugins[7].getAvailableEngineNames()).toEqual(['uibundle']); + expect(enginePlugins[8].getAvailableEngineNames()).toEqual(['flowAnalyzer']); }); });