Skip to content

Commit 4534eaf

Browse files
committed
[eslint-bulk-suppressions] Use suppressedMessages
Defer detection of bulk suppressions until after inline suppressions. Simplify patching.
1 parent a9dc768 commit 4534eaf

File tree

6 files changed

+204
-178
lines changed

6 files changed

+204
-178
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/eslint-patch",
5+
"comment": "Ensure that lint problems suppressed by eslint-bulk-suppressions are available in the `getSuppressedMessages()` function on the linter. Defer evaluation of bulk suppressions until after inline suppressions.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/eslint-patch"
10+
}

eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-file.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import fs from 'fs';
5-
import { VSCODE_PID_ENV_VAR_NAME } from './constants';
5+
import { VSCODE_PID_ENV_VAR_NAME, SUPPRESSIONS_JSON_FILENAME } from './constants';
66

77
export interface ISuppression {
88
file: string;
@@ -23,7 +23,6 @@ export interface IBulkSuppressionsJson {
2323

2424
const IS_RUNNING_IN_VSCODE: boolean = process.env[VSCODE_PID_ENV_VAR_NAME] !== undefined;
2525
const TEN_SECONDS_MS: number = 10 * 1000;
26-
const SUPPRESSIONS_JSON_FILENAME: string = '.eslint-bulk-suppressions.json';
2726

2827
function throwIfAnythingOtherThanNotExistError(e: NodeJS.ErrnoException): void | never {
2928
if (e?.code !== 'ENOENT') {
@@ -56,7 +55,8 @@ export function getSuppressionsConfigForEslintrcFolderPath(
5655
const suppressionsPath: string = `${eslintrcFolderPath}/${SUPPRESSIONS_JSON_FILENAME}`;
5756
let rawJsonFile: string | undefined;
5857
try {
59-
rawJsonFile = fs.readFileSync(suppressionsPath).toString();
58+
// Decoding during read hits an optimized fast path in NodeJS.
59+
rawJsonFile = fs.readFileSync(suppressionsPath, 'utf8');
6060
} catch (e) {
6161
throwIfAnythingOtherThanNotExistError(e);
6262
}

eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts

Lines changed: 155 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import * as Guards from './ast-guards';
88

99
import { eslintFolder } from '../_patch-base';
1010
import {
11-
ESLINT_BULK_ENABLE_ENV_VAR_NAME,
12-
ESLINT_BULK_PRUNE_ENV_VAR_NAME,
13-
ESLINT_BULK_SUPPRESS_ENV_VAR_NAME
11+
ESLINT_BULK_SUPPRESS_ENV_VAR_NAME,
12+
ESLINT_BULK_ESLINTRC_FOLDER_PATH_ENV_VAR_NAME
1413
} from './constants';
1514
import {
1615
getSuppressionsConfigForEslintrcFolderPath,
@@ -27,21 +26,49 @@ const ESLINTRC_FILENAMES: string[] = [
2726
// Several other filenames are allowed, but this patch requires that it be loaded via a JS config file,
2827
// so we only need to check for the JS-based filenames
2928
];
30-
const SUPPRESSION_SYMBOL: unique symbol = Symbol('suppression');
3129
const ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE: string | undefined = process.env[ESLINT_BULK_SUPPRESS_ENV_VAR_NAME];
3230
const SUPPRESS_ALL_RULES: boolean = ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE === '*';
3331
const RULES_TO_SUPPRESS: Set<string> | undefined = ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE
3432
? new Set(ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE.split(','))
3533
: undefined;
3634

35+
interface IBulkSuppression {
36+
suppression: ISuppression;
37+
serializedSuppression: string;
38+
}
39+
3740
interface IProblem {
38-
[SUPPRESSION_SYMBOL]?: {
39-
config: IBulkSuppressionsConfig;
40-
suppression: ISuppression;
41-
serializedSuppression: string;
41+
line: number;
42+
column: number;
43+
ruleId: string;
44+
suppressions?: {
45+
kind: string;
46+
justification: string;
47+
}[];
48+
}
49+
50+
export type VerifyMethod = (
51+
textOrSourceCode: string,
52+
config: unknown,
53+
filename: string
54+
) => IProblem[] | undefined;
55+
56+
export interface ILinterClass {
57+
prototype: {
58+
verify: VerifyMethod;
4259
};
4360
}
4461

62+
const astNodeForProblem: Map<IProblem, TSESTree.Node> = new Map();
63+
64+
export function setAstNodeForProblem(problem: IProblem, node: TSESTree.Node): void {
65+
astNodeForProblem.set(problem, node);
66+
}
67+
68+
interface ILinterInternalSlots {
69+
lastSuppressedMessages: IProblem[] | undefined;
70+
}
71+
4572
function getNodeName(node: TSESTree.Node): string | undefined {
4673
if (Guards.isClassDeclarationWithName(node)) {
4774
return node.id.name;
@@ -91,6 +118,12 @@ function calculateScopeId(node: NodeWithParent | undefined): string {
91118
const eslintrcPathByFileOrFolderPath: Map<string, string> = new Map();
92119

93120
function findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFilePath: string): string {
121+
// Heft, for example, suppresses nested eslintrc files, so it can pass this environment variable to suppress
122+
// searching for the eslintrc file completely.
123+
let eslintrcFolderPath: string | undefined = process.env[ESLINT_BULK_ESLINTRC_FOLDER_PATH_ENV_VAR_NAME];
124+
if (eslintrcFolderPath) {
125+
return eslintrcFolderPath;
126+
}
94127
const cachedFolderPathForFilePath: string | undefined =
95128
eslintrcPathByFileOrFolderPath.get(normalizedFilePath);
96129
if (cachedFolderPathForFilePath) {
@@ -102,7 +135,6 @@ function findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFilePath:
102135
);
103136

104137
const pathsToCache: string[] = [normalizedFilePath];
105-
let eslintrcFolderPath: string | undefined;
106138
findEslintrcFileLoop: for (
107139
let currentFolder: string = normalizedFileFolderPath;
108140
currentFolder; // 'something'.substring(0, -1) is ''
@@ -133,39 +165,46 @@ function findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFilePath:
133165
}
134166
}
135167

136-
// One-line insert into the ruleContext report method to prematurely exit if the ESLint problem has been suppressed
137-
export function shouldBulkSuppress(params: {
138-
filename: string;
139-
currentNode: TSESTree.Node;
140-
ruleId: string;
141-
problem: IProblem;
142-
}): boolean {
143-
// Use this ENV variable to turn off eslint-bulk-suppressions functionality, default behavior is on
144-
if (process.env[ESLINT_BULK_ENABLE_ENV_VAR_NAME] === 'false') {
145-
return false;
168+
let rawGetLinterInternalSlots: ((linter: unknown) => ILinterInternalSlots) | undefined;
169+
170+
export function getLinterInternalSlots(linter: unknown): ILinterInternalSlots {
171+
if (!rawGetLinterInternalSlots) {
172+
throw new Error('getLinterInternalSlots has not been set');
146173
}
147174

148-
const { filename: fileAbsolutePath, currentNode, ruleId: rule, problem } = params;
149-
const normalizedFileAbsolutePath: string = fileAbsolutePath.replace(/\\/g, '/');
150-
const eslintrcDirectory: string =
151-
findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFileAbsolutePath);
152-
const fileRelativePath: string = normalizedFileAbsolutePath.substring(eslintrcDirectory.length + 1);
175+
return rawGetLinterInternalSlots(linter);
176+
}
177+
178+
export function getBulkSuppression(params: {
179+
serializedSuppressions: Set<string>;
180+
fileRelativePath: string;
181+
problem: IProblem;
182+
}): IBulkSuppression | undefined {
183+
const { fileRelativePath, serializedSuppressions, problem } = params;
184+
const { ruleId: rule } = problem;
185+
186+
const currentNode: TSESTree.Node | undefined = astNodeForProblem.get(problem);
187+
153188
const scopeId: string = calculateScopeId(currentNode);
154189
const suppression: ISuppression = { file: fileRelativePath, scopeId, rule };
155190

156-
const config: IBulkSuppressionsConfig = getSuppressionsConfigForEslintrcFolderPath(eslintrcDirectory);
157191
const serializedSuppression: string = serializeSuppression(suppression);
158-
const currentNodeIsSuppressed: boolean = config.serializedSuppressions.has(serializedSuppression);
192+
const currentNodeIsSuppressed: boolean = serializedSuppressions.has(serializedSuppression);
159193

160194
if (currentNodeIsSuppressed || SUPPRESS_ALL_RULES || RULES_TO_SUPPRESS?.has(suppression.rule)) {
161-
problem[SUPPRESSION_SYMBOL] = {
195+
// The suppressions object should already be empty, otherwise we shouldn't see this problem
196+
problem.suppressions = [
197+
{
198+
kind: 'bulk',
199+
justification: serializedSuppression
200+
}
201+
];
202+
203+
return {
162204
suppression,
163-
serializedSuppression,
164-
config
205+
serializedSuppression
165206
};
166207
}
167-
168-
return process.env[ESLINT_BULK_PRUNE_ENV_VAR_NAME] !== '1' && currentNodeIsSuppressed;
169208
}
170209

171210
export function prune(): void {
@@ -187,15 +226,11 @@ export function prune(): void {
187226
}
188227
}
189228

229+
/**
230+
* @deprecated Use "prune" instead.
231+
*/
190232
export function write(): void {
191-
for (const [
192-
eslintrcFolderPath,
193-
suppressionsConfig
194-
] of getAllBulkSuppressionsConfigsByEslintrcFolderPath()) {
195-
if (suppressionsConfig) {
196-
writeSuppressionsJsonToFile(eslintrcFolderPath, suppressionsConfig);
197-
}
198-
}
233+
return prune();
199234
}
200235

201236
// utility function for linter-patch.js to make require statements that use relative paths in linter.js work in linter-patch.js
@@ -209,56 +244,94 @@ export function requireFromPathToLinterJS(importPath: string): import('eslint').
209244
return require(moduleAbsolutePath);
210245
}
211246

212-
export function patchClass<T, U extends T>(originalClass: new () => T, patchedClass: new () => U): void {
213-
// Get all the property names of the patched class prototype
214-
const patchedProperties: string[] = Object.getOwnPropertyNames(patchedClass.prototype);
215-
216-
// Loop through all the properties
217-
for (const prop of patchedProperties) {
218-
// Override the property in the original class
219-
originalClass.prototype[prop] = patchedClass.prototype[prop];
220-
}
247+
/**
248+
* Patches ESLint's Linter class to support bulk suppressions
249+
* @param originalClass - The original Linter class from ESLint
250+
* @param patchedClass - The patched Linter class from the generated file
251+
* @param originalGetLinterInternalSlots - The original getLinterInternalSlots function from ESLint
252+
*/
253+
export function patchLinter(
254+
originalClass: ILinterClass,
255+
patchedClass: ILinterClass,
256+
originalGetLinterInternalSlots: typeof getLinterInternalSlots
257+
): void {
258+
// Ensure we use the correct internal slots map
259+
rawGetLinterInternalSlots = originalGetLinterInternalSlots;
221260

222-
// Handle getters and setters
261+
// Transfer all properties
223262
for (const [prop, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(patchedClass.prototype))) {
224-
if (descriptor.get || descriptor.set) {
225-
Object.defineProperty(originalClass.prototype, prop, descriptor);
226-
}
263+
Object.defineProperty(originalClass.prototype, prop, descriptor);
227264
}
228-
}
229265

230-
/**
231-
* This returns a wrapped version of the "verify" function from ESLint's Linter class
232-
* that postprocesses rule violations that weren't suppressed by comments. This postprocessing
233-
* records suppressions that weren't otherwise suppressed by comments to be used
234-
* by the "suppress" and "prune" commands.
235-
*/
236-
export function extendVerifyFunction(
237-
originalFn: (this: unknown, ...args: unknown[]) => IProblem[] | undefined
238-
): (this: unknown, ...args: unknown[]) => IProblem[] | undefined {
239-
return function (this: unknown, ...args: unknown[]): IProblem[] | undefined {
240-
const problems: IProblem[] | undefined = originalFn.apply(this, args);
241-
if (problems) {
266+
const originalVerify: (...args: unknown[]) => IProblem[] | undefined = originalClass.prototype.verify as (
267+
...args: unknown[]
268+
) => IProblem[] | undefined;
269+
originalClass.prototype.verify = verify;
270+
271+
function verify(this: unknown, ...args: unknown[]): IProblem[] | undefined {
272+
try {
273+
const problems: IProblem[] | undefined = originalVerify.apply(this, args);
274+
if (!problems) {
275+
return problems;
276+
}
277+
278+
const internalSlots: ILinterInternalSlots = getLinterInternalSlots(this);
279+
280+
if (args.length < 3) {
281+
throw new Error('Expected at least 3 arguments to Linter.prototype.verify');
282+
}
283+
284+
const fileNameOrOptions: string | { filename: string } = args[2] as string | { filename: string };
285+
const filename: string =
286+
typeof fileNameOrOptions === 'string' ? fileNameOrOptions : fileNameOrOptions.filename;
287+
288+
let { lastSuppressedMessages } = internalSlots;
289+
290+
const normalizedFileAbsolutePath: string = filename.replace(/\\/g, '/');
291+
const eslintrcDirectory: string =
292+
findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFileAbsolutePath);
293+
const fileRelativePath: string = normalizedFileAbsolutePath.substring(eslintrcDirectory.length + 1);
294+
const config: IBulkSuppressionsConfig = getSuppressionsConfigForEslintrcFolderPath(eslintrcDirectory);
295+
const {
296+
newSerializedSuppressions,
297+
serializedSuppressions,
298+
jsonObject: { suppressions },
299+
newJsonObject: { suppressions: newSuppressions }
300+
} = config;
301+
302+
const filteredProblems: IProblem[] = [];
303+
242304
for (const problem of problems) {
243-
if (problem[SUPPRESSION_SYMBOL]) {
244-
const {
245-
serializedSuppression,
246-
suppression,
247-
config: {
248-
newSerializedSuppressions,
249-
jsonObject: { suppressions },
250-
newJsonObject: { suppressions: newSuppressions }
251-
}
252-
} = problem[SUPPRESSION_SYMBOL];
253-
if (!newSerializedSuppressions.has(serializedSuppression)) {
254-
newSerializedSuppressions.add(serializedSuppression);
255-
newSuppressions.push(suppression);
256-
suppressions.push(suppression);
305+
const bulkSuppression: IBulkSuppression | undefined = getBulkSuppression({
306+
fileRelativePath,
307+
serializedSuppressions,
308+
problem
309+
});
310+
311+
if (!bulkSuppression) {
312+
filteredProblems.push(problem);
313+
continue;
314+
}
315+
316+
const { serializedSuppression, suppression } = bulkSuppression;
317+
318+
if (!newSerializedSuppressions.has(serializedSuppression)) {
319+
newSerializedSuppressions.add(serializedSuppression);
320+
newSuppressions.push(suppression);
321+
suppressions.push(suppression);
322+
323+
if (!lastSuppressedMessages) {
324+
lastSuppressedMessages = [];
325+
internalSlots.lastSuppressedMessages = lastSuppressedMessages;
257326
}
327+
328+
lastSuppressedMessages.push(problem);
258329
}
259330
}
260-
}
261331

262-
return problems;
263-
};
332+
return filteredProblems;
333+
} finally {
334+
astNodeForProblem.clear();
335+
}
336+
}
264337
}

eslint/eslint-patch/src/eslint-bulk-suppressions/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const ESLINT_BULK_SUPPRESS_ENV_VAR_NAME: 'RUSHSTACK_ESLINT_BULK_SUPPRESS'
77
'RUSHSTACK_ESLINT_BULK_SUPPRESS';
88
export const ESLINT_BULK_ENABLE_ENV_VAR_NAME: 'ESLINT_BULK_ENABLE' = 'ESLINT_BULK_ENABLE';
99
export const ESLINT_BULK_PRUNE_ENV_VAR_NAME: 'ESLINT_BULK_PRUNE' = 'ESLINT_BULK_PRUNE';
10+
export const ESLINT_BULK_ESLINTRC_FOLDER_PATH_ENV_VAR_NAME: 'ESLINT_BULK_ESLINTRC_FOLDER_PATH' =
11+
'ESLINT_BULK_ESLINTRC_FOLDER_PATH';
1012
export const ESLINT_BULK_DETECT_ENV_VAR_NAME: '_RUSHSTACK_ESLINT_BULK_DETECT' =
1113
'_RUSHSTACK_ESLINT_BULK_DETECT';
1214
export const ESLINT_BULK_FORCE_REGENERATE_PATCH_ENV_VAR_NAME: 'RUSHSTACK_ESLINT_BULK_FORCE_REGENERATE_PATCH' =
@@ -18,3 +20,5 @@ export const ESLINT_PACKAGE_NAME_ENV_VAR_NAME: '_RUSHSTACK_ESLINT_PACKAGE_NAME'
1820

1921
export const BULK_SUPPRESSIONS_CLI_ESLINT_PACKAGE_NAME: string =
2022
process.env[ESLINT_PACKAGE_NAME_ENV_VAR_NAME] ?? 'eslint';
23+
24+
export const SUPPRESSIONS_JSON_FILENAME: '.eslint-bulk-suppressions.json' = '.eslint-bulk-suppressions.json';

0 commit comments

Comments
 (0)