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
37 changes: 13 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 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.43.0",
"version": "0.44.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand All @@ -19,6 +19,7 @@
"@salesforce/code-analyzer-engine-api": "0.35.0",
"@types/node": "^20.0.0",
"csv-stringify": "^6.6.0",
"isbinaryfile": "^5.0.4",
"js-yaml": "^4.1.1",
"semver": "^7.7.4",
"xmlbuilder": "^15.1.1"
Expand Down
53 changes: 52 additions & 1 deletion packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
RunResults,
RunResultsImpl,
UnexpectedErrorEngineRunResults,
UninstantiableEngineRunResults
UninstantiableEngineRunResults,
Violation
} from "./results"
import {processSuppressions} from "./suppressions"
import {SemVer} from 'semver';
import {
EngineLogEvent,
Expand Down Expand Up @@ -384,9 +386,58 @@ export class CodeAnalyzer {
for (const [uninstantiableEngine, error] of this.uninstantiableEnginesMap.entries()) {
runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error));
}

// Process inline suppressions (post-processing step)
// This filters out violations that have been suppressed via inline markers
await this.applyInlineSuppressions(runResults);

return runResults;
}

/**
* Applies suppression filtering to the run results
* This processes suppression markers in source files and filters out suppressed violations
* @param runResults The run results to apply suppressions to
*/
private async applyInlineSuppressions(runResults: RunResultsImpl): Promise<void> {
// Check if suppressions are enabled
if (!this.config.getSuppressionsEnabled()) {
return; // Feature disabled, skip processing
}

const allViolations = runResults.getViolations();

if (allViolations.length === 0) {
return; // No violations to process
}

this.emitLogEvent(LogLevel.Debug, getMessage('ProcessingInlineSuppressions', allViolations.length));

// Process suppressions (returns filtered violations)
const logger = (level: 'error' | 'warn' | 'debug', message: string) => {
const logLevel = level === 'error' ? LogLevel.Error : level === 'warn' ? LogLevel.Warn : LogLevel.Debug;
this.emitLogEvent(logLevel, message);
};
const filteredViolations = await processSuppressions(allViolations, logger);

// Calculate which violations were suppressed
const suppressedViolations = new Set<Violation>();
const filteredSet = new Set(filteredViolations);
for (const violation of allViolations) {
if (!filteredSet.has(violation)) {
suppressedViolations.add(violation);
}
}

const suppressedCount = suppressedViolations.size;
if (suppressedCount > 0) {
this.emitLogEvent(LogLevel.Info, getMessage('SuppressedViolationsCount', suppressedCount));
runResults.applySuppressedViolationsFilter(suppressedViolations);
} else {
this.emitLogEvent(LogLevel.Info, getMessage('NoViolationsSuppressed'));
}
}

/**
* Attach a listener callback to one of the events that Code Analyzer may emit
* Example usage:
Expand Down
30 changes: 29 additions & 1 deletion packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const FIELDS = {
ROOT_WORKING_FOLDER: 'root_working_folder', // Hidden
CUSTOM_ENGINE_PLUGIN_MODULES: 'custom_engine_plugin_modules', // Hidden
PRESERVE_ALL_WORKING_FOLDERS: 'preserve_all_working_folders', // Hidden
SUPPRESSIONS: 'suppressions',
DISABLE_SUPPRESSIONS: 'disable_suppressions',
RULES: 'rules',
ENGINES: 'engines',
SEVERITY: 'severity',
Expand Down Expand Up @@ -48,13 +50,21 @@ export type Ignores = {
files: string[]
}

/**
* Object containing the user specified suppressions configuration
*/
export type Suppressions = {
disable_suppressions: boolean
}

type TopLevelConfig = {
config_root: string
log_folder: string
log_level: LogLevel
rules: Record<string, RuleOverrides>
engines: Record<string, EngineOverrides>
ignores: Ignores
suppressions: Suppressions
root_working_folder: string, // INTERNAL USE ONLY
preserve_all_working_folders: boolean // INTERNAL USE ONLY
custom_engine_plugin_modules: string[] // INTERNAL USE ONLY
Expand All @@ -65,6 +75,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
config_root: process.cwd(),
log_folder: os.tmpdir(),
log_level: LogLevel.Debug,
suppressions: { disable_suppressions: false }, // Suppressions enabled by default
rules: {},
engines: {},
ignores: { files: [] },
Expand Down Expand Up @@ -156,11 +167,12 @@ export class CodeAnalyzerConfig {
validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT);
const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot);
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS, FIELDS.ROOT_WORKING_FOLDER]); // Hidden fields bypass validation
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES]);
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES, FIELDS.SUPPRESSIONS]);
const config: TopLevelConfig = {
config_root: configRoot,
log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!,
log_level: extractLogLevel(configExtractor),
suppressions: extractSuppressionsValue(configExtractor),
custom_engine_plugin_modules: configExtractor.extractArray(FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES,
engApi.ValueValidator.validateString,
DEFAULT_CONFIG.custom_engine_plugin_modules)!,
Expand Down Expand Up @@ -239,6 +251,15 @@ export class CodeAnalyzerConfig {
return this.config.log_level;
}

/**
* Returns whether suppression markers should be processed.
* When enabled, code-analyzer-suppress/unsuppress markers in source files will filter out violations.
* Returns true by default unless explicitly disabled via suppressions.disable_suppressions config.
*/
public getSuppressionsEnabled(): boolean {
return !this.config.suppressions.disable_suppressions;
}

/**
* Returns the absolute path folder where all path based values within the configuration may be relative to.
* Typically, this is set as the folder where a configuration file was loaded from, but doesn't have to be.
Expand Down Expand Up @@ -358,6 +379,13 @@ function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Igno
return { files };
}

function extractSuppressionsValue(configExtractor: engApi.ConfigValueExtractor): Suppressions {
const suppressionsExtractor: engApi.ConfigValueExtractor = configExtractor.extractObjectAsExtractor(FIELDS.SUPPRESSIONS, DEFAULT_CONFIG.suppressions);
suppressionsExtractor.validateContainsOnlySpecifiedKeys([FIELDS.DISABLE_SUPPRESSIONS]);
const disable_suppressions: boolean = suppressionsExtractor.extractBoolean(FIELDS.DISABLE_SUPPRESSIONS, DEFAULT_CONFIG.suppressions.disable_suppressions) || false;
return { disable_suppressions };
}

/**
* Validates that a value is a string and is a valid glob pattern.
* Throws an error if the pattern is empty or has unbalanced brackets/braces/parentheses.
Expand Down
11 changes: 10 additions & 1 deletion packages/code-analyzer-core/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,16 @@ const MESSAGE_CATALOG : MessageCatalog = {
`Since preserve_all_working_folders config setting is true, all temporary working folders in %s have been kept.`,

EngineWorkingFolderKeptDueToError:
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`,

ProcessingInlineSuppressions:
`Processing inline suppressions for %d violation(s).`,

SuppressedViolationsCount:
`%d violation(s) were suppressed by inline suppression markers.`,

NoViolationsSuppressed:
`No violations were suppressed by inline suppression markers.`
}

/**
Expand Down
60 changes: 58 additions & 2 deletions packages/code-analyzer-core/src/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export class EngineRunResultsImpl implements EngineRunResults {
private readonly engineVersion: string;
private readonly apiEngineRunResults: engApi.EngineRunResults;
private readonly ruleSelection: RuleSelection;
private cachedViolations: Violation[] | undefined;

constructor(engineName: string, engineVersion: string, apiEngineRunResults: engApi.EngineRunResults, ruleSelection: RuleSelection) {
this.engineName = engineName;
Expand All @@ -301,8 +302,13 @@ export class EngineRunResultsImpl implements EngineRunResults {
}

getViolations(): Violation[] {
return this.apiEngineRunResults.violations.map(v =>
new ViolationImpl(v, this.ruleSelection.getRule(this.engineName, v.ruleName)));
// Cache violations to ensure the same objects are returned on multiple calls
// This is critical for Set-based filtering (e.g., inline suppressions)
if (!this.cachedViolations) {
this.cachedViolations = this.apiEngineRunResults.violations.map(v =>
new ViolationImpl(v, this.ruleSelection.getRule(this.engineName, v.ruleName)));
}
return this.cachedViolations;
}
}

Expand Down Expand Up @@ -350,6 +356,39 @@ export class UnexpectedErrorEngineRunResults extends AbstractErroneousEngineRunR
}
}

/**
* Wrapper class that filters violations from an existing EngineRunResults
*/
class FilteredEngineRunResults implements EngineRunResults {
private readonly originalResults: EngineRunResults;
private readonly filteredViolations: Violation[];

constructor(originalResults: EngineRunResults, filteredViolations: Violation[]) {
this.originalResults = originalResults;
this.filteredViolations = filteredViolations;
}

getEngineName(): string {
return this.originalResults.getEngineName();
}

getEngineVersion(): string {
return this.originalResults.getEngineVersion();
}

getViolationCount(): number {
return this.filteredViolations.length;
}

getViolationCountOfSeverity(severity: SeverityLevel): number {
return this.filteredViolations.filter(v => v.getRule().getSeverityLevel() == severity).length;
}

getViolations(): Violation[] {
return this.filteredViolations;
}
}

export class RunResultsImpl implements RunResults {
private readonly clock: Clock;
private readonly runDir: string;
Expand Down Expand Up @@ -415,4 +454,21 @@ export class RunResultsImpl implements RunResults {
addEngineRunResults(engineRunResults: EngineRunResults): void {
this.engineRunResultsMap.set(engineRunResults.getEngineName(), engineRunResults);
}

/**
* Applies suppression filtering to all violations in this RunResults
* This method filters out violations that have been suppressed via inline markers
* @param suppressedViolations Set of violations to suppress
*/
applySuppressedViolationsFilter(suppressedViolations: Set<Violation>): void {
// For each engine, filter its violations
for (const [engineName, originalResults] of this.engineRunResultsMap.entries()) {
const originalViolations = originalResults.getViolations();
const filteredViolations = originalViolations.filter(v => !suppressedViolations.has(v));

// Replace with filtered results
const filteredResults = new FilteredEngineRunResults(originalResults, filteredViolations);
this.engineRunResultsMap.set(engineName, filteredResults);
}
}
}
24 changes: 24 additions & 0 deletions packages/code-analyzer-core/src/suppressions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Public API for the suppressions module
*/

export type {
SuppressionMarker,
SuppressionRange,
FileSuppressions,
SuppressionsMap
} from './suppression-types';

export {
parseSuppressionMarkers,
buildSuppressionRanges,
parseFileSuppressions
} from './suppression-parser';

export {
processSuppressions,
isTextFile,
extractSuppressionsFromFiles
} from './suppression-processor';

export type { LoggerCallback } from './suppression-processor';
Loading
Loading