Skip to content

Commit 027e20c

Browse files
authored
✨ Add support for eslint-disable-line block comments and directive comments with descriptions. (#43)
1 parent e93ac75 commit 027e20c

14 files changed

+704
-36
lines changed

lib/internal/disabled-area.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"use strict"
66

77
const utils = require("./utils")
8-
const COMMENT_DIRECTIVE = /^\s*(eslint-(?:en|dis)able(?:(?:-next)?-line)?)\s*(?:(\S|\S[\s\S]*\S)\s*)?$/u
98
const DELIMITER = /[\s,]+/gu
109
const pool = new WeakMap()
1110

@@ -162,39 +161,44 @@ module.exports = class DisabledArea {
162161
}
163162

164163
/**
165-
* Scan the souce code and setup disabled area list.
164+
* Scan the source code and setup disabled area list.
166165
*
167166
* @param {eslint.SourceCode} sourceCode - The source code to scan.
168167
* @returns {void}
169168
* @private
170169
*/
171170
_scan(sourceCode) {
172171
for (const comment of sourceCode.getAllComments()) {
173-
const m = COMMENT_DIRECTIVE.exec(comment.value)
174-
if (m == null) {
172+
const directiveComment = utils.parseDirectiveComment(comment)
173+
if (directiveComment == null) {
175174
continue
176175
}
177-
const kind = m[1]
178-
const ruleIds = m[2] ? m[2].split(DELIMITER) : null
179176

180-
if (comment.type === "Block" && kind === "eslint-disable") {
177+
const kind = directiveComment.kind
178+
if (
179+
kind !== "eslint-disable" &&
180+
kind !== "eslint-enable" &&
181+
kind !== "eslint-disable-line" &&
182+
kind !== "eslint-disable-next-line"
183+
) {
184+
continue
185+
}
186+
const ruleIds = directiveComment.value
187+
? directiveComment.value.split(DELIMITER)
188+
: null
189+
190+
if (kind === "eslint-disable") {
181191
this._disable(comment, comment.loc.start, ruleIds, "block")
182-
} else if (comment.type === "Block" && kind === "eslint-enable") {
192+
} else if (kind === "eslint-enable") {
183193
this._enable(comment, comment.loc.start, ruleIds, "block")
184-
} else if (
185-
comment.type === "Line" &&
186-
kind === "eslint-disable-line"
187-
) {
194+
} else if (kind === "eslint-disable-line") {
188195
const line = comment.loc.start.line
189196
const start = { line, column: 0 }
190197
const end = { line: line + 1, column: -1 }
191198

192199
this._disable(comment, start, ruleIds, "line")
193200
this._enable(comment, end, ruleIds, "line")
194-
} else if (
195-
comment.type === "Line" &&
196-
kind === "eslint-disable-next-line"
197-
) {
201+
} else if (kind === "eslint-disable-next-line") {
198202
const line = comment.loc.start.line
199203
const start = { line: line + 1, column: 0 }
200204
const end = { line: line + 2, column: -1 }

lib/internal/utils.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
const escapeStringRegexp = require("escape-string-regexp")
88
const LINE_PATTERN = /[^\r\n\u2028\u2029]*(?:\r\n|[\r\n\u2028\u2029]|$)/gu
99

10+
const DIRECTIVE_PATTERN = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u
11+
const LINE_COMMENT_PATTERN = /^eslint-disable-(next-)?line$/u
12+
1013
module.exports = {
1114
/**
1215
* Make the location ignoring `eslint-disable` comments.
@@ -95,4 +98,55 @@ module.exports = {
9598
lte(a, b) {
9699
return a.line < b.line || (a.line === b.line && a.column <= b.column)
97100
},
101+
102+
/**
103+
* Parse the given comment token as a directive comment.
104+
*
105+
* @param {Token} comment - The comment token to parse.
106+
* @returns {{kind: string, value: string, description: string | null}|null} The parsed data of the given comment. If `null`, it is not a directive comment.
107+
*/
108+
parseDirectiveComment(comment) {
109+
const { text, description } = divideDirectiveComment(comment.value)
110+
const match = DIRECTIVE_PATTERN.exec(text)
111+
112+
if (!match) {
113+
return null
114+
}
115+
const directiveText = match[1]
116+
const lineCommentSupported = LINE_COMMENT_PATTERN.test(directiveText)
117+
118+
if (comment.type === "Line" && !lineCommentSupported) {
119+
return null
120+
}
121+
122+
if (
123+
lineCommentSupported &&
124+
comment.loc.start.line !== comment.loc.end.line
125+
) {
126+
// disable-line comment should not span multiple lines.
127+
return null
128+
}
129+
130+
const directiveValue = text.slice(match.index + directiveText.length)
131+
132+
return {
133+
kind: directiveText,
134+
value: directiveValue.trim(),
135+
description,
136+
}
137+
},
138+
}
139+
140+
/**
141+
* Divides and trims description text and directive comments.
142+
* @param {string} value The comment text to strip.
143+
* @returns {{text: string, description: string | null}} The stripped text.
144+
*/
145+
function divideDirectiveComment(value) {
146+
const divided = value.split(/\s-{2,}\s/u)
147+
const text = divided[0].trim()
148+
return {
149+
text,
150+
description: divided.length > 1 ? divided[1].trim() : null,
151+
}
98152
}

lib/rules/no-unlimited-disable.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66

77
const utils = require("../internal/utils")
88

9-
const PATTERNS = {
10-
Block: /^\s*(eslint-disable)\s*(\S)?/u,
11-
Line: /^\s*(eslint-disable(?:-next)?-line)\s*(\S)?/u,
12-
}
13-
149
module.exports = {
1510
meta: {
1611
docs: {
@@ -32,18 +27,27 @@ module.exports = {
3227
return {
3328
Program() {
3429
for (const comment of sourceCode.getAllComments()) {
35-
const pattern = PATTERNS[comment.type]
36-
if (pattern == null) {
30+
const directiveComment = utils.parseDirectiveComment(
31+
comment
32+
)
33+
if (directiveComment == null) {
3734
continue
3835
}
3936

40-
const m = pattern.exec(comment.value)
41-
if (m && !m[2]) {
37+
const kind = directiveComment.kind
38+
if (
39+
kind !== "eslint-disable" &&
40+
kind !== "eslint-disable-line" &&
41+
kind !== "eslint-disable-next-line"
42+
) {
43+
continue
44+
}
45+
if (!directiveComment.value) {
4246
context.report({
4347
loc: utils.toForceLocation(comment.loc),
4448
message:
4549
"Unexpected unlimited '{{kind}}' comment. Specify some rule names to disable.",
46-
data: { kind: m[1] },
50+
data: { kind: directiveComment.kind },
4751
})
4852
}
4953
}

lib/rules/no-use.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
"use strict"
66

77
const utils = require("../internal/utils")
8-
const PATTERNS = {
9-
Block: /^\s*(eslint(?:-disable|-enable|-env)?|exported|globals?)(?:\s|$)/u,
10-
Line: /^\s*(eslint-disable(?:-next)?-line)(?:\s|$)/u,
11-
}
128

139
module.exports = {
1410
meta: {
@@ -58,13 +54,14 @@ module.exports = {
5854
return {
5955
Program() {
6056
for (const comment of sourceCode.getAllComments()) {
61-
const pattern = PATTERNS[comment.type]
62-
if (pattern == null) {
57+
const directiveComment = utils.parseDirectiveComment(
58+
comment
59+
)
60+
if (directiveComment == null) {
6361
continue
6462
}
6563

66-
const m = pattern.exec(comment.value)
67-
if (m != null && !allowed.has(m[1])) {
64+
if (!allowed.has(directiveComment.kind)) {
6865
context.report({
6966
loc: utils.toForceLocation(comment.loc),
7067
message: "Unexpected ESLint directive comment.",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"nyc": "^14.1.1",
3131
"opener": "^1.4.3",
3232
"rimraf": "^2.6.2",
33+
"semver": "^7.3.2",
3334
"string-replace-loader": "^2.1.1",
3435
"vue-eslint-editor": "^1.1.0",
3536
"vuepress": "^1.0.1"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Test that multi-line eslint-disable-line comments are not false positives.
3+
*/
4+
"use strict"
5+
6+
const assert = require("assert")
7+
const fs = require("fs")
8+
const path = require("path")
9+
const spawn = require("cross-spawn")
10+
const rimraf = require("rimraf")
11+
const semver = require("semver")
12+
const eslintVersion = require("eslint/package").version
13+
14+
/**
15+
* Run eslint CLI command with a given source code.
16+
* @param {string} code The source code to lint.
17+
* @returns {Promise<Message[]>} The result message.
18+
*/
19+
function runESLint(code) {
20+
return new Promise((resolve, reject) => {
21+
const cp = spawn(
22+
"eslint",
23+
[
24+
"--stdin",
25+
"--stdin-filename",
26+
"test.js",
27+
"--no-eslintrc",
28+
"--plugin",
29+
"eslint-comments",
30+
"--format",
31+
"json",
32+
],
33+
{ stdio: ["pipe", "pipe", "inherit"] }
34+
)
35+
const chunks = []
36+
let totalLength = 0
37+
38+
cp.stdout.on("data", chunk => {
39+
chunks.push(chunk)
40+
totalLength += chunk.length
41+
})
42+
cp.stdout.on("end", () => {
43+
try {
44+
const resultsStr = String(Buffer.concat(chunks, totalLength))
45+
const results = JSON.parse(resultsStr)
46+
resolve(results[0].messages)
47+
} catch (error) {
48+
reject(error)
49+
}
50+
})
51+
cp.on("error", reject)
52+
53+
cp.stdin.end(code)
54+
})
55+
}
56+
57+
describe("multi-line eslint-disable-line comments", () => {
58+
before(() => {
59+
// Register this plugin.
60+
const selfPath = path.resolve(__dirname, "../../")
61+
const pluginPath = path.resolve(
62+
__dirname,
63+
"../../node_modules/eslint-plugin-eslint-comments"
64+
)
65+
66+
if (fs.existsSync(pluginPath)) {
67+
rimraf.sync(pluginPath)
68+
}
69+
fs.symlinkSync(selfPath, pluginPath, "junction")
70+
})
71+
72+
describe("`eslint-comments/*` rules are valid", () => {
73+
for (const code of [
74+
`/* eslint eslint-comments/no-use:[error, {allow: ['eslint']}] */
75+
/* eslint-disable-line
76+
*/
77+
/* eslint-disable-next-line
78+
*/`,
79+
`/* eslint eslint-comments/no-duplicate-disable:error */
80+
/*eslint-disable no-undef*/
81+
/*eslint-disable-line
82+
no-undef*/
83+
`,
84+
]) {
85+
it(code, () =>
86+
runESLint(code).then(messages => {
87+
if (semver.satisfies(eslintVersion, ">=5.0.0")) {
88+
assert.strictEqual(messages.length > 0, true)
89+
}
90+
const normalMessages = messages.filter(
91+
message => message.ruleId != null
92+
)
93+
assert.strictEqual(normalMessages.length, 0)
94+
})
95+
)
96+
}
97+
})
98+
})

tests/lib/rules/disable-enable-pair.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
"use strict"
66

7+
const semver = require("semver")
8+
const eslintVersion = require("eslint/package").version
79
const RuleTester = require("eslint").RuleTester
810
const rule = require("../../../lib/rules/disable-enable-pair")
911
const tester = new RuleTester()
@@ -24,6 +26,8 @@ tester.run("disable-enable-pair", rule, {
2426
`,
2527
"//eslint-disable-line",
2628
"//eslint-disable-next-line",
29+
"/*eslint-disable-line*/",
30+
"/*eslint-disable-next-line*/",
2731
"/*eslint no-undef: off */",
2832
`
2933
function foo() {
@@ -76,6 +80,19 @@ var foo = 1
7680
`,
7781
options: [{ allowWholeFile: true }],
7882
},
83+
// -- description
84+
...(semver.satisfies(eslintVersion, ">=7.0.0 || <6.0.0")
85+
? [
86+
`
87+
/*eslint-disable no-undef -- description*/
88+
/*eslint-enable no-undef*/
89+
`,
90+
`
91+
/*eslint-disable no-undef,no-unused-vars -- description*/
92+
/*eslint-enable no-undef,no-unused-vars*/
93+
`,
94+
]
95+
: []),
7996
],
8097
invalid: [
8198
{
@@ -193,5 +210,28 @@ console.log();
193210
},
194211
],
195212
},
213+
// -- description
214+
...(semver.satisfies(eslintVersion, ">=7.0.0 || <6.0.0")
215+
? [
216+
{
217+
code: `
218+
{
219+
/*eslint-disable no-unused-vars -- description */
220+
}
221+
`,
222+
options: [{ allowWholeFile: true }],
223+
errors: [
224+
{
225+
message:
226+
"Requires 'eslint-enable' directive for 'no-unused-vars'.",
227+
line: 3,
228+
column: 18,
229+
endLine: 3,
230+
endColumn: 32,
231+
},
232+
],
233+
},
234+
]
235+
: []),
196236
],
197237
})

0 commit comments

Comments
 (0)