From db274c3851f2dcc25961adc437c4338fbfb4026e Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Wed, 18 Mar 2026 15:30:00 +0530 Subject: [PATCH 1/3] adding changes for inline marker cli flag --- messages/run-command.md | 14 ++ src/commands/code-analyzer/run.ts | 9 +- src/lib/actions/RunAction.ts | 10 +- .../factories/CodeAnalyzerConfigFactory.ts | 91 ++++++++- test/commands/code-analyzer/run.test.ts | 66 +++++++ .../workspace-with-suppressions/TestClass.cls | 28 +++ .../CodeAnalyzerConfigFactory.test.ts | 174 ++++++++++++++++++ 7 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/example-workspaces/workspace-with-suppressions/TestClass.cls diff --git a/messages/run-command.md b/messages/run-command.md index 1117844b7..c6d70a01a 100644 --- a/messages/run-command.md +++ b/messages/run-command.md @@ -56,6 +56,10 @@ We're continually improving Salesforce Code Analyzer. Tell us what you think! Gi <%= config.bin %> <%= command.id %> --rule-selector eslint:getter-return --rule-selector no-inner-declarations +- Analyze the files and ignore all inline suppression markers (code-analyzer-suppress/unsuppress) in the source code: + + <%= config.bin %> <%= command.id %> --no-suppressions + # flags.workspace.summary Set of files that make up your workspace. @@ -142,6 +146,16 @@ 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.no-suppressions.summary + +Disable processing of inline suppression markers. + +# flags.no-suppressions.description + +By default, Code Analyzer processes inline suppression markers (code-analyzer-suppress and code-analyzer-unsuppress) found in your source code to filter out violations. Use this flag to ignore all suppression markers and report all violations. + +Note: If you have a `code-analyzer.yml` or `code-analyzer.yaml` configuration file with the `suppressions.disable_suppressions` field, the configuration file takes precedence over this flag. + # 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..94365291a 100644 --- a/src/commands/code-analyzer/run.ts +++ b/src/commands/code-analyzer/run.ts @@ -70,6 +70,12 @@ export default class RunCommand extends SfCommand implements Displayable { description: getMessage(BundleName.RunCommand, 'flags.config-file.description'), char: 'c', exists: true + }), + 'no-suppressions': Flags.boolean({ + summary: getMessage(BundleName.RunCommand, 'flags.no-suppressions.summary'), + description: getMessage(BundleName.RunCommand, 'flags.no-suppressions.description'), + default: false, + required: false }) }; @@ -84,7 +90,8 @@ 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'], + 'no-suppressions': parsedFlags['no-suppressions'] }; await action.execute(runInput); } diff --git a/src/lib/actions/RunAction.ts b/src/lib/actions/RunAction.ts index 510620c3d..d5db9a456 100644 --- a/src/lib/actions/RunAction.ts +++ b/src/lib/actions/RunAction.ts @@ -40,7 +40,7 @@ export type RunInput = { 'severity-threshold'?: SeverityLevel; target?: string[]; workspace: string[]; - + 'no-suppressions'?: boolean; } export class RunAction { @@ -51,7 +51,13 @@ export class RunAction { } public async execute(input: RunInput): Promise { - const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file']); + const cliOverrides = input['no-suppressions'] !== undefined + ? { noSuppressions: input['no-suppressions'] } + : 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 6264d554e..0bcaec33d 100644 --- a/src/lib/factories/CodeAnalyzerConfigFactory.ts +++ b/src/lib/factories/CodeAnalyzerConfigFactory.ts @@ -1,19 +1,39 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import {CodeAnalyzerConfig} from '@salesforce/code-analyzer-core'; +import * as yaml from 'js-yaml'; + +export type CliOverrides = { + noSuppressions?: boolean; + // Future CLI flag overrides can be added here +} export interface CodeAnalyzerConfigFactory { - create(configPath?: string): CodeAnalyzerConfig; + create(configPath?: string, cliOverrides?: CliOverrides): CodeAnalyzerConfig; } export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory { private static readonly CONFIG_FILE_NAME: string = 'code-analyzer'; private static readonly CONFIG_FILE_EXTENSIONS: string[] = ['yaml', 'yml']; - public create(configPath?: string): CodeAnalyzerConfig { - return this.getConfigFromProvidedPath(configPath) + public create(configPath?: string, cliOverrides?: CliOverrides): CodeAnalyzerConfig { + // Fast path: If no CLI overrides, use existing simple logic + if (!cliOverrides || cliOverrides.noSuppressions === undefined) { + return this.getConfigFromProvidedPath(configPath) || this.seekConfigInCurrentDirectory() || CodeAnalyzerConfig.withDefaults(); + } + + // CLI overrides present - need to get file path to read raw YAML + const usedPath = this.getConfigFilePath(configPath); + + // If config file exists, read YAML and apply CLI overrides if needed + if (usedPath) { + return this.createConfigFromFile(usedPath, cliOverrides); + } + + // No config file found - create config from CLI overrides or defaults + return this.createConfigFromCliOverrides(cliOverrides); } private getConfigFromProvidedPath(configPath?: string): CodeAnalyzerConfig|undefined { @@ -29,4 +49,69 @@ export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory } return undefined; } + + private createConfigFromFile( + configFilePath: string, + cliOverrides: CliOverrides + ): CodeAnalyzerConfig { + // Read raw YAML to check if suppressions field is explicitly set + const rawYaml: Record | undefined = this.readRawYamlFile(configFilePath); + const suppressionsExplicitlySet = (rawYaml?.suppressions as Record | undefined)?.disable_suppressions !== undefined; + + if (suppressionsExplicitlySet) { + // YAML explicitly sets suppressions - use it as-is (YAML wins) + return CodeAnalyzerConfig.fromFile(configFilePath); + } + + // Config file exists but doesn't specify suppressions - merge with CLI overrides + if (cliOverrides.noSuppressions !== undefined && rawYaml) { + const mergedConfig: Record = { + ...rawYaml, + suppressions: { disable_suppressions: cliOverrides.noSuppressions } + }; + return CodeAnalyzerConfig.fromObject(mergedConfig); + } + + // Config file exists, no CLI override, use config as-is with defaults + return CodeAnalyzerConfig.fromFile(configFilePath); + } + + private createConfigFromCliOverrides(cliOverrides: CliOverrides): CodeAnalyzerConfig { + // Apply CLI overrides if provided + if (cliOverrides?.noSuppressions) { + return CodeAnalyzerConfig.fromObject({ + suppressions: { disable_suppressions: true } + }); + } + + // No config file, no CLI overrides - use defaults (suppressions enabled) + return CodeAnalyzerConfig.withDefaults(); + } + + private getConfigFilePath(configPath?: string): string|undefined { + // If explicit path provided, use it + if (configPath) { + return configPath; + } + + // Otherwise, seek in current directory + for (const ext of CodeAnalyzerConfigFactoryImpl.CONFIG_FILE_EXTENSIONS) { + const possibleConfigFilePath = path.resolve(`${CodeAnalyzerConfigFactoryImpl.CONFIG_FILE_NAME}.${ext}`); + if (fs.existsSync(possibleConfigFilePath)) { + return possibleConfigFilePath; + } + } + return undefined; + } + + private readRawYamlFile(filePath: string): Record | undefined { + try { + const fileContents = fs.readFileSync(filePath, 'utf8'); + return yaml.load(fileContents) as Record; + } catch (_err) { + // If file can't be read or parsed, return undefined + // The config loading will handle the error + return undefined; + } + } } diff --git a/test/commands/code-analyzer/run.test.ts b/test/commands/code-analyzer/run.test.ts index 312b9c7fd..3c52c0b05 100644 --- a/test/commands/code-analyzer/run.test.ts +++ b/test/commands/code-analyzer/run.test.ts @@ -67,6 +67,72 @@ describe('`code-analyzer run` end to end tests', () => { }); }); +describe('`code-analyzer run` end to end tests for inline suppressions', () => { + const origDir: string = process.cwd(); + const suppressionWorkspace: string = path.resolve(rootFolderWithPackageJson, 'test', 'fixtures', 'example-workspaces', 'workspace-with-suppressions'); + + beforeAll(async () => { + process.chdir(suppressionWorkspace); + await config.load(); + }); + + afterAll(async () => { + process.chdir(origDir); + }); + + it('Inline suppression markers should suppress violations for marked methods', async () => { + const outputInterceptor: ConsoleOuputInterceptor = new ConsoleOuputInterceptor(); + try { + outputInterceptor.start(); + await runRunCommand(['-r', 'pmd', '-t', 'TestClass.cls']); + } finally { + outputInterceptor.stop(); + } + + const output = outputInterceptor.out; + + // Should not throw any unexpected errors + expect(output).not.toContain('threw an unexpected error'); + + // Verify suppressions were applied + expect(output).toContain('suppressed by inline suppression markers'); + + // Line 7 - methodWithoutDoc (no suppression) - should have ApexDoc violation + expect(output).toContain('TestClass.cls:7'); + expect(output).toContain('ApexDoc'); + + // Line 25 - unsuppressedMethod (after unsuppress marker) - should have ApexDoc violation + expect(output).toContain('TestClass.cls:25'); + + // Line 13 - suppressedMethod has code-analyzer-suppress(pmd:ApexDoc) + // ApexDoc violation should NOT appear for line 13 + expect(output).not.toMatch(/TestClass\.cls:13.*ApexDoc/); + + // Line 19-21 - allSuppressedMethod has code-analyzer-suppress (all rules) + // No violations should appear for lines 19, 20, or 21 + expect(output).not.toContain('TestClass.cls:19'); + expect(output).not.toContain('TestClass.cls:20'); + expect(output).not.toContain('TestClass.cls:21'); + }); + + it('Specific rule suppression only suppresses that rule, not others', async () => { + const outputInterceptor: ConsoleOuputInterceptor = new ConsoleOuputInterceptor(); + try { + outputInterceptor.start(); + await runRunCommand(['-r', 'pmd', '-t', 'TestClass.cls']); + } finally { + outputInterceptor.stop(); + } + + const output = outputInterceptor.out; + + // code-analyzer-suppress(pmd:ApexDoc) on line 11 only suppresses ApexDoc + // AvoidDebugStatements on line 14 should still appear (not suppressed) + expect(output).toContain('TestClass.cls:14'); + expect(output).toContain('AvoidDebugStatements'); + }); +}); + describe('`code-analyzer run` unit tests', () => { beforeAll(async () => { await config.load(); diff --git a/test/fixtures/example-workspaces/workspace-with-suppressions/TestClass.cls b/test/fixtures/example-workspaces/workspace-with-suppressions/TestClass.cls new file mode 100644 index 000000000..b29aa00d2 --- /dev/null +++ b/test/fixtures/example-workspaces/workspace-with-suppressions/TestClass.cls @@ -0,0 +1,28 @@ +/** + * Test class demonstrating inline suppression markers + */ +public class TestClass { + + // This method has no ApexDoc - should trigger violation + public void methodWithoutDoc() { + System.debug('No suppression here'); + } + + // code-analyzer-suppress(pmd:ApexDoc) + // This method has no ApexDoc but is suppressed - should NOT trigger violation + public void suppressedMethod() { + System.debug('Suppressed'); + } + + // code-analyzer-suppress + // All rules suppressed for this method + public void allSuppressedMethod() { + System.debug('All suppressed'); + } + + // code-analyzer-unsuppress + // Back to normal - should trigger violation again + public void unsuppressedMethod() { + System.debug('Back to normal'); + } +} diff --git a/test/lib/factories/CodeAnalyzerConfigFactory.test.ts b/test/lib/factories/CodeAnalyzerConfigFactory.test.ts index c20a73619..49314c2b0 100644 --- a/test/lib/factories/CodeAnalyzerConfigFactory.test.ts +++ b/test/lib/factories/CodeAnalyzerConfigFactory.test.ts @@ -108,4 +108,178 @@ describe('CodeAnalyzerConfigFactoryImpl', () => { // that the user was informed of this problem. expect(() => factory.create(configPath)).toThrow('nonExistentLogFolder'); }); + + describe('CLI Overrides functionality', () => { + it('When no config file and no CLI overrides, uses defaults (suppressions enabled)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const testedConfig = factory.create(undefined, undefined); + + // Suppressions should be enabled by default + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + }); + + it('When no config file and noSuppressions override is true, disables suppressions', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const testedConfig = factory.create(undefined, { noSuppressions: true }); + + // Suppressions should be disabled due to CLI override + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + }); + + it('When no config file and noSuppressions override is false, uses defaults (suppressions enabled)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const testedConfig = factory.create(undefined, { noSuppressions: false }); + + // Suppressions should be enabled (default behavior) + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + }); + + it('When config file (no suppressions field) exists with explicit path, CLI override is applied', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'sample-config-file.yml'); + + // Config file doesn't have suppressions field, CLI override tries to disable + const testedConfig = factory.create(configPath, { noSuppressions: true }); + + // CLI override should be applied since YAML doesn't specify suppressions + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + // Verify rule overrides from config are still loaded + expect(testedConfig.getRuleOverridesFor('stubEngine1')).toEqual({ + stub1RuleB: { + severity: 1 + }, + stub1RuleD: { + severity: 5, + tags: ["Recommended", "CodeStyle", "Performance"] + } + }); + }); + + it('When config file (no suppressions field) is auto-discovered, CLI override is applied', () => { + const primaryTestDir = process.cwd(); + try { + // Move into directory with config file + process.chdir(path.resolve(import.meta.dirname, '..', '..', 'fixtures', 'example-workspaces', 'workspace-with-yaml-config')); + const factory = new CodeAnalyzerConfigFactoryImpl(); + + // Config file doesn't have suppressions field, CLI override tries to disable + const testedConfig = factory.create(undefined, { noSuppressions: true }); + + // CLI override should be applied since YAML doesn't specify suppressions + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + // Verify rule overrides from config are still loaded + expect(testedConfig.getRuleOverridesFor('stubEngine2')).toEqual({ + stub2RuleA: { + tags: ['Security', 'SomeYamlOnlyTag'] + } + }); + } finally { + process.chdir(primaryTestDir); + } + }); + + it('When YAML config enables suppressions, CLI override to disable is ignored (YAML wins)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-with-suppressions-enabled.yml'); + + // Config file has suppressions enabled (disable_suppressions: false) + // CLI override tries to disable suppressions (noSuppressions: true) + const testedConfig = factory.create(configPath, { noSuppressions: true }); + + // YAML config should win - suppressions should be ENABLED + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + }); + + it('When YAML config enables suppressions, CLI override to keep enabled is ignored (YAML wins, both agree)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-with-suppressions-enabled.yml'); + + // Config file has suppressions enabled (disable_suppressions: false) + // CLI override also tries to keep enabled (noSuppressions: false) + const testedConfig = factory.create(configPath, { noSuppressions: false }); + + // YAML config should win - suppressions should be ENABLED (both agree) + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + }); + + it('When YAML config disables suppressions, CLI override is ignored (YAML wins)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-with-suppressions-disabled.yml'); + + // Config file has suppressions disabled (disable_suppressions: true) + // CLI override is not provided (should not matter) + const testedConfig = factory.create(configPath, { noSuppressions: false }); + + // YAML config should win - suppressions should be DISABLED + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + }); + + it('When YAML config disables suppressions and CLI also tries to disable, YAML wins (both disabled)', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-with-suppressions-disabled.yml'); + + // Both config file and CLI override want to disable suppressions + const testedConfig = factory.create(configPath, { noSuppressions: true }); + + // YAML config wins (but result is same - disabled) + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + }); + + it('When YAML config exists but does not specify suppressions field, CLI override is applied', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-without-suppressions-field.yml'); + + // Config file exists but doesn't have suppressions field + // CLI override tries to disable suppressions + const testedConfig = factory.create(configPath, { noSuppressions: true }); + + // CLI override should be applied since YAML doesn't specify suppressions + expect(testedConfig.getSuppressionsEnabled()).toBe(false); + // Verify rule overrides from config are still loaded + expect(testedConfig.getRuleOverridesFor('stubEngine1')).toEqual({ + stub1RuleC: { + severity: 4, + tags: ['TestTag'] + } + }); + }); + + it('When YAML config exists without suppressions field and CLI override says keep enabled, uses default', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-without-suppressions-field.yml'); + + // Config file exists but doesn't have suppressions field + // CLI override tries to keep suppressions enabled (noSuppressions: false) + const testedConfig = factory.create(configPath, { noSuppressions: false }); + + // CLI override should be applied - suppressions enabled + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + // Verify rule overrides from config are still loaded + expect(testedConfig.getRuleOverridesFor('stubEngine1')).toEqual({ + stub1RuleC: { + severity: 4, + tags: ['TestTag'] + } + }); + }); + + it('When YAML config exists without suppressions field and no CLI override, uses defaults', () => { + const factory = new CodeAnalyzerConfigFactoryImpl(); + const configPath = path.resolve('test', 'fixtures', 'valid-configs', 'config-without-suppressions-field.yml'); + + // Config file exists but doesn't have suppressions field + // No CLI override provided + const testedConfig = factory.create(configPath, undefined); + + // Should use default (suppressions enabled) + expect(testedConfig.getSuppressionsEnabled()).toBe(true); + // Verify rule overrides from config are still loaded + expect(testedConfig.getRuleOverridesFor('stubEngine1')).toEqual({ + stub1RuleC: { + severity: 4, + tags: ['TestTag'] + } + }); + }); + }); }); From 48fb033ad27ec63ad1976709b9920e194a153c33 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Wed, 18 Mar 2026 15:44:34 +0530 Subject: [PATCH 2/3] updating core dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4421802ce..25a3449b9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "@oclif/core": "^4.8.1", "@oclif/table": "^0.5.1", - "@salesforce/code-analyzer-core": "0.43.0", + "@salesforce/code-analyzer-core": "0.44.0", "@salesforce/code-analyzer-engine-api": "0.35.0", "@salesforce/code-analyzer-eslint-engine": "0.40.2", "@salesforce/code-analyzer-flow-engine": "0.34.0", From cb4027bf0dd4d00af064ebd17ead7bc7a4b92d88 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Wed, 18 Mar 2026 15:52:55 +0530 Subject: [PATCH 3/3] add test configs --- .../valid-configs/config-with-suppressions-disabled.yml | 7 +++++++ .../valid-configs/config-with-suppressions-enabled.yml | 7 +++++++ .../valid-configs/config-without-suppressions-field.yml | 5 +++++ 3 files changed, 19 insertions(+) create mode 100644 test/fixtures/valid-configs/config-with-suppressions-disabled.yml create mode 100644 test/fixtures/valid-configs/config-with-suppressions-enabled.yml create mode 100644 test/fixtures/valid-configs/config-without-suppressions-field.yml diff --git a/test/fixtures/valid-configs/config-with-suppressions-disabled.yml b/test/fixtures/valid-configs/config-with-suppressions-disabled.yml new file mode 100644 index 000000000..7199dca1c --- /dev/null +++ b/test/fixtures/valid-configs/config-with-suppressions-disabled.yml @@ -0,0 +1,7 @@ +suppressions: + disable_suppressions: true + +rules: + stubEngine1: + stub1RuleB: + severity: 3 diff --git a/test/fixtures/valid-configs/config-with-suppressions-enabled.yml b/test/fixtures/valid-configs/config-with-suppressions-enabled.yml new file mode 100644 index 000000000..d20ac7641 --- /dev/null +++ b/test/fixtures/valid-configs/config-with-suppressions-enabled.yml @@ -0,0 +1,7 @@ +suppressions: + disable_suppressions: false + +rules: + stubEngine1: + stub1RuleA: + severity: 2 diff --git a/test/fixtures/valid-configs/config-without-suppressions-field.yml b/test/fixtures/valid-configs/config-without-suppressions-field.yml new file mode 100644 index 000000000..72d569c82 --- /dev/null +++ b/test/fixtures/valid-configs/config-without-suppressions-field.yml @@ -0,0 +1,5 @@ +rules: + stubEngine1: + stub1RuleC: + severity: 4 + tags: ["TestTag"]