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
104 changes: 92 additions & 12 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Violation
} from "./results"
import {processSuppressions, extractSuppressionsFromFiles, SuppressionsMap, LoggerCallback} from "./suppressions"
import {applyBulkSuppressions, BulkSuppressionQuotas} from "./suppressions/bulk-suppression-processor"
import {SemVer} from 'semver';
import {
EngineLogEvent,
Expand Down Expand Up @@ -48,6 +49,14 @@ export interface Workspace {
*/
getWorkspaceId(): string

/**
* Returns the longest root folder that contains all the workspace paths or null if one does not exist.
* For example, if the workspace was constructed with "/some/folder/subFolder/file1.txt" and
* "/some/folder/file2.txt", then the workspace root folder would be equal to "/some/folder".
* Returns null if a root folder does not exist (e.g., paths from different drives).
*/
getWorkspaceRoot(): string | null

/**
* Returns the unique list of files and folders that were used to construct the workspace.
*/
Expand Down Expand Up @@ -123,8 +132,13 @@ export class CodeAnalyzer {
// Caching for per-engine suppression processing to avoid duplicate file processing
private readonly suppressionsMap: SuppressionsMap = new Map();
private readonly fileProcessingPromises: Map<string, Promise<void>> = new Map();
// Track total suppressed violations for aggregate logging
private totalSuppressedViolations: number = 0;
// Track suppressed violations for aggregate logging (separated by type)
private totalInlineSuppressedViolations: number = 0;
private totalBulkSuppressedViolations: number = 0;
// Bulk suppression quota tracking (shared across files, scoped to config paths)
private readonly bulkSuppressionQuotas: BulkSuppressionQuotas = new Map();
// Current workspace root (set during run, used for bulk suppression path resolution)
private currentWorkspaceRoot: string | null = null;

constructor(config: CodeAnalyzerConfig, fileSystem: FileSystem = new RealFileSystem(), nodeVersion: string = process.version) {
this.validateEnvironment(nodeVersion);
Expand Down Expand Up @@ -353,14 +367,20 @@ export class CodeAnalyzer {
// up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may
// override each other.

// Reset suppression counter for this run
this.totalSuppressedViolations = 0;
// Reset suppression counters for this run
this.totalInlineSuppressedViolations = 0;
this.totalBulkSuppressedViolations = 0;

// Clear suppression caches from previous runs to prevent unbounded memory growth
// Each run typically analyzes a different workspace, so caching across runs provides minimal benefit
// while keeping stale data in memory.
this.suppressionsMap.clear();
this.fileProcessingPromises.clear();
this.bulkSuppressionQuotas.clear();

// Store workspace root for bulk suppression path resolution (consistent with ignores feature)
// Falls back to config root if workspace root is null (e.g., files from different drives)
this.currentWorkspaceRoot = runOptions.workspace.getWorkspaceRoot() || this.config.getConfigRoot();

this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({
filesAndFolders: runOptions.workspace.getRawFilesAndFolders(),
Expand Down Expand Up @@ -408,12 +428,16 @@ export class CodeAnalyzer {
for (const [uninstantiableEngine, error] of this.uninstantiableEnginesMap.entries()) {
runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error));
}

// Note: Inline suppressions are now applied per-engine in runEngineAndValidateResults() before EngineResultsEvent is emitted

// Log aggregate suppression count if any violations were suppressed
if (this.config.getSuppressionsEnabled() && this.totalSuppressedViolations > 0) {
this.emitLogEvent(LogLevel.Info, getMessage('SuppressedViolationsCount', this.totalSuppressedViolations));
if (!this.config.getSuppressionsEnabled()) {
return runResults;
}
// Note: Inline and bulk suppressions are now applied per-engine in runEngineAndValidateResults() before EngineResultsEvent is emitted
// Log aggregate suppression counts if any violations were suppressed (separate messages for inline vs bulk)
if (this.totalInlineSuppressedViolations > 0) {
this.emitLogEvent(LogLevel.Info, getMessage('InlineSuppressedViolationsCount', this.totalInlineSuppressedViolations));
}
if (this.totalBulkSuppressedViolations > 0) {
this.emitLogEvent(LogLevel.Info, getMessage('BulkSuppressedViolationsCount', this.totalBulkSuppressedViolations));
}

return runResults;
Expand Down Expand Up @@ -473,8 +497,8 @@ export class CodeAnalyzer {
return engineRunResults;
}

// Track suppressed violations for aggregate logging
this.totalSuppressedViolations += suppressedCount;
// Track inline suppressed violations for aggregate logging
this.totalInlineSuppressedViolations += suppressedCount;

// Return filtered results using FilteredEngineRunResults wrapper
return this.createFilteredEngineRunResults(engineRunResults, filteredViolations);
Expand All @@ -500,6 +524,55 @@ export class CodeAnalyzer {
};
}

/**
* Applies bulk suppression filtering to a single engine's results
* This processes bulk suppression rules from config and returns a filtered version of the engine results
* @param engineRunResults The engine run results to apply bulk suppressions to
* @returns Filtered engine run results with bulk suppressions applied
*/
private applyBulkSuppressionsToEngineResults(
engineRunResults: EngineRunResults
): EngineRunResults {
// Check if suppressions are enabled
if (!this.config.getSuppressionsEnabled()) {
return engineRunResults;
}

const violations = engineRunResults.getViolations();
if (violations.length === 0) {
return engineRunResults;
}

const bulkConfig = this.config.getBulkSuppressions();
if (Object.keys(bulkConfig).length === 0) {
return engineRunResults; // No bulk suppressions configured
}

// Use workspace root for path resolution (consistent with ignores feature)
// This is set during run() and should never be null at this point
const workspaceRoot = this.currentWorkspaceRoot || this.config.getConfigRoot();

const bulkResult = applyBulkSuppressions(
violations,
bulkConfig,
this.bulkSuppressionQuotas,
workspaceRoot
);

const suppressedCount = bulkResult.suppressedCount;

// If nothing was suppressed, return original results
if (suppressedCount === 0) {
return engineRunResults;
}

// Track bulk suppressed violations for aggregate logging
this.totalBulkSuppressedViolations += suppressedCount;

// Return filtered results
return this.createFilteredEngineRunResults(engineRunResults, bulkResult.unsuppressedViolations);
}

/**
* Processes files for suppression markers with race condition handling
* Uses caching to avoid processing the same file multiple times when multiple engines
Expand Down Expand Up @@ -672,6 +745,9 @@ export class CodeAnalyzer {
// Apply inline suppressions per-engine BEFORE emitting EngineResultsEvent
engineRunResults = await this.applyInlineSuppressionsToEngineResults(engineRunResults);

// Apply bulk suppressions per-engine AFTER inline suppressions, still BEFORE emitting EngineResultsEvent
engineRunResults = this.applyBulkSuppressionsToEngineResults(engineRunResults);

Comment thread
namrata111f marked this conversation as resolved.
this.emitEvent<EngineRunProgressEvent>({
type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 100
});
Expand Down Expand Up @@ -845,6 +921,10 @@ class WorkspaceImpl implements Workspace {
return this.delegate.getWorkspaceId();
}

getWorkspaceRoot(): string | null {
return this.delegate.getWorkspaceRoot();
}

getRawFilesAndFolders(): string[] {
return this.delegate.getRawFilesAndFolders();
}
Expand Down
91 changes: 87 additions & 4 deletions packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ export type Ignores = {
files: string[]
}

/**
* Rule for bulk suppression of violations
*/
export type BulkSuppressionRule = {
rule_selector: string;
max_suppressed_violations?: number | null;
Comment thread
namrata111f marked this conversation as resolved.
reason?: string;
};

/**
* Object containing the user specified suppressions configuration
*/
export type Suppressions = {
disable_suppressions: boolean
disable_suppressions: boolean;
bulk_suppressions?: Record<string, BulkSuppressionRule[]>;
}

type TopLevelConfig = {
Expand All @@ -75,7 +85,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
suppressions: { disable_suppressions: false }, // Suppressions enabled by default, no bulk suppressions by default
rules: {},
engines: {},
ignores: { files: [] },
Expand Down Expand Up @@ -273,6 +283,13 @@ export class CodeAnalyzerConfig {
return this.config.suppressions;
}

/**
* Returns the bulk suppressions configuration (file paths mapped to suppression rules).
*/
public getBulkSuppressions(): Record<string, BulkSuppressionRule[]> {
return this.config.suppressions.bulk_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 @@ -306,6 +323,15 @@ export class CodeAnalyzerConfig {
return this.config.root_working_folder;
}

/**
* Returns the names of engines that have at least one rule override in the configuration.
* Used when writing config output to preserve rule overrides (e.g. disabled rules) for engines
* that may have no selected rules.
*/
public getEngineNamesWithRuleOverrides(): string[] {
return Object.keys(this.config.rules);
}

/**
* Returns a {@link RuleOverrides} instance containing the user specified overrides for all rules associated with the specified engine
* @param engineName name of the engine
Expand Down Expand Up @@ -394,9 +420,66 @@ function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Igno

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 };

// Extract bulk suppressions - all keys except 'disable_suppressions' and 'bulk_suppressions' are file/folder paths
const bulk_suppressions: Record<string, BulkSuppressionRule[]> = {};
const suppressionKeys = suppressionsExtractor.getKeys();

for (const key of suppressionKeys) {
if (key === FIELDS.DISABLE_SUPPRESSIONS || key === 'bulk_suppressions') {
continue; // Skip the disable_suppressions flag and bulk_suppressions placeholder from default config
}

// key is a file/folder path, value should be an array of suppression rules
const rulesArray = suppressionsExtractor.extractArray(
key,
(value, fieldPath) => validateBulkSuppressionRule(value, fieldPath)
);

if (rulesArray && rulesArray.length > 0) {
bulk_suppressions[key] = rulesArray;
}
}

return { disable_suppressions, bulk_suppressions };
}

function validateBulkSuppressionRule(value: unknown, fieldPath: string): BulkSuppressionRule {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(getMessage('InvalidBulkSuppressionRule', fieldPath, 'Expected an object'));
}

const rule = value as Record<string, unknown>;

// rule_selector is required
if (!rule.rule_selector || typeof rule.rule_selector !== 'string') {
throw new Error(getMessage('InvalidBulkSuppressionRule', fieldPath, 'rule_selector is required and must be a string'));
}

// max_suppressed_violations is optional, can be number or null
if (rule.max_suppressed_violations !== undefined &&
rule.max_suppressed_violations !== null &&
typeof rule.max_suppressed_violations !== 'number') {
throw new Error(getMessage('InvalidBulkSuppressionRule', fieldPath, 'max_suppressed_violations must be a number or null'));
}

// max_suppressed_violations must be non-negative if specified
if (typeof rule.max_suppressed_violations === 'number' && rule.max_suppressed_violations < 0) {
throw new Error(getMessage('InvalidBulkSuppressionRule', fieldPath, 'max_suppressed_violations must be a non-negative number'));
}

// reason is optional
if (rule.reason !== undefined && typeof rule.reason !== 'string') {
throw new Error(getMessage('InvalidBulkSuppressionRule', fieldPath, 'reason must be a string'));
}

return {
rule_selector: rule.rule_selector as string,
max_suppressed_violations: rule.max_suppressed_violations as number | null | undefined,
reason: rule.reason as string | undefined
};
}

/**
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 @@ -152,6 +152,9 @@ const MESSAGE_CATALOG : MessageCatalog = {
InvalidGlobPattern:
`The configuration field '%s' contains an invalid glob pattern '%s': %s`,

InvalidBulkSuppressionRule:
`The bulk suppression rule at '%s' is invalid: %s`,

RulePropertyOverridden:
`The %s value of rule '%s' of engine '%s' was overridden according to the specified configuration. The old value '%s' was replaced with the new value '%s'.`,

Expand Down Expand Up @@ -240,7 +243,13 @@ const MESSAGE_CATALOG : MessageCatalog = {
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`,

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

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

BulkSuppressedViolationsCount:
`%d violation(s) were suppressed by bulk suppressions.`
}

/**
Expand Down
Loading
Loading