2 * @fileoverview A module that filters reported problems based on `eslint-disable` and `eslint-enable` comments
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 /** @typedef {import("../shared/types").LintMessage} LintMessage */
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const escapeRegExp
= require("escape-string-regexp");
21 * Compares the locations of two objects in a source file
22 * @param {{line: number, column: number}} itemA The first object
23 * @param {{line: number, column: number}} itemB The second object
24 * @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
25 * itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
27 function compareLocations(itemA
, itemB
) {
28 return itemA
.line
- itemB
.line
|| itemA
.column
- itemB
.column
;
32 * Groups a set of directives into sub-arrays by their parent comment.
33 * @param {Directive[]} directives Unused directives to be removed.
34 * @returns {Directive[][]} Directives grouped by their parent comment.
36 function groupByParentComment(directives
) {
37 const groups
= new Map();
39 for (const directive
of directives
) {
40 const { unprocessedDirective
: { parentComment
} } = directive
;
42 if (groups
.has(parentComment
)) {
43 groups
.get(parentComment
).push(directive
);
45 groups
.set(parentComment
, [directive
]);
49 return [...groups
.values()];
53 * Creates removal details for a set of directives within the same comment.
54 * @param {Directive[]} directives Unused directives to be removed.
55 * @param {Token} commentToken The backing Comment token.
56 * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
58 function createIndividualDirectivesRemoval(directives
, commentToken
) {
61 * `commentToken.value` starts right after `//` or `/*`.
62 * All calculated offsets will be relative to this index.
64 const commentValueStart
= commentToken
.range
[0] + "//".length
;
66 // Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
67 const listStartOffset
= /^\s*\S+\s+/u.exec(commentToken
.value
)[0].length
;
70 * Get the list text without any surrounding whitespace. In order to preserve the original
71 * formatting, we don't want to change that whitespace.
73 * // eslint-disable-line rule-one , rule-two , rule-three -- comment
74 * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 const listText
= commentToken
.value
77 .slice(listStartOffset
) // remove directive name and all whitespace before the list
78 .split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
79 .trimEnd(); // remove all whitespace after the list
82 * We can assume that `listText` contains multiple elements.
83 * Otherwise, this function wouldn't be called - if there is
84 * only one rule in the list, then the whole comment must be removed.
87 return directives
.map(directive
=> {
88 const { ruleId
} = directive
;
90 const regex
= new RegExp(String
.raw
`(?:^|\s*,\s*)${escapeRegExp(ruleId)}(?:\s*,\s*|$)`, "u");
91 const match
= regex
.exec(listText
);
92 const matchedText
= match
[0];
93 const matchStartOffset
= listStartOffset
+ match
.index
;
94 const matchEndOffset
= matchStartOffset
+ matchedText
.length
;
96 const firstIndexOfComma
= matchedText
.indexOf(",");
97 const lastIndexOfComma
= matchedText
.lastIndexOf(",");
99 let removalStartOffset
, removalEndOffset
;
101 if (firstIndexOfComma
!== lastIndexOfComma
) {
104 * Since there are two commas, this must one of the elements in the middle of the list.
105 * Matched range starts where the previous rule name ends, and ends where the next rule name starts.
107 * // eslint-disable-line rule-one , rule-two , rule-three -- comment
110 * We want to remove only the content between the two commas, and also one of the commas.
112 * // eslint-disable-line rule-one , rule-two , rule-three -- comment
115 removalStartOffset
= matchStartOffset
+ firstIndexOfComma
;
116 removalEndOffset
= matchStartOffset
+ lastIndexOfComma
;
121 * This is either the first element or the last element.
123 * If this is the first element, matched range starts where the first rule name starts
124 * and ends where the second rule name starts. This is exactly the range we want
125 * to remove so that the second rule name will start where the first one was starting
126 * and thus preserve the original formatting.
128 * // eslint-disable-line rule-one , rule-two , rule-three -- comment
131 * Similarly, if this is the last element, we've already matched the range we want to
132 * remove. The previous rule name will end where the last one was ending, relative
133 * to the content on the right side.
135 * // eslint-disable-line rule-one , rule-two , rule-three -- comment
138 removalStartOffset
= matchStartOffset
;
139 removalEndOffset
= matchEndOffset
;
143 description
: `'${ruleId}'`,
146 commentValueStart
+ removalStartOffset
,
147 commentValueStart
+ removalEndOffset
151 unprocessedDirective
: directive
.unprocessedDirective
157 * Creates a description of deleting an entire unused disable comment.
158 * @param {Directive[]} directives Unused directives to be removed.
159 * @param {Token} commentToken The backing Comment token.
160 * @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output Problem.
162 function createCommentRemoval(directives
, commentToken
) {
163 const { range
} = commentToken
;
164 const ruleIds
= directives
.filter(directive
=> directive
.ruleId
).map(directive
=> `'${directive.ruleId}'`);
167 description
: ruleIds
.length
<= 2
168 ? ruleIds
.join(" or ")
169 : `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds[ruleIds.length - 1]}`,
174 unprocessedDirective
: directives
[0].unprocessedDirective
179 * Parses details from directives to create output Problems.
180 * @param {Directive[]} allDirectives Unused directives to be removed.
181 * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
183 function processUnusedDisableDirectives(allDirectives
) {
184 const directiveGroups
= groupByParentComment(allDirectives
);
186 return directiveGroups
.flatMap(
188 const { parentComment
} = directives
[0].unprocessedDirective
;
189 const remainingRuleIds
= new Set(parentComment
.ruleIds
);
191 for (const directive
of directives
) {
192 remainingRuleIds
.delete(directive
.ruleId
);
195 return remainingRuleIds
.size
196 ? createIndividualDirectivesRemoval(directives
, parentComment
.commentToken
)
197 : [createCommentRemoval(directives
, parentComment
.commentToken
)];
203 * This is the same as the exported function, except that it
204 * doesn't handle disable-line and disable-next-line directives, and it always reports unused
205 * disable directives.
206 * @param {Object} options options for applying directives. This is the same as the options
207 * for the exported function, except that `reportUnusedDisableDirectives` is not supported
208 * (this function always reports unused disable directives).
209 * @returns {{problems: LintMessage[], unusedDisableDirectives: LintMessage[]}} An object with a list
210 * of problems (including suppressed ones) and unused eslint-disable directives
212 function applyDirectives(options
) {
214 const usedDisableDirectives
= new Set();
216 for (const problem
of options
.problems
) {
217 let disableDirectivesForProblem
= [];
218 let nextDirectiveIndex
= 0;
221 nextDirectiveIndex
< options
.directives
.length
&&
222 compareLocations(options
.directives
[nextDirectiveIndex
], problem
) <= 0
224 const directive
= options
.directives
[nextDirectiveIndex
++];
226 if (directive
.ruleId
=== null || directive
.ruleId
=== problem
.ruleId
) {
227 switch (directive
.type
) {
229 disableDirectivesForProblem
.push(directive
);
233 disableDirectivesForProblem
= [];
241 if (disableDirectivesForProblem
.length
> 0) {
242 const suppressions
= disableDirectivesForProblem
.map(directive
=> ({
244 justification
: directive
.unprocessedDirective
.justification
247 if (problem
.suppressions
) {
248 problem
.suppressions
= problem
.suppressions
.concat(suppressions
);
250 problem
.suppressions
= suppressions
;
251 usedDisableDirectives
.add(disableDirectivesForProblem
[disableDirectivesForProblem
.length
- 1]);
255 problems
.push(problem
);
258 const unusedDisableDirectivesToReport
= options
.directives
259 .filter(directive
=> directive
.type
=== "disable" && !usedDisableDirectives
.has(directive
));
261 const processed
= processUnusedDisableDirectives(unusedDisableDirectivesToReport
);
263 const unusedDisableDirectives
= processed
264 .map(({ description
, fix
, unprocessedDirective
}) => {
265 const { parentComment
, type
, line
, column
} = unprocessedDirective
;
270 ? `Unused eslint-disable directive (no problems were reported from ${description}).`
271 : "Unused eslint-disable directive (no problems were reported).",
272 line
: type
=== "disable-next-line" ? parentComment
.commentToken
.loc
.start
.line
: line
,
273 column
: type
=== "disable-next-line" ? parentComment
.commentToken
.loc
.start
.column
+ 1 : column
,
274 severity
: options
.reportUnusedDisableDirectives
=== "warn" ? 1 : 2,
276 ...options
.disableFixes
? {} : { fix
}
280 return { problems
, unusedDisableDirectives
};
284 * Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
285 * of reported problems, adds the suppression information to the problems.
286 * @param {Object} options Information about directives and problems
288 * type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
289 * ruleId: (string|null),
292 * justification: string
293 * }} options.directives Directive comments found in the file, with one-based columns.
294 * Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
295 * comment for two different rules is represented as two directives).
296 * @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
297 * A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
298 * @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
299 * @param {boolean} options.disableFixes If true, it doesn't make `fix` properties.
300 * @returns {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]}
301 * An object with a list of reported problems, the suppressed of which contain the suppression information.
303 module
.exports
= ({ directives
, disableFixes
, problems
, reportUnusedDisableDirectives
= "off" }) => {
304 const blockDirectives
= directives
305 .filter(directive
=> directive
.type
=== "disable" || directive
.type
=== "enable")
306 .map(directive
=> Object
.assign({}, directive
, { unprocessedDirective
: directive
}))
307 .sort(compareLocations
);
309 const lineDirectives
= directives
.flatMap(directive
=> {
310 switch (directive
.type
) {
317 { type
: "disable", line
: directive
.line
, column
: 1, ruleId
: directive
.ruleId
, unprocessedDirective
: directive
},
318 { type
: "enable", line
: directive
.line
+ 1, column
: 0, ruleId
: directive
.ruleId
, unprocessedDirective
: directive
}
321 case "disable-next-line":
323 { type
: "disable", line
: directive
.line
+ 1, column
: 1, ruleId
: directive
.ruleId
, unprocessedDirective
: directive
},
324 { type
: "enable", line
: directive
.line
+ 2, column
: 0, ruleId
: directive
.ruleId
, unprocessedDirective
: directive
}
328 throw new TypeError(`Unrecognized directive type '${directive.type}'`);
330 }).sort(compareLocations
);
332 const blockDirectivesResult
= applyDirectives({
334 directives
: blockDirectives
,
336 reportUnusedDisableDirectives
338 const lineDirectivesResult
= applyDirectives({
339 problems
: blockDirectivesResult
.problems
,
340 directives
: lineDirectives
,
342 reportUnusedDisableDirectives
345 return reportUnusedDisableDirectives
!== "off"
346 ? lineDirectivesResult
.problems
347 .concat(blockDirectivesResult
.unusedDisableDirectives
)
348 .concat(lineDirectivesResult
.unusedDisableDirectives
)
349 .sort(compareLocations
)
350 : lineDirectivesResult
.problems
;