Skip to content

Commit c5a8e19

Browse files
authored
NEW @W-21705354@ Adding changes to use rule selectors in inline suppression markers (#447)
1 parent 9f22899 commit c5a8e19

13 files changed

Lines changed: 994 additions & 337 deletions

packages/code-analyzer-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-core",
33
"description": "Core Package for the Salesforce Code Analyzer",
4-
"version": "0.45.0",
4+
"version": "0.46.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",

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

Lines changed: 136 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
UninstantiableEngineRunResults,
1010
Violation
1111
} from "./results"
12-
import {processSuppressions} from "./suppressions"
12+
import {processSuppressions, extractSuppressionsFromFiles, SuppressionsMap, LoggerCallback} from "./suppressions"
1313
import {SemVer} from 'semver';
1414
import {
1515
EngineLogEvent,
@@ -120,6 +120,11 @@ export class CodeAnalyzer {
120120
private readonly engineConfigDescriptions: Map<string, ConfigDescription> = new Map();
121121
private readonly rulesCache: Map<string, RuleImpl[]> = new Map();
122122
private readonly engineRuleDiscoveryProgressAggregator: EngineProgressAggregator = new EngineProgressAggregator();
123+
// Caching for per-engine suppression processing to avoid duplicate file processing
124+
private readonly suppressionsMap: SuppressionsMap = new Map();
125+
private readonly fileProcessingPromises: Map<string, Promise<void>> = new Map();
126+
// Track total suppressed violations for aggregate logging
127+
private totalSuppressedViolations: number = 0;
123128

124129
constructor(config: CodeAnalyzerConfig, fileSystem: FileSystem = new RealFileSystem(), nodeVersion: string = process.version) {
125130
this.validateEnvironment(nodeVersion);
@@ -348,6 +353,15 @@ export class CodeAnalyzer {
348353
// up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may
349354
// override each other.
350355

356+
// Reset suppression counter for this run
357+
this.totalSuppressedViolations = 0;
358+
359+
// Clear suppression caches from previous runs to prevent unbounded memory growth
360+
// Each run typically analyzes a different workspace, so caching across runs provides minimal benefit
361+
// while keeping stale data in memory.
362+
this.suppressionsMap.clear();
363+
this.fileProcessingPromises.clear();
364+
351365
this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({
352366
filesAndFolders: runOptions.workspace.getRawFilesAndFolders(),
353367
targets: runOptions.workspace.getRawTargets()
@@ -395,55 +409,143 @@ export class CodeAnalyzer {
395409
runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error));
396410
}
397411

398-
// Process inline suppressions (post-processing step)
399-
// This filters out violations that have been suppressed via inline markers
400-
await this.applyInlineSuppressions(runResults);
412+
// Note: Inline suppressions are now applied per-engine in runEngineAndValidateResults() before EngineResultsEvent is emitted
413+
414+
// Log aggregate suppression count if any violations were suppressed
415+
if (this.config.getSuppressionsEnabled() && this.totalSuppressedViolations > 0) {
416+
this.emitLogEvent(LogLevel.Info, getMessage('SuppressedViolationsCount', this.totalSuppressedViolations));
417+
}
401418

402419
return runResults;
403420
}
404421

405422
/**
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
423+
* Applies suppression filtering to a single engine's results
424+
* This processes suppression markers in source files and returns a filtered version of the engine results
425+
* This method handles race conditions by caching suppression ranges per file
426+
* @param engineRunResults The engine run results to apply suppressions to
427+
* @returns Filtered engine run results with suppressions applied
409428
*/
410-
private async applyInlineSuppressions(runResults: RunResultsImpl): Promise<void> {
429+
private async applyInlineSuppressionsToEngineResults(
430+
engineRunResults: EngineRunResults
431+
): Promise<EngineRunResults> {
411432
// Check if suppressions are enabled
412433
if (!this.config.getSuppressionsEnabled()) {
413-
return; // Feature disabled, skip processing
434+
return engineRunResults; // Feature disabled, return original results
414435
}
415436

416-
const allViolations = runResults.getViolations();
437+
const violations = engineRunResults.getViolations();
438+
439+
if (violations.length === 0) {
440+
return engineRunResults; // No violations to process
441+
}
442+
443+
// Extract unique file paths from violations for race condition handling
444+
const filePaths = new Set<string>();
445+
for (const violation of violations) {
446+
const primaryLocation = violation.getPrimaryLocation();
447+
const file = primaryLocation.getFile();
448+
if (file) {
449+
filePaths.add(file);
450+
}
451+
}
417452

418-
if (allViolations.length === 0) {
419-
return; // No violations to process
453+
if (filePaths.size === 0) {
454+
return engineRunResults; // No files with violations
420455
}
421456

422-
this.emitLogEvent(LogLevel.Debug, getMessage('ProcessingInlineSuppressions', allViolations.length));
457+
// Process files with race condition handling to pre-populate the shared map
458+
await this.processFilesForSuppressions(filePaths);
423459

424-
// Process suppressions (returns filtered violations)
425-
const logger = (level: 'error' | 'warn' | 'debug', message: string) => {
460+
// Use processSuppressions with the pre-populated shared map
461+
// This will skip re-parsing files already in the map and just filter violations
462+
const logger: LoggerCallback = (level: 'error' | 'warn' | 'debug', message: string) => {
426463
const logLevel = level === 'error' ? LogLevel.Error : level === 'warn' ? LogLevel.Warn : LogLevel.Debug;
427464
this.emitLogEvent(logLevel, message);
428465
};
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-
}
466+
const filteredViolations = await processSuppressions(violations, logger, this.suppressionsMap);
467+
468+
// Calculate how many violations were suppressed
469+
const suppressedCount = violations.length - filteredViolations.length;
470+
471+
// If all violations remain (nothing suppressed), return original results
472+
if (suppressedCount === 0) {
473+
return engineRunResults;
438474
}
439475

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'));
476+
// Track suppressed violations for aggregate logging
477+
this.totalSuppressedViolations += suppressedCount;
478+
479+
// Return filtered results using FilteredEngineRunResults wrapper
480+
return this.createFilteredEngineRunResults(engineRunResults, filteredViolations);
481+
}
482+
483+
/**
484+
* Creates a FilteredEngineRunResults wrapper
485+
* This is a temporary method until FilteredEngineRunResults is exported from results.ts
486+
*/
487+
private createFilteredEngineRunResults(
488+
originalResults: EngineRunResults,
489+
filteredViolations: Violation[]
490+
): EngineRunResults {
491+
// We need to create an instance that implements EngineRunResults
492+
// but filters the violations
493+
return {
494+
getEngineName: () => originalResults.getEngineName(),
495+
getEngineVersion: () => originalResults.getEngineVersion(),
496+
getViolationCount: () => filteredViolations.length,
497+
getViolationCountOfSeverity: (severity: number) =>
498+
filteredViolations.filter(v => v.getRule().getSeverityLevel() === severity).length,
499+
getViolations: () => filteredViolations
500+
};
501+
}
502+
503+
/**
504+
* Processes files for suppression markers with race condition handling
505+
* Uses caching to avoid processing the same file multiple times when multiple engines
506+
* return violations for the same file
507+
* @param filePaths Set of file paths that need suppression information
508+
*/
509+
private async processFilesForSuppressions(filePaths: Set<string>): Promise<void> {
510+
const logger: LoggerCallback = (level: 'error' | 'warn' | 'debug', message: string) => {
511+
const logLevel = level === 'error' ? LogLevel.Error : level === 'warn' ? LogLevel.Warn : LogLevel.Debug;
512+
this.emitLogEvent(logLevel, message);
513+
};
514+
515+
const processingPromises: Promise<void>[] = [];
516+
517+
for (const filePath of filePaths) {
518+
// If already in cache, skip
519+
if (this.suppressionsMap.has(filePath)) {
520+
continue;
521+
}
522+
523+
// If currently being processed, await that promise
524+
let processingPromise = this.fileProcessingPromises.get(filePath);
525+
526+
if (!processingPromise) {
527+
// Start new processing - wrap extractSuppressionsFromFiles call
528+
processingPromise = extractSuppressionsFromFiles(
529+
new Set([filePath]),
530+
this.suppressionsMap,
531+
logger
532+
).then(() => {
533+
// Clean up the promise from tracking map since it's done
534+
this.fileProcessingPromises.delete(filePath);
535+
}).catch((err) => {
536+
// Clean up on error too
537+
this.fileProcessingPromises.delete(filePath);
538+
throw err;
539+
});
540+
541+
this.fileProcessingPromises.set(filePath, processingPromise);
542+
}
543+
544+
processingPromises.push(processingPromise);
446545
}
546+
547+
// Wait for all file processing to complete
548+
await Promise.all(processingPromises);
447549
}
448550

449551
/**
@@ -565,7 +667,10 @@ export class CodeAnalyzer {
565667
}
566668

567669
validateEngineRunResults(engineName, apiEngineRunResults, ruleSelection);
568-
const engineRunResults: EngineRunResults = new EngineRunResultsImpl(engineName, await engine.getEngineVersion(), apiEngineRunResults, ruleSelection);
670+
let engineRunResults: EngineRunResults = new EngineRunResultsImpl(engineName, await engine.getEngineVersion(), apiEngineRunResults, ruleSelection);
671+
672+
// Apply inline suppressions per-engine BEFORE emitting EngineResultsEvent
673+
engineRunResults = await this.applyInlineSuppressionsToEngineResults(engineRunResults);
569674

570675
this.emitEvent<EngineRunProgressEvent>({
571676
type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 100

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ export class CodeAnalyzerConfig {
227227
valueType: 'object',
228228
defaultValue: { files: [] },
229229
wasSuppliedByUser: !deepEquals(this.config.ignores, DEFAULT_CONFIG.ignores)
230+
},
231+
suppressions: {
232+
descriptionText: getMessage('ConfigFieldDescription_suppressions'),
233+
valueType: 'object',
234+
defaultValue: { disable_suppressions: false },
235+
wasSuppliedByUser: !deepEquals(this.config.suppressions, DEFAULT_CONFIG.suppressions)
230236
}
231237
}
232238
};
@@ -260,6 +266,13 @@ export class CodeAnalyzerConfig {
260266
return !this.config.suppressions.disable_suppressions;
261267
}
262268

269+
/**
270+
* Returns the suppressions configuration object.
271+
*/
272+
public getSuppressions(): Suppressions {
273+
return this.config.suppressions;
274+
}
275+
263276
/**
264277
* Returns the absolute path folder where all path based values within the configuration may be relative to.
265278
* Typically, this is set as the folder where a configuration file was loaded from, but doesn't have to be.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export type {
88
EngineOverrides,
99
Ignores,
1010
RuleOverrides,
11-
RuleOverride
11+
RuleOverride,
12+
Suppressions
1213
} from "./config"
1314

1415

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ const MESSAGE_CATALOG : MessageCatalog = {
6161
` - "**/*.test.js"\n` +
6262
`-------------------------------------------`,
6363

64+
ConfigFieldDescription_suppressions:
65+
`Configuration for inline suppression markers in source code.\n` +
66+
` disable_suppressions: Boolean to disable processing of suppression markers.\n` +
67+
`---- [Example usage]: ---------------------\n` +
68+
`suppressions:\n` +
69+
` disable_suppressions: false\n` +
70+
`-------------------------------------------`,
71+
6472
GenericEngineConfigOverview:
6573
`%s ENGINE CONFIGURATION`,
6674

@@ -231,14 +239,8 @@ const MESSAGE_CATALOG : MessageCatalog = {
231239
EngineWorkingFolderKeptDueToError:
232240
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`,
233241

234-
ProcessingInlineSuppressions:
235-
`Processing inline suppressions for %d violation(s).`,
236-
237242
SuppressedViolationsCount:
238-
`%d violation(s) were suppressed by inline suppression markers.`,
239-
240-
NoViolationsSuppressed:
241-
`No violations were suppressed by inline suppression markers.`
243+
`%d violation(s) were suppressed by inline suppression markers.`
242244
}
243245

244246
/**

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export {
1818
export {
1919
processSuppressions,
2020
isTextFile,
21-
extractSuppressionsFromFiles
21+
extractSuppressionsFromFiles,
22+
filterSuppressedViolations
2223
} from './suppression-processor';
2324

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

0 commit comments

Comments
 (0)