Skip to content

Commit d97ec74

Browse files
committed
Breaking: change no-unused-disable rule
1 parent a9c7954 commit d97ec74

File tree

4 files changed

+607
-314
lines changed

4 files changed

+607
-314
lines changed

lib/patch.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* @author Toru Nagashima <https://github.com/mysticatea>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
"use strict"
6+
7+
const path = require("path")
8+
const { toRuleIdLocation } = require("./utils")
9+
const needle = `${path.sep}eslint${path.sep}`
10+
const quotedName = /'(.+?)'/
11+
12+
/**
13+
* Get the severity of a given rule.
14+
* @param {object} config The config object to check.
15+
* @param {string} ruleId The rule ID to check.
16+
* @returns {number} The severity of the rule.
17+
*/
18+
function getSeverity(config, ruleId) {
19+
const rules = config && config.rules
20+
const ruleOptions = rules && rules[ruleId]
21+
const severity = Array.isArray(ruleOptions) ? ruleOptions[0] : ruleOptions
22+
23+
switch (severity) {
24+
case 2:
25+
case "error":
26+
return 2
27+
28+
case 1:
29+
case "warn":
30+
return 1
31+
32+
default:
33+
return 0
34+
}
35+
}
36+
37+
/**
38+
* Check whether a given message is a `reportUnusedDisableDirectives` error.
39+
* @param {Message} message The message.
40+
* @param {Comment} comment The comment which existed at the message location.
41+
* @returns {boolean} `true` if the message is a `reportUnusedDisableDirectives` error.
42+
*/
43+
function isUnusedDisableDirectiveError(message, comment) {
44+
return (
45+
!message.fatal &&
46+
!message.ruleId &&
47+
message.message.includes("eslint-disable") &&
48+
comment != null &&
49+
(comment.type === "Block" || comment.type === "Line")
50+
)
51+
}
52+
53+
/**
54+
* Create `eslint-comments/no-unused-disable` error.
55+
* @param {string} ruleId The ruleId.
56+
* @param {number} severity The severity of the rule.
57+
* @param {Message} message The original message.
58+
* @param {Comment} comment The directive comment.
59+
* @returns {Message} The created error.
60+
*/
61+
function createNoUnusedDisableError(ruleId, severity, message, comment) {
62+
const clone = Object.assign({}, message)
63+
const match = quotedName.exec(message.message)
64+
const targetRuleId = match && match[1]
65+
66+
clone.ruleId = ruleId
67+
clone.severity = severity
68+
clone.message = targetRuleId
69+
? `'${targetRuleId}' rule is disabled but never reported.`
70+
: "ESLint rules are disabled but never reported."
71+
72+
if (targetRuleId) {
73+
const loc = toRuleIdLocation(comment, targetRuleId)
74+
clone.line = loc.start.line
75+
clone.column = loc.start.column + 1
76+
clone.endLine = loc.end.line
77+
clone.endColumn = loc.end.column + 1
78+
} else {
79+
clone.endLine = comment.loc.end.line
80+
clone.endColumn = comment.loc.end.column + 1
81+
}
82+
83+
return clone
84+
}
85+
86+
/**
87+
* Convert `reportUnusedDisableDirectives` errors to `eslint-comments/no-unused-disable` errors.
88+
* @param {Message[]} messages The original messages.
89+
* @param {SourceCode} sourceCode The source code object.
90+
* @param {string} ruleId The rule ID to convert.
91+
* @param {number} severity The severity of the rule.
92+
* @param {boolean} keepAsIs The flag to keep original errors as is.
93+
* @returns {Message[]} The converted messages.
94+
*/
95+
function convert(messages, sourceCode, ruleId, severity, keepAsIs) {
96+
for (let i = messages.length - 1; i >= 0; --i) {
97+
const message = messages[i]
98+
const rangeStart = sourceCode.getIndexFromLoc(message) - 1
99+
const comment = sourceCode.getTokenByRangeStart(rangeStart, {
100+
includeComments: true,
101+
})
102+
if (!isUnusedDisableDirectiveError(message, comment)) {
103+
continue
104+
}
105+
106+
const newMessage = createNoUnusedDisableError(
107+
ruleId,
108+
severity,
109+
message,
110+
comment
111+
)
112+
113+
if (keepAsIs) {
114+
messages.splice(i + 1, 0, newMessage)
115+
} else {
116+
messages.splice(i, 1, newMessage)
117+
}
118+
}
119+
120+
return messages
121+
}
122+
123+
module.exports.patch = (ruleId = "eslint-comments/no-unused-disable") => {
124+
for (const eslintPath of new Set(
125+
Object.keys(require.cache)
126+
.filter(id => id.includes(needle))
127+
.map(id => id.slice(0, id.indexOf(needle) + needle.length))
128+
)) {
129+
const Linter = require(eslintPath).Linter
130+
const verify0 = Linter.prototype.verify
131+
132+
Object.defineProperty(Linter.prototype, "verify", {
133+
value: function verify(
134+
textOrSourceCode,
135+
config,
136+
filenameOrOptions
137+
) {
138+
const severity = getSeverity(config, ruleId)
139+
if (severity === 0) {
140+
return verify0.call(
141+
this,
142+
textOrSourceCode,
143+
config,
144+
filenameOrOptions
145+
)
146+
}
147+
148+
const options =
149+
typeof filenameOrOptions === "string"
150+
? { filename: filenameOrOptions }
151+
: filenameOrOptions || {}
152+
const reportUnusedDisableDirectives = Boolean(
153+
options.reportUnusedDisableDirectives
154+
)
155+
const messages = verify0.call(
156+
this,
157+
textOrSourceCode,
158+
config,
159+
Object.assign({}, options, {
160+
reportUnusedDisableDirectives: true,
161+
})
162+
)
163+
return convert(
164+
messages,
165+
this.getSourceCode(),
166+
ruleId,
167+
severity,
168+
reportUnusedDisableDirectives
169+
)
170+
},
171+
configurable: true,
172+
writable: true,
173+
})
174+
}
175+
}

lib/rules/no-unused-disable.js

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,30 @@
44
*/
55
"use strict"
66

7-
const DisabledArea = require("../disabled-area")
8-
const utils = require("../utils")
7+
// Patch `Linter#verify` to work.
8+
require("../patch").patch()
99

1010
module.exports = {
1111
meta: {
1212
docs: {
1313
description: "disallows unused `eslint-disable` comments",
1414
category: "Best Practices",
15-
recommended: true,
15+
recommended: false,
1616
url:
1717
"https://github.com/mysticatea/eslint-plugin-eslint-comments/blob/v2.0.2/docs/rules/no-unused-disable.md",
1818
},
1919
fixable: null,
2020
schema: [],
2121
},
2222

23-
create(context) {
24-
const linter = context.eslint || context._linter
25-
const originalReport = linter.report
26-
const sourceCode = context.getSourceCode()
27-
const disabledArea = DisabledArea.get(sourceCode)
28-
29-
// Override `report` method to mark disabled-area as reported.
30-
linter.report = function(ruleId, _severity, node, locationArg) {
31-
const location =
32-
typeof locationArg === "string"
33-
? node.loc.start
34-
: locationArg.start || locationArg
35-
36-
disabledArea.report(ruleId, location)
37-
38-
//eslint-disable-next-line prefer-rest-params
39-
originalReport.apply(this, arguments)
40-
}
41-
42-
/**
43-
* Reports the result.
44-
*
45-
* @returns {void}
46-
*/
47-
function report() {
48-
for (const area of disabledArea.areas) {
49-
if (area.reported) {
50-
continue
51-
}
52-
53-
context.report({
54-
loc: utils.toRuleIdLocation(area.comment, area.ruleId),
55-
message: area.ruleId
56-
? "'{{ruleId}}' rule is disabled but never reported."
57-
: "ESLint rules are disabled but never reported.",
58-
data: area,
59-
})
60-
}
61-
62-
// Restore
63-
linter.report = originalReport
64-
}
65-
66-
return {
67-
Program() {
68-
// Ensure that this listener is the last in `Program:exit` listeners
69-
// even if this rule was initialized before other rules.
70-
linter.on("Program:exit", report)
71-
},
72-
"Program:exit"() {
73-
// Ensure that at least one Program:exit listener exists so that the report listener will be called.
74-
},
75-
}
23+
create() {
24+
// This rule patches `Linter#verify` method and:
25+
//
26+
// 1. enables `reportUnusedDisableDirectives` option.
27+
// 2. verifies the code.
28+
// 3. converts `reportUnusedDisableDirectives` errors to `no-unused-disable` errors.
29+
//
30+
// So this rule itself does nothing.
31+
return {}
7632
},
7733
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"ignore": "^3.3.8"
1818
},
1919
"devDependencies": {
20+
"@types/node": "^10.0.4",
2021
"codecov": "^3.0.1",
22+
"cross-spawn": "^6.0.5",
2123
"eslint": "^4.19.1",
2224
"eslint-plugin-mysticatea": "^5.0.0-beta.5",
2325
"mocha": "^5.1.1",

0 commit comments

Comments
 (0)