@@ -8,9 +8,8 @@ import * as Guards from './ast-guards';
88
99import { eslintFolder } from '../_patch-base' ;
1010import {
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' ;
1514import {
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' ) ;
3129const ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE : string | undefined = process . env [ ESLINT_BULK_SUPPRESS_ENV_VAR_NAME ] ;
3230const SUPPRESS_ALL_RULES : boolean = ESLINT_BULK_SUPPRESS_ENV_VAR_VALUE === '*' ;
3331const 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+
3740interface 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+
4572function 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 {
91118const eslintrcPathByFileOrFolderPath : Map < string , string > = new Map ( ) ;
92119
93120function 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
171210export function prune ( ) : void {
@@ -187,15 +226,11 @@ export function prune(): void {
187226 }
188227}
189228
229+ /**
230+ * @deprecated Use "prune" instead.
231+ */
190232export 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}
0 commit comments