From 00173de1401d76e038ddf56b3d677a85947541b5 Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Tue, 17 Mar 2026 12:38:09 +0530 Subject: [PATCH 1/2] fixes and suggestions support --- messages/run-command.md | 16 ++++++ src/commands/code-analyzer/run.ts | 17 ++++++- src/lib/actions/RunAction.ts | 9 +++- test/lib/actions/RunAction.test.ts | 81 ++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 4 deletions(-) diff --git a/messages/run-command.md b/messages/run-command.md index 1117844b7..c84714250 100644 --- a/messages/run-command.md +++ b/messages/run-command.md @@ -142,6 +142,22 @@ To output the results to multiple files, specify this flag multiple times. For e If you specify a file within a folder, such as `--output-file ./out/results.json`, the folder must already exist, or you get an error. If the file already exists, it's overwritten without prompting. +# flags.include-fixes.summary + +Include fix data for violations when available. + +# flags.include-fixes.description + +When enabled, the output includes fix information for violations that have auto-fixes available. Each fix contains a code location and the replacement code. This flag may increase analysis time because engines must perform additional processing to compute fixes. + +# flags.include-suggestions.summary + +Include suggestion data for violations when available. + +# flags.include-suggestions.description + +When enabled, the output includes suggestion information for violations that have suggestions available. Each suggestion contains a code location and a message describing the suggested change. + # error.invalid-severity-threshold Expected --severity-threshold=%s to be one of: %s diff --git a/src/commands/code-analyzer/run.ts b/src/commands/code-analyzer/run.ts index ff3773332..17d2f08b2 100644 --- a/src/commands/code-analyzer/run.ts +++ b/src/commands/code-analyzer/run.ts @@ -70,7 +70,18 @@ export default class RunCommand extends SfCommand implements Displayable { description: getMessage(BundleName.RunCommand, 'flags.config-file.description'), char: 'c', exists: true - }) + }), + // === Flags pertaining to fixes and suggestions === + 'include-fixes': Flags.boolean({ + summary: getMessage(BundleName.RunCommand, 'flags.include-fixes.summary'), + description: getMessage(BundleName.RunCommand, 'flags.include-fixes.description'), + default: false + }), + 'include-suggestions': Flags.boolean({ + summary: getMessage(BundleName.RunCommand, 'flags.include-suggestions.summary'), + description: getMessage(BundleName.RunCommand, 'flags.include-suggestions.description'), + default: false + }), }; public async run(): Promise { @@ -84,7 +95,9 @@ export default class RunCommand extends SfCommand implements Displayable { 'workspace': parsedFlags['workspace'], 'severity-threshold': parsedFlags['severity-threshold'] === undefined ? undefined : convertThresholdToEnum(parsedFlags['severity-threshold'].toLowerCase()), - 'target': parsedFlags['target'] + 'target': parsedFlags['target'], + 'include-fixes': parsedFlags['include-fixes'], + 'include-suggestions': parsedFlags['include-suggestions'] }; await action.execute(runInput); } diff --git a/src/lib/actions/RunAction.ts b/src/lib/actions/RunAction.ts index 510620c3d..2b02b988d 100644 --- a/src/lib/actions/RunAction.ts +++ b/src/lib/actions/RunAction.ts @@ -40,7 +40,8 @@ export type RunInput = { 'severity-threshold'?: SeverityLevel; target?: string[]; workspace: string[]; - + 'include-fixes'?: boolean; + 'include-suggestions'?: boolean; } export class RunAction { @@ -75,7 +76,11 @@ export class RunAction { // that's when progress events can start being emitted. this.dependencies.progressListeners.forEach(listener => listener.listen(core)); const ruleSelection: RuleSelection = await core.selectRules(input['rule-selector'], {workspace}); - const runOptions: RunOptions = {workspace}; + const runOptions: RunOptions = { + workspace, + includeFixes: input['include-fixes'], + includeSuggestions: input['include-suggestions'] + }; const results: RunResults = await core.run(ruleSelection, runOptions); this.emitEngineTelemetry(ruleSelection, results, enginePlugins.flatMap(p => p.getAvailableEngineNames())); // After Core is done running, the listeners need to be told to stop, since some of them have persistent UI elements diff --git a/test/lib/actions/RunAction.test.ts b/test/lib/actions/RunAction.test.ts index aa5591c3b..8829dfbb2 100644 --- a/test/lib/actions/RunAction.test.ts +++ b/test/lib/actions/RunAction.test.ts @@ -407,6 +407,87 @@ describe('RunAction tests', () => { expect(spyTelemetryEmitter.getCapturedTelemetry()[3].data.violationCount).toEqual(0); }); }) + + describe('include-fixes and include-suggestions flags', () => { + it.each([ + {case: 'neither flag set', includeFixes: undefined, includeSuggestions: undefined}, + {case: 'include-fixes=false, include-suggestions=false', includeFixes: false, includeSuggestions: false}, + ])('When $case, both are passed as-is to RunOptions', async ({includeFixes, includeSuggestions}) => { + // ==== SETUP ==== + const input: RunInput = { + 'rule-selector': ['all'], + 'workspace': ['.'], + 'output-file': [], + 'include-fixes': includeFixes, + 'include-suggestions': includeSuggestions + }; + + // ==== TESTED BEHAVIOR ==== + await action.execute(input); + + // ==== ASSERTIONS ==== + const runOptions = engine1.runRulesCallHistory[0].runOptions; + expect(runOptions.includeFixes).toEqual(includeFixes); + expect(runOptions.includeSuggestions).toEqual(includeSuggestions); + }); + + it('When include-fixes=true, it is forwarded to RunOptions', async () => { + // ==== SETUP ==== + const input: RunInput = { + 'rule-selector': ['all'], + 'workspace': ['.'], + 'output-file': [], + 'include-fixes': true, + 'include-suggestions': false + }; + + // ==== TESTED BEHAVIOR ==== + await action.execute(input); + + // ==== ASSERTIONS ==== + const runOptions = engine1.runRulesCallHistory[0].runOptions; + expect(runOptions.includeFixes).toBe(true); + expect(runOptions.includeSuggestions).toBe(false); + }); + + it('When include-suggestions=true, it is forwarded to RunOptions', async () => { + // ==== SETUP ==== + const input: RunInput = { + 'rule-selector': ['all'], + 'workspace': ['.'], + 'output-file': [], + 'include-fixes': false, + 'include-suggestions': true + }; + + // ==== TESTED BEHAVIOR ==== + await action.execute(input); + + // ==== ASSERTIONS ==== + const runOptions = engine1.runRulesCallHistory[0].runOptions; + expect(runOptions.includeFixes).toBe(false); + expect(runOptions.includeSuggestions).toBe(true); + }); + + it('When both include-fixes=true and include-suggestions=true, both are forwarded to RunOptions', async () => { + // ==== SETUP ==== + const input: RunInput = { + 'rule-selector': ['all'], + 'workspace': ['.'], + 'output-file': [], + 'include-fixes': true, + 'include-suggestions': true + }; + + // ==== TESTED BEHAVIOR ==== + await action.execute(input); + + // ==== ASSERTIONS ==== + const runOptions = engine1.runRulesCallHistory[0].runOptions; + expect(runOptions.includeFixes).toBe(true); + expect(runOptions.includeSuggestions).toBe(true); + }); + }); }); // TODO: Whenever we decide to document the custom_engine_plugin_modules flag in our configuration file, then we'll want From ae3c7452032bd9d2151b04e4961e92ce034ca56f Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Wed, 18 Mar 2026 18:54:19 +0530 Subject: [PATCH 2/2] add test cases --- test/lib/actions/RunAction.test.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/test/lib/actions/RunAction.test.ts b/test/lib/actions/RunAction.test.ts index 8829dfbb2..09a5d8de1 100644 --- a/test/lib/actions/RunAction.test.ts +++ b/test/lib/actions/RunAction.test.ts @@ -413,7 +413,6 @@ describe('RunAction tests', () => { {case: 'neither flag set', includeFixes: undefined, includeSuggestions: undefined}, {case: 'include-fixes=false, include-suggestions=false', includeFixes: false, includeSuggestions: false}, ])('When $case, both are passed as-is to RunOptions', async ({includeFixes, includeSuggestions}) => { - // ==== SETUP ==== const input: RunInput = { 'rule-selector': ['all'], 'workspace': ['.'], @@ -422,17 +421,15 @@ describe('RunAction tests', () => { 'include-suggestions': includeSuggestions }; - // ==== TESTED BEHAVIOR ==== - await action.execute(input); - // ==== ASSERTIONS ==== + await action.execute(input); const runOptions = engine1.runRulesCallHistory[0].runOptions; expect(runOptions.includeFixes).toEqual(includeFixes); expect(runOptions.includeSuggestions).toEqual(includeSuggestions); }); it('When include-fixes=true, it is forwarded to RunOptions', async () => { - // ==== SETUP ==== + const input: RunInput = { 'rule-selector': ['all'], 'workspace': ['.'], @@ -441,17 +438,15 @@ describe('RunAction tests', () => { 'include-suggestions': false }; - // ==== TESTED BEHAVIOR ==== await action.execute(input); - // ==== ASSERTIONS ==== const runOptions = engine1.runRulesCallHistory[0].runOptions; expect(runOptions.includeFixes).toBe(true); expect(runOptions.includeSuggestions).toBe(false); }); it('When include-suggestions=true, it is forwarded to RunOptions', async () => { - // ==== SETUP ==== + const input: RunInput = { 'rule-selector': ['all'], 'workspace': ['.'], @@ -460,17 +455,14 @@ describe('RunAction tests', () => { 'include-suggestions': true }; - // ==== TESTED BEHAVIOR ==== await action.execute(input); - // ==== ASSERTIONS ==== const runOptions = engine1.runRulesCallHistory[0].runOptions; expect(runOptions.includeFixes).toBe(false); expect(runOptions.includeSuggestions).toBe(true); }); it('When both include-fixes=true and include-suggestions=true, both are forwarded to RunOptions', async () => { - // ==== SETUP ==== const input: RunInput = { 'rule-selector': ['all'], 'workspace': ['.'], @@ -479,10 +471,8 @@ describe('RunAction tests', () => { 'include-suggestions': true }; - // ==== TESTED BEHAVIOR ==== await action.execute(input); - // ==== ASSERTIONS ==== const runOptions = engine1.runRulesCallHistory[0].runOptions; expect(runOptions.includeFixes).toBe(true); expect(runOptions.includeSuggestions).toBe(true);