Skip to content

Commit 36c43a9

Browse files
Merge branch 'feature/fixes-suggestion-data-modelling' into feature/eslint-fixes-suggestions
2 parents d1552f4 + 737edfb commit 36c43a9

29 files changed

Lines changed: 2684 additions & 36 deletions

package-lock.json

Lines changed: 13 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/code-analyzer-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@salesforce/code-analyzer-engine-api": "0.36.0-SNAPSHOT",
2020
"@types/node": "^20.0.0",
2121
"csv-stringify": "^6.6.0",
22+
"isbinaryfile": "^5.0.4",
2223
"js-yaml": "^4.1.1",
2324
"semver": "^7.7.4",
2425
"xmlbuilder": "^15.1.1"

packages/code-analyzer-core/src/code-analyzer.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
RunResults,
77
RunResultsImpl,
88
UnexpectedErrorEngineRunResults,
9-
UninstantiableEngineRunResults
9+
UninstantiableEngineRunResults,
10+
Violation
1011
} from "./results"
12+
import {processSuppressions} from "./suppressions"
1113
import {SemVer} from 'semver';
1214
import {
1315
EngineLogEvent,
@@ -392,9 +394,58 @@ export class CodeAnalyzer {
392394
for (const [uninstantiableEngine, error] of this.uninstantiableEnginesMap.entries()) {
393395
runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error));
394396
}
397+
398+
// Process inline suppressions (post-processing step)
399+
// This filters out violations that have been suppressed via inline markers
400+
await this.applyInlineSuppressions(runResults);
401+
395402
return runResults;
396403
}
397404

405+
/**
406+
* Applies suppression filtering to the run results
407+
* This processes suppression markers in source files and filters out suppressed violations
408+
* @param runResults The run results to apply suppressions to
409+
*/
410+
private async applyInlineSuppressions(runResults: RunResultsImpl): Promise<void> {
411+
// Check if suppressions are enabled
412+
if (!this.config.getSuppressionsEnabled()) {
413+
return; // Feature disabled, skip processing
414+
}
415+
416+
const allViolations = runResults.getViolations();
417+
418+
if (allViolations.length === 0) {
419+
return; // No violations to process
420+
}
421+
422+
this.emitLogEvent(LogLevel.Debug, getMessage('ProcessingInlineSuppressions', allViolations.length));
423+
424+
// Process suppressions (returns filtered violations)
425+
const logger = (level: 'error' | 'warn' | 'debug', message: string) => {
426+
const logLevel = level === 'error' ? LogLevel.Error : level === 'warn' ? LogLevel.Warn : LogLevel.Debug;
427+
this.emitLogEvent(logLevel, message);
428+
};
429+
const filteredViolations = await processSuppressions(allViolations, logger);
430+
431+
// Calculate which violations were suppressed
432+
const suppressedViolations = new Set<Violation>();
433+
const filteredSet = new Set(filteredViolations);
434+
for (const violation of allViolations) {
435+
if (!filteredSet.has(violation)) {
436+
suppressedViolations.add(violation);
437+
}
438+
}
439+
440+
const suppressedCount = suppressedViolations.size;
441+
if (suppressedCount > 0) {
442+
this.emitLogEvent(LogLevel.Info, getMessage('SuppressedViolationsCount', suppressedCount));
443+
runResults.applySuppressedViolationsFilter(suppressedViolations);
444+
} else {
445+
this.emitLogEvent(LogLevel.Info, getMessage('NoViolationsSuppressed'));
446+
}
447+
}
448+
398449
/**
399450
* Attach a listener callback to one of the events that Code Analyzer may emit
400451
* Example usage:

packages/code-analyzer-core/src/config.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export const FIELDS = {
1616
ROOT_WORKING_FOLDER: 'root_working_folder', // Hidden
1717
CUSTOM_ENGINE_PLUGIN_MODULES: 'custom_engine_plugin_modules', // Hidden
1818
PRESERVE_ALL_WORKING_FOLDERS: 'preserve_all_working_folders', // Hidden
19+
SUPPRESSIONS: 'suppressions',
20+
DISABLE_SUPPRESSIONS: 'disable_suppressions',
1921
RULES: 'rules',
2022
ENGINES: 'engines',
2123
SEVERITY: 'severity',
@@ -48,13 +50,21 @@ export type Ignores = {
4850
files: string[]
4951
}
5052

53+
/**
54+
* Object containing the user specified suppressions configuration
55+
*/
56+
export type Suppressions = {
57+
disable_suppressions: boolean
58+
}
59+
5160
type TopLevelConfig = {
5261
config_root: string
5362
log_folder: string
5463
log_level: LogLevel
5564
rules: Record<string, RuleOverrides>
5665
engines: Record<string, EngineOverrides>
5766
ignores: Ignores
67+
suppressions: Suppressions
5868
root_working_folder: string, // INTERNAL USE ONLY
5969
preserve_all_working_folders: boolean // INTERNAL USE ONLY
6070
custom_engine_plugin_modules: string[] // INTERNAL USE ONLY
@@ -65,6 +75,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
6575
config_root: process.cwd(),
6676
log_folder: os.tmpdir(),
6777
log_level: LogLevel.Debug,
78+
suppressions: { disable_suppressions: false }, // Suppressions enabled by default
6879
rules: {},
6980
engines: {},
7081
ignores: { files: [] },
@@ -156,11 +167,12 @@ export class CodeAnalyzerConfig {
156167
validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT);
157168
const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot);
158169
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS, FIELDS.ROOT_WORKING_FOLDER]); // Hidden fields bypass validation
159-
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES]);
170+
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES, FIELDS.SUPPRESSIONS]);
160171
const config: TopLevelConfig = {
161172
config_root: configRoot,
162173
log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!,
163174
log_level: extractLogLevel(configExtractor),
175+
suppressions: extractSuppressionsValue(configExtractor),
164176
custom_engine_plugin_modules: configExtractor.extractArray(FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES,
165177
engApi.ValueValidator.validateString,
166178
DEFAULT_CONFIG.custom_engine_plugin_modules)!,
@@ -239,6 +251,15 @@ export class CodeAnalyzerConfig {
239251
return this.config.log_level;
240252
}
241253

254+
/**
255+
* Returns whether suppression markers should be processed.
256+
* When enabled, code-analyzer-suppress/unsuppress markers in source files will filter out violations.
257+
* Returns true by default unless explicitly disabled via suppressions.disable_suppressions config.
258+
*/
259+
public getSuppressionsEnabled(): boolean {
260+
return !this.config.suppressions.disable_suppressions;
261+
}
262+
242263
/**
243264
* Returns the absolute path folder where all path based values within the configuration may be relative to.
244265
* Typically, this is set as the folder where a configuration file was loaded from, but doesn't have to be.
@@ -358,6 +379,13 @@ function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Igno
358379
return { files };
359380
}
360381

382+
function extractSuppressionsValue(configExtractor: engApi.ConfigValueExtractor): Suppressions {
383+
const suppressionsExtractor: engApi.ConfigValueExtractor = configExtractor.extractObjectAsExtractor(FIELDS.SUPPRESSIONS, DEFAULT_CONFIG.suppressions);
384+
suppressionsExtractor.validateContainsOnlySpecifiedKeys([FIELDS.DISABLE_SUPPRESSIONS]);
385+
const disable_suppressions: boolean = suppressionsExtractor.extractBoolean(FIELDS.DISABLE_SUPPRESSIONS, DEFAULT_CONFIG.suppressions.disable_suppressions) || false;
386+
return { disable_suppressions };
387+
}
388+
361389
/**
362390
* Validates that a value is a string and is a valid glob pattern.
363391
* Throws an error if the pattern is empty or has unbalanced brackets/braces/parentheses.

packages/code-analyzer-core/src/messages.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,16 @@ const MESSAGE_CATALOG : MessageCatalog = {
229229
`Since preserve_all_working_folders config setting is true, all temporary working folders in %s have been kept.`,
230230

231231
EngineWorkingFolderKeptDueToError:
232-
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`
232+
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`,
233+
234+
ProcessingInlineSuppressions:
235+
`Processing inline suppressions for %d violation(s).`,
236+
237+
SuppressedViolationsCount:
238+
`%d violation(s) were suppressed by inline suppression markers.`,
239+
240+
NoViolationsSuppressed:
241+
`No violations were suppressed by inline suppression markers.`
233242
}
234243

235244
/**

packages/code-analyzer-core/src/results.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export class EngineRunResultsImpl implements EngineRunResults {
352352
private readonly engineVersion: string;
353353
private readonly apiEngineRunResults: engApi.EngineRunResults;
354354
private readonly ruleSelection: RuleSelection;
355+
private cachedViolations: Violation[] | undefined;
355356

356357
constructor(engineName: string, engineVersion: string, apiEngineRunResults: engApi.EngineRunResults, ruleSelection: RuleSelection) {
357358
this.engineName = engineName;
@@ -377,8 +378,13 @@ export class EngineRunResultsImpl implements EngineRunResults {
377378
}
378379

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

@@ -426,6 +432,39 @@ export class UnexpectedErrorEngineRunResults extends AbstractErroneousEngineRunR
426432
}
427433
}
428434

435+
/**
436+
* Wrapper class that filters violations from an existing EngineRunResults
437+
*/
438+
class FilteredEngineRunResults implements EngineRunResults {
439+
private readonly originalResults: EngineRunResults;
440+
private readonly filteredViolations: Violation[];
441+
442+
constructor(originalResults: EngineRunResults, filteredViolations: Violation[]) {
443+
this.originalResults = originalResults;
444+
this.filteredViolations = filteredViolations;
445+
}
446+
447+
getEngineName(): string {
448+
return this.originalResults.getEngineName();
449+
}
450+
451+
getEngineVersion(): string {
452+
return this.originalResults.getEngineVersion();
453+
}
454+
455+
getViolationCount(): number {
456+
return this.filteredViolations.length;
457+
}
458+
459+
getViolationCountOfSeverity(severity: SeverityLevel): number {
460+
return this.filteredViolations.filter(v => v.getRule().getSeverityLevel() == severity).length;
461+
}
462+
463+
getViolations(): Violation[] {
464+
return this.filteredViolations;
465+
}
466+
}
467+
429468
export class RunResultsImpl implements RunResults {
430469
private readonly clock: Clock;
431470
private readonly runDir: string;
@@ -491,4 +530,21 @@ export class RunResultsImpl implements RunResults {
491530
addEngineRunResults(engineRunResults: EngineRunResults): void {
492531
this.engineRunResultsMap.set(engineRunResults.getEngineName(), engineRunResults);
493532
}
533+
534+
/**
535+
* Applies suppression filtering to all violations in this RunResults
536+
* This method filters out violations that have been suppressed via inline markers
537+
* @param suppressedViolations Set of violations to suppress
538+
*/
539+
applySuppressedViolationsFilter(suppressedViolations: Set<Violation>): void {
540+
// For each engine, filter its violations
541+
for (const [engineName, originalResults] of this.engineRunResultsMap.entries()) {
542+
const originalViolations = originalResults.getViolations();
543+
const filteredViolations = originalViolations.filter(v => !suppressedViolations.has(v));
544+
545+
// Replace with filtered results
546+
const filteredResults = new FilteredEngineRunResults(originalResults, filteredViolations);
547+
this.engineRunResultsMap.set(engineName, filteredResults);
548+
}
549+
}
494550
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Public API for the suppressions module
3+
*/
4+
5+
export type {
6+
SuppressionMarker,
7+
SuppressionRange,
8+
FileSuppressions,
9+
SuppressionsMap
10+
} from './suppression-types';
11+
12+
export {
13+
parseSuppressionMarkers,
14+
buildSuppressionRanges,
15+
parseFileSuppressions
16+
} from './suppression-parser';
17+
18+
export {
19+
processSuppressions,
20+
isTextFile,
21+
extractSuppressionsFromFiles
22+
} from './suppression-processor';
23+
24+
export type { LoggerCallback } from './suppression-processor';

0 commit comments

Comments
 (0)