@@ -8,9 +8,8 @@ import * as Guards from './ast-guards';
8
8
9
9
import { eslintFolder } from '../_patch-base' ;
10
10
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
14
13
} from './constants' ;
15
14
import {
16
15
getSuppressionsConfigForEslintrcFolderPath ,
@@ -27,21 +26,49 @@ const ESLINTRC_FILENAMES: string[] = [
27
26
// Several other filenames are allowed, but this patch requires that it be loaded via a JS config file,
28
27
// so we only need to check for the JS-based filenames
29
28
] ;
30
- const SUPPRESSION_SYMBOL : unique symbol = Symbol ( 'suppression' ) ;
31
29
const ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE : string | undefined = process . env [ ESLINT_BULK_SUPPRESS_ENV_VAR_NAME ] ;
32
30
const SUPPRESS_ALL_RULES : boolean = ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE === '*' ;
33
31
const RULES_TO_SUPPRESS : Set < string > | undefined = ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE
34
32
? new Set ( ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE . split ( ',' ) )
35
33
: undefined ;
36
34
35
+ interface IBulkSuppression {
36
+ suppression : ISuppression ;
37
+ serializedSuppression : string ;
38
+ }
39
+
37
40
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 ;
42
59
} ;
43
60
}
44
61
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
+
45
72
function getNodeName ( node : TSESTree . Node ) : string | undefined {
46
73
if ( Guards . isClassDeclarationWithName ( node ) ) {
47
74
return node . id . name ;
@@ -91,6 +118,12 @@ function calculateScopeId(node: NodeWithParent | undefined): string {
91
118
const eslintrcPathByFileOrFolderPath : Map < string , string > = new Map ( ) ;
92
119
93
120
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
+ }
94
127
const cachedFolderPathForFilePath : string | undefined =
95
128
eslintrcPathByFileOrFolderPath . get ( normalizedFilePath ) ;
96
129
if ( cachedFolderPathForFilePath ) {
@@ -102,7 +135,6 @@ function findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFilePath:
102
135
) ;
103
136
104
137
const pathsToCache : string [ ] = [ normalizedFilePath ] ;
105
- let eslintrcFolderPath : string | undefined ;
106
138
findEslintrcFileLoop: for (
107
139
let currentFolder : string = normalizedFileFolderPath ;
108
140
currentFolder ; // 'something'.substring(0, -1) is ''
@@ -133,39 +165,46 @@ function findEslintrcFolderPathForNormalizedFileAbsolutePath(normalizedFilePath:
133
165
}
134
166
}
135
167
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' ) ;
146
173
}
147
174
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
+
153
188
const scopeId : string = calculateScopeId ( currentNode ) ;
154
189
const suppression : ISuppression = { file : fileRelativePath , scopeId, rule } ;
155
190
156
- const config : IBulkSuppressionsConfig = getSuppressionsConfigForEslintrcFolderPath ( eslintrcDirectory ) ;
157
191
const serializedSuppression : string = serializeSuppression ( suppression ) ;
158
- const currentNodeIsSuppressed : boolean = config . serializedSuppressions . has ( serializedSuppression ) ;
192
+ const currentNodeIsSuppressed : boolean = serializedSuppressions . has ( serializedSuppression ) ;
159
193
160
194
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 {
162
204
suppression,
163
- serializedSuppression,
164
- config
205
+ serializedSuppression
165
206
} ;
166
207
}
167
-
168
- return process . env [ ESLINT_BULK_PRUNE_ENV_VAR_NAME ] !== '1' && currentNodeIsSuppressed ;
169
208
}
170
209
171
210
export function prune ( ) : void {
@@ -187,15 +226,11 @@ export function prune(): void {
187
226
}
188
227
}
189
228
229
+ /**
230
+ * @deprecated Use "prune" instead.
231
+ */
190
232
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 ( ) ;
199
234
}
200
235
201
236
// 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').
209
244
return require ( moduleAbsolutePath ) ;
210
245
}
211
246
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 ;
221
260
222
- // Handle getters and setters
261
+ // Transfer all properties
223
262
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 ) ;
227
264
}
228
- }
229
265
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
+
242
304
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 ;
257
326
}
327
+
328
+ lastSuppressedMessages . push ( problem ) ;
258
329
}
259
330
}
260
- }
261
331
262
- return problems ;
263
- } ;
332
+ return filteredProblems ;
333
+ } finally {
334
+ astNodeForProblem . clear ( ) ;
335
+ }
336
+ }
264
337
}
0 commit comments