Skip to content

Commit ac19e81

Browse files
committed
New: disable-enable-pair rule
1 parent 1d0f6ae commit ac19e81

File tree

5 files changed

+532
-0
lines changed

5 files changed

+532
-0
lines changed

docs/rules/disable-enable-pair.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# requires a `eslint-enable` comment for every `eslint-disable` comment (eslint-comments/disable-enable-pair)
2+
3+
`eslint-disable` directive-comments disable ESLint rules in all lines preceded by the comment.
4+
If you forget `eslint-enable` directive-comment, you may overlook ESLint warnings unintentionally.
5+
6+
This rule warns `eslint-disable` directive-comments if the `eslint-enable` directive-comment for that does not exist.
7+
8+
## Rule Details
9+
10+
Examples of :-1: **incorrect** code for this rule:
11+
12+
```js
13+
/*eslint eslint-comments/disable-enable-pair: error */
14+
15+
/*eslint-disable no-undef, no-unused-vars */
16+
var foo = bar()
17+
```
18+
19+
```js
20+
/*eslint eslint-comments/disable-enable-pair: error */
21+
22+
/*eslint-disable no-undef, no-unused-vars */ "※ Requires 'eslint-enable' directive for 'no-undef'."
23+
var foo = bar()
24+
/*eslint-enable no-unused-vars */
25+
```
26+
27+
Examples of :+1: **correct** code for this rule:
28+
29+
```js
30+
/*eslint eslint-comments/disable-enable-pair: error */
31+
32+
/*eslint-disable no-undef, no-unused-vars */
33+
var foo = bar()
34+
/*eslint-enable no-undef, no-unused-vars */
35+
```
36+
37+
```js
38+
/*eslint eslint-comments/disable-enable-pair: error */
39+
40+
/*eslint-disable no-undef, no-unused-vars */
41+
var foo = bar()
42+
/*eslint-enable*/
43+
```

lib/disabled-area.js

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/**
2+
* @author Toru Nagashima
3+
* @copyright 2016 Toru Nagashima. All rights reserved.
4+
* See LICENSE file in root directory for full license.
5+
*/
6+
"use strict"
7+
8+
//------------------------------------------------------------------------------
9+
// Helpers
10+
//------------------------------------------------------------------------------
11+
12+
const COMMENT_DIRECTIVE = /^\s*(eslint-(?:en|dis)able(?:(?:-next)?-line)?)\s*(?:(\S[\s\S]+\S)\s*)?$/
13+
const DELIMITER = /[\s,]+/g
14+
const pool = new WeakMap()
15+
16+
/**
17+
* Checks `a` is less than `b` or `a` equals `b`.
18+
*
19+
* @param {{line: number, column: number}} a - A location to compare.
20+
* @param {{line: number, column: number}} b - Another location to compare.
21+
* @returns {boolean} `true` if `a` is less than `b` or `a` equals `b`.
22+
* @private
23+
*/
24+
function lte(a, b) {
25+
return a.line < b.line || (a.line === b.line && a.column <= b.column)
26+
}
27+
28+
//------------------------------------------------------------------------------
29+
// Exports
30+
//------------------------------------------------------------------------------
31+
32+
module.exports = class DisabledArea {
33+
/**
34+
* Get singleton instance for the given source code.
35+
*
36+
* @param {eslint.SourceCode} sourceCode - The source code to get.
37+
* @returns {DisabledArea} The singleton object for the source code.
38+
*/
39+
static get(sourceCode) {
40+
let retv = pool.get(sourceCode.ast)
41+
42+
if (retv == null) {
43+
retv = new DisabledArea()
44+
retv._scan(sourceCode)
45+
pool.set(sourceCode.ast, retv)
46+
}
47+
48+
return retv
49+
}
50+
51+
/**
52+
* Constructor.
53+
*/
54+
constructor() {
55+
this.areas = []
56+
this.duplicateDisableDirectives = []
57+
this.uselessEnableDirectives = []
58+
}
59+
60+
/**
61+
* Make disabled area.
62+
*
63+
* @param {Token} comment - The comment token to disable.
64+
* @param {object} location - The start location to disable.
65+
* @param {string[]|null} ruleIds - The ruleId names to disable.
66+
* @param {string} kind - The kind of disable-comments to show in reports.
67+
* @returns {void}
68+
* @private
69+
*/
70+
_disable(comment, location, ruleIds, kind) {
71+
if (ruleIds) {
72+
for (const ruleId of ruleIds) {
73+
if (this._isDisabled(ruleId, location)) {
74+
this.duplicateDisableDirectives.push({
75+
comment,
76+
ruleId,
77+
kind,
78+
})
79+
}
80+
81+
this.areas.push({
82+
comment,
83+
ruleId,
84+
kind,
85+
start: location,
86+
end: null,
87+
reported: false,
88+
})
89+
}
90+
}
91+
else {
92+
if (this._isDisabled(null, location)) {
93+
this.duplicateDisableDirectives.push({
94+
comment,
95+
ruleId: null,
96+
kind,
97+
})
98+
}
99+
100+
this.areas.push({
101+
comment,
102+
ruleId: null,
103+
kind,
104+
start: location,
105+
end: null,
106+
reported: false,
107+
})
108+
}
109+
}
110+
111+
/**
112+
* Close disabled area.
113+
*
114+
* @param {Token} comment - The comment token to enable.
115+
* @param {object} location - The start location to enable.
116+
* @param {string[]|null} ruleIds - The ruleId names to enable.
117+
* @returns {void}
118+
* @private
119+
*/
120+
_enable(comment, location, ruleIds) {
121+
if (ruleIds) {
122+
for (const ruleId of ruleIds) {
123+
let used = false
124+
125+
for (let i = this.areas.length - 1; i >= 0; --i) {
126+
const area = this.areas[i]
127+
128+
if (area.end === null && area.ruleId === ruleId) {
129+
area.end = location
130+
used = true
131+
break
132+
}
133+
}
134+
135+
if (!used) {
136+
this.uselessEnableDirectives.push({comment, ruleId})
137+
}
138+
}
139+
}
140+
else {
141+
let start = null
142+
143+
for (let i = this.areas.length - 1; i >= 0; --i) {
144+
const area = this.areas[i]
145+
146+
if (start !== null && area.start !== start) {
147+
break
148+
}
149+
if (area.end === null) {
150+
area.end = location
151+
start = area.start
152+
}
153+
}
154+
155+
if (start === null) {
156+
this.uselessEnableDirectives.push({comment, ruleId: null})
157+
}
158+
}
159+
}
160+
161+
/**
162+
* Gets the area of the given ruleId and location.
163+
*
164+
* @param {string|null} ruleId - The ruleId name to get.
165+
* @param {object} location - The location to get.
166+
* @returns {Generator<object>} The area of the given ruleId and location.
167+
* @private
168+
*/
169+
* _getAreas(ruleId, location) {
170+
for (let i = this.areas.length - 1; i >= 0; --i) {
171+
const area = this.areas[i]
172+
173+
if (lte(location, area.start)) {
174+
break
175+
}
176+
if ((area.ruleId === null || area.ruleId === ruleId) &&
177+
lte(area.start, location) &&
178+
(area.end === null || lte(location, area.end))
179+
) {
180+
yield area
181+
}
182+
}
183+
}
184+
185+
/**
186+
* Checks whether the given ruleId and location pair is disabled.
187+
*
188+
* @param {string|null} ruleId - The ruleId name to check.
189+
* @param {object} location - The location to check.
190+
* @returns {boolean} `true` if it's disabled.
191+
* @private
192+
*/
193+
_isDisabled(ruleId, location) {
194+
return !this._getAreas(ruleId, location).next().done
195+
}
196+
197+
/**
198+
* Scan the souce code and setup disabled area list.
199+
*
200+
* @param {eslint.SourceCode} sourceCode - The source code to scan.
201+
* @returns {void}
202+
* @private
203+
*/
204+
_scan(sourceCode) {
205+
for (const comment of sourceCode.getAllComments()) {
206+
const m = COMMENT_DIRECTIVE.exec(comment.value)
207+
if (m == null) {
208+
continue
209+
}
210+
const kind = m[1]
211+
const ruleIds = m[2] ? m[2].split(DELIMITER) : null
212+
213+
if (comment.type === "Block" && kind === "eslint-disable") {
214+
this._disable(comment, comment.loc.start, ruleIds, kind)
215+
}
216+
else if (comment.type === "Block" && kind === "eslint-enable") {
217+
this._enable(comment, comment.loc.start, ruleIds)
218+
}
219+
else if (
220+
comment.type === "Line" &&
221+
kind === "eslint-disable-line"
222+
) {
223+
const line = comment.loc.start.line
224+
const start = {line, column: 0}
225+
const end = {line: line + 1, column: -1}
226+
227+
this._disable(comment, start, ruleIds, kind)
228+
this._enable(comment, end, ruleIds)
229+
}
230+
else if (
231+
comment.type === "Line" &&
232+
kind === "eslint-disable-next-line"
233+
) {
234+
const line = comment.loc.start.line
235+
const start = {line: line + 1, column: 0}
236+
const end = {line: line + 2, column: -1}
237+
238+
this._disable(comment, start, ruleIds, kind)
239+
this._enable(comment, end, ruleIds)
240+
}
241+
}
242+
}
243+
244+
/**
245+
* Mark the area of the given ruleId and location as reported.
246+
*
247+
* @param {string} ruleId - The ruleId name to mark.
248+
* @param {object} location - The location to mark.
249+
* @returns {void}
250+
*/
251+
report(ruleId, location) {
252+
for (const area of this._getAreas(ruleId, location)) {
253+
area.reported = true
254+
return
255+
}
256+
}
257+
}

lib/rules/disable-enable-pair.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @author Toru Nagashima
3+
* @copyright 2016 Toru Nagashima. All rights reserved.
4+
* See LICENSE file in root directory for full license.
5+
*/
6+
"use strict"
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const DisabledArea = require("../disabled-area")
13+
const utils = require("../utils")
14+
15+
//------------------------------------------------------------------------------
16+
// Rule Definition
17+
//------------------------------------------------------------------------------
18+
19+
module.exports = {
20+
meta: {
21+
docs: {
22+
description: "requires a `eslint-enable` comment for every `eslint-disable` comment",
23+
category: "Best Practices",
24+
recommended: false,
25+
},
26+
fixable: false,
27+
schema: [],
28+
},
29+
30+
create(context) {
31+
const sourceCode = context.getSourceCode()
32+
const disabledArea = DisabledArea.get(sourceCode)
33+
34+
return {
35+
Program() {
36+
for (const area of disabledArea.areas) {
37+
if (area.end != null) {
38+
continue
39+
}
40+
41+
context.report({
42+
loc: utils.toRuleIdLocation(area.comment, area.ruleId),
43+
message: (area.ruleId) ?
44+
"Requires 'eslint-enable' directive for '{{ruleId}}'." :
45+
"Requires 'eslint-enable' directive.",
46+
data: area,
47+
})
48+
}
49+
},
50+
}
51+
},
52+
}

0 commit comments

Comments
 (0)