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
14 changes: 14 additions & 0 deletions messages/run-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion src/commands/code-analyzer/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export default class RunCommand extends SfCommand<void> 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
})
};

Expand All @@ -84,7 +90,8 @@ export default class RunCommand extends SfCommand<void> 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);
}
Expand Down
10 changes: 8 additions & 2 deletions src/lib/actions/RunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type RunInput = {
'severity-threshold'?: SeverityLevel;
target?: string[];
workspace: string[];

'no-suppressions'?: boolean;
}

export class RunAction {
Expand All @@ -51,7 +51,13 @@ export class RunAction {
}

public async execute(input: RunInput): Promise<void> {
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.
Expand Down
91 changes: 88 additions & 3 deletions src/lib/factories/CodeAnalyzerConfigFactory.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string, unknown> | undefined = this.readRawYamlFile(configFilePath);
const suppressionsExplicitlySet = (rawYaml?.suppressions as Record<string, unknown> | 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<string, unknown> = {
...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<string, unknown> | undefined {
try {
const fileContents = fs.readFileSync(filePath, 'utf8');
return yaml.load(fileContents) as Record<string, unknown>;
} catch (_err) {
// If file can't be read or parsed, return undefined
// The config loading will handle the error
return undefined;
}
}
}
66 changes: 66 additions & 0 deletions test/commands/code-analyzer/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
suppressions:
disable_suppressions: true

rules:
stubEngine1:
stub1RuleB:
severity: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
suppressions:
disable_suppressions: false

rules:
stubEngine1:
stub1RuleA:
severity: 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
rules:
stubEngine1:
stub1RuleC:
severity: 4
tags: ["TestTag"]
Loading
Loading