eslint 9.25.1 → 9.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -39
- package/bin/eslint.js +19 -0
- package/lib/cli-engine/cli-engine.js +1 -1
- package/lib/cli.js +1 -1
- package/lib/eslint/eslint.js +6 -9
- package/lib/eslint/legacy-eslint.js +2 -2
- package/lib/languages/js/index.js +4 -3
- package/lib/linter/esquery.js +329 -0
- package/lib/linter/linter.js +8 -9
- package/lib/linter/node-event-generator.js +94 -251
- package/lib/mcp/mcp-server.js +66 -0
- package/lib/options.js +11 -0
- package/lib/rule-tester/rule-tester.js +14 -6
- package/lib/rules/eqeqeq.js +31 -8
- package/lib/rules/index.js +1 -1
- package/lib/rules/no-shadow-restricted-names.js +25 -2
- package/lib/rules/no-unused-expressions.js +7 -1
- package/lib/rules/utils/lazy-loading-rule-map.js +2 -2
- package/lib/services/processor-service.js +0 -1
- package/lib/shared/serialization.js +29 -6
- package/lib/shared/types.js +0 -17
- package/lib/types/index.d.ts +4 -2
- package/lib/types/rules.d.ts +14 -1
- package/package.json +7 -4
@@ -9,248 +9,28 @@
|
|
9
9
|
// Requirements
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
|
-
const
|
12
|
+
const { parse, matches } = require("./esquery");
|
13
13
|
|
14
|
-
|
14
|
+
//-----------------------------------------------------------------------------
|
15
15
|
// Typedefs
|
16
|
-
|
16
|
+
//-----------------------------------------------------------------------------
|
17
17
|
|
18
18
|
/**
|
19
|
-
*
|
20
|
-
* @typedef {Object} ASTSelector
|
21
|
-
* @property {string} rawSelector The string that was parsed into this selector
|
22
|
-
* @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
|
23
|
-
* @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
|
24
|
-
* @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
|
25
|
-
* or `null` if all node types could cause a match
|
26
|
-
* @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
|
27
|
-
* @property {number} identifierCount The total number of identifier queries in this selector
|
19
|
+
* @import { ESQueryParsedSelector } from "./esquery.js";
|
28
20
|
*/
|
29
21
|
|
30
|
-
|
22
|
+
//-----------------------------------------------------------------------------
|
31
23
|
// Helpers
|
32
|
-
|
33
|
-
|
34
|
-
/**
|
35
|
-
* Computes the union of one or more arrays
|
36
|
-
* @param {...any[]} arrays One or more arrays to union
|
37
|
-
* @returns {any[]} The union of the input arrays
|
38
|
-
*/
|
39
|
-
function union(...arrays) {
|
40
|
-
return [...new Set(arrays.flat())];
|
41
|
-
}
|
42
|
-
|
43
|
-
/**
|
44
|
-
* Computes the intersection of one or more arrays
|
45
|
-
* @param {...any[]} arrays One or more arrays to intersect
|
46
|
-
* @returns {any[]} The intersection of the input arrays
|
47
|
-
*/
|
48
|
-
function intersection(...arrays) {
|
49
|
-
if (arrays.length === 0) {
|
50
|
-
return [];
|
51
|
-
}
|
52
|
-
|
53
|
-
let result = [...new Set(arrays[0])];
|
54
|
-
|
55
|
-
for (const array of arrays.slice(1)) {
|
56
|
-
result = result.filter(x => array.includes(x));
|
57
|
-
}
|
58
|
-
return result;
|
59
|
-
}
|
60
|
-
|
61
|
-
/**
|
62
|
-
* Gets the possible types of a selector
|
63
|
-
* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
|
64
|
-
* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
|
65
|
-
*/
|
66
|
-
function getPossibleTypes(parsedSelector) {
|
67
|
-
switch (parsedSelector.type) {
|
68
|
-
case "identifier":
|
69
|
-
return [parsedSelector.value];
|
70
|
-
|
71
|
-
case "matches": {
|
72
|
-
const typesForComponents =
|
73
|
-
parsedSelector.selectors.map(getPossibleTypes);
|
74
|
-
|
75
|
-
if (typesForComponents.every(Boolean)) {
|
76
|
-
return union(...typesForComponents);
|
77
|
-
}
|
78
|
-
return null;
|
79
|
-
}
|
80
|
-
|
81
|
-
case "compound": {
|
82
|
-
const typesForComponents = parsedSelector.selectors
|
83
|
-
.map(getPossibleTypes)
|
84
|
-
.filter(typesForComponent => typesForComponent);
|
85
|
-
|
86
|
-
// If all of the components could match any type, then the compound could also match any type.
|
87
|
-
if (!typesForComponents.length) {
|
88
|
-
return null;
|
89
|
-
}
|
90
|
-
|
91
|
-
/*
|
92
|
-
* If at least one of the components could only match a particular type, the compound could only match
|
93
|
-
* the intersection of those types.
|
94
|
-
*/
|
95
|
-
return intersection(...typesForComponents);
|
96
|
-
}
|
97
|
-
|
98
|
-
case "child":
|
99
|
-
case "descendant":
|
100
|
-
case "sibling":
|
101
|
-
case "adjacent":
|
102
|
-
return getPossibleTypes(parsedSelector.right);
|
103
|
-
|
104
|
-
case "class":
|
105
|
-
if (parsedSelector.name === "function") {
|
106
|
-
return [
|
107
|
-
"FunctionDeclaration",
|
108
|
-
"FunctionExpression",
|
109
|
-
"ArrowFunctionExpression",
|
110
|
-
];
|
111
|
-
}
|
112
|
-
|
113
|
-
return null;
|
114
|
-
|
115
|
-
default:
|
116
|
-
return null;
|
117
|
-
}
|
118
|
-
}
|
119
|
-
|
120
|
-
/**
|
121
|
-
* Counts the number of class, pseudo-class, and attribute queries in this selector
|
122
|
-
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
|
123
|
-
* @returns {number} The number of class, pseudo-class, and attribute queries in this selector
|
124
|
-
*/
|
125
|
-
function countClassAttributes(parsedSelector) {
|
126
|
-
switch (parsedSelector.type) {
|
127
|
-
case "child":
|
128
|
-
case "descendant":
|
129
|
-
case "sibling":
|
130
|
-
case "adjacent":
|
131
|
-
return (
|
132
|
-
countClassAttributes(parsedSelector.left) +
|
133
|
-
countClassAttributes(parsedSelector.right)
|
134
|
-
);
|
135
|
-
|
136
|
-
case "compound":
|
137
|
-
case "not":
|
138
|
-
case "matches":
|
139
|
-
return parsedSelector.selectors.reduce(
|
140
|
-
(sum, childSelector) =>
|
141
|
-
sum + countClassAttributes(childSelector),
|
142
|
-
0,
|
143
|
-
);
|
144
|
-
|
145
|
-
case "attribute":
|
146
|
-
case "field":
|
147
|
-
case "nth-child":
|
148
|
-
case "nth-last-child":
|
149
|
-
return 1;
|
150
|
-
|
151
|
-
default:
|
152
|
-
return 0;
|
153
|
-
}
|
154
|
-
}
|
155
|
-
|
156
|
-
/**
|
157
|
-
* Counts the number of identifier queries in this selector
|
158
|
-
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
|
159
|
-
* @returns {number} The number of identifier queries
|
160
|
-
*/
|
161
|
-
function countIdentifiers(parsedSelector) {
|
162
|
-
switch (parsedSelector.type) {
|
163
|
-
case "child":
|
164
|
-
case "descendant":
|
165
|
-
case "sibling":
|
166
|
-
case "adjacent":
|
167
|
-
return (
|
168
|
-
countIdentifiers(parsedSelector.left) +
|
169
|
-
countIdentifiers(parsedSelector.right)
|
170
|
-
);
|
171
|
-
|
172
|
-
case "compound":
|
173
|
-
case "not":
|
174
|
-
case "matches":
|
175
|
-
return parsedSelector.selectors.reduce(
|
176
|
-
(sum, childSelector) => sum + countIdentifiers(childSelector),
|
177
|
-
0,
|
178
|
-
);
|
179
|
-
|
180
|
-
case "identifier":
|
181
|
-
return 1;
|
182
|
-
|
183
|
-
default:
|
184
|
-
return 0;
|
185
|
-
}
|
186
|
-
}
|
24
|
+
//-----------------------------------------------------------------------------
|
187
25
|
|
188
26
|
/**
|
189
|
-
* Compares
|
190
|
-
* @param {
|
191
|
-
* @param {
|
192
|
-
* @returns {number}
|
193
|
-
* a value less than 0 if selectorA is less specific than selectorB
|
194
|
-
* a value greater than 0 if selectorA is more specific than selectorB
|
195
|
-
* a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
|
196
|
-
* a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
|
27
|
+
* Compares two ESQuery selectors by specificity.
|
28
|
+
* @param {ESQueryParsedSelector} a The first selector to compare.
|
29
|
+
* @param {ESQueryParsedSelector} b The second selector to compare.
|
30
|
+
* @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
|
197
31
|
*/
|
198
|
-
function compareSpecificity(
|
199
|
-
return (
|
200
|
-
selectorA.attributeCount - selectorB.attributeCount ||
|
201
|
-
selectorA.identifierCount - selectorB.identifierCount ||
|
202
|
-
(selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1)
|
203
|
-
);
|
204
|
-
}
|
205
|
-
|
206
|
-
/**
|
207
|
-
* Parses a raw selector string, and throws a useful error if parsing fails.
|
208
|
-
* @param {string} rawSelector A raw AST selector
|
209
|
-
* @returns {Object} An object (from esquery) describing the matching behavior of this selector
|
210
|
-
* @throws {Error} An error if the selector is invalid
|
211
|
-
*/
|
212
|
-
function tryParseSelector(rawSelector) {
|
213
|
-
try {
|
214
|
-
return esquery.parse(rawSelector.replace(/:exit$/u, ""));
|
215
|
-
} catch (err) {
|
216
|
-
if (
|
217
|
-
err.location &&
|
218
|
-
err.location.start &&
|
219
|
-
typeof err.location.start.offset === "number"
|
220
|
-
) {
|
221
|
-
throw new SyntaxError(
|
222
|
-
`Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`,
|
223
|
-
);
|
224
|
-
}
|
225
|
-
throw err;
|
226
|
-
}
|
227
|
-
}
|
228
|
-
|
229
|
-
const selectorCache = new Map();
|
230
|
-
|
231
|
-
/**
|
232
|
-
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
|
233
|
-
* @param {string} rawSelector A raw AST selector
|
234
|
-
* @returns {ASTSelector} A selector descriptor
|
235
|
-
*/
|
236
|
-
function parseSelector(rawSelector) {
|
237
|
-
if (selectorCache.has(rawSelector)) {
|
238
|
-
return selectorCache.get(rawSelector);
|
239
|
-
}
|
240
|
-
|
241
|
-
const parsedSelector = tryParseSelector(rawSelector);
|
242
|
-
|
243
|
-
const result = {
|
244
|
-
rawSelector,
|
245
|
-
isExit: rawSelector.endsWith(":exit"),
|
246
|
-
parsedSelector,
|
247
|
-
listenerTypes: getPossibleTypes(parsedSelector),
|
248
|
-
attributeCount: countClassAttributes(parsedSelector),
|
249
|
-
identifierCount: countIdentifiers(parsedSelector),
|
250
|
-
};
|
251
|
-
|
252
|
-
selectorCache.set(rawSelector, result);
|
253
|
-
return result;
|
32
|
+
function compareSpecificity(a, b) {
|
33
|
+
return a.compare(b);
|
254
34
|
}
|
255
35
|
|
256
36
|
//------------------------------------------------------------------------------
|
@@ -270,6 +50,52 @@ function parseSelector(rawSelector) {
|
|
270
50
|
* ```
|
271
51
|
*/
|
272
52
|
class NodeEventGenerator {
|
53
|
+
/**
|
54
|
+
* The emitter to use during traversal.
|
55
|
+
* @type {SafeEmitter}
|
56
|
+
*/
|
57
|
+
emitter;
|
58
|
+
|
59
|
+
/**
|
60
|
+
* The options for `esquery` to use during matching.
|
61
|
+
* @type {ESQueryOptions}
|
62
|
+
*/
|
63
|
+
esqueryOptions;
|
64
|
+
|
65
|
+
/**
|
66
|
+
* The ancestry of the currently visited node.
|
67
|
+
* @type {ASTNode[]}
|
68
|
+
*/
|
69
|
+
currentAncestry = [];
|
70
|
+
|
71
|
+
/**
|
72
|
+
* A map of node type to selectors targeting that node type on the
|
73
|
+
* enter phase of traversal.
|
74
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
75
|
+
*/
|
76
|
+
enterSelectorsByNodeType = new Map();
|
77
|
+
|
78
|
+
/**
|
79
|
+
* A map of node type to selectors targeting that node type on the
|
80
|
+
* exit phase of traversal.
|
81
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
82
|
+
*/
|
83
|
+
exitSelectorsByNodeType = new Map();
|
84
|
+
|
85
|
+
/**
|
86
|
+
* An array of selectors that match any node type on the
|
87
|
+
* enter phase of traversal.
|
88
|
+
* @type {ESQueryParsedSelector[]}
|
89
|
+
*/
|
90
|
+
anyTypeEnterSelectors = [];
|
91
|
+
|
92
|
+
/**
|
93
|
+
* An array of selectors that match any node type on the
|
94
|
+
* exit phase of traversal.
|
95
|
+
* @type {ESQueryParsedSelector[]}
|
96
|
+
*/
|
97
|
+
anyTypeExitSelectors = [];
|
98
|
+
|
273
99
|
/**
|
274
100
|
* @param {SafeEmitter} emitter
|
275
101
|
* An SafeEmitter which is the destination of events. This emitter must already
|
@@ -281,21 +107,20 @@ class NodeEventGenerator {
|
|
281
107
|
constructor(emitter, esqueryOptions) {
|
282
108
|
this.emitter = emitter;
|
283
109
|
this.esqueryOptions = esqueryOptions;
|
284
|
-
this.currentAncestry = [];
|
285
|
-
this.enterSelectorsByNodeType = new Map();
|
286
|
-
this.exitSelectorsByNodeType = new Map();
|
287
|
-
this.anyTypeEnterSelectors = [];
|
288
|
-
this.anyTypeExitSelectors = [];
|
289
110
|
|
290
111
|
emitter.eventNames().forEach(rawSelector => {
|
291
|
-
const selector =
|
112
|
+
const selector = parse(rawSelector);
|
292
113
|
|
293
|
-
|
114
|
+
/*
|
115
|
+
* If this selector has identified specific node types,
|
116
|
+
* add it to the map for these node types for faster lookup.
|
117
|
+
*/
|
118
|
+
if (selector.nodeTypes) {
|
294
119
|
const typeMap = selector.isExit
|
295
120
|
? this.exitSelectorsByNodeType
|
296
121
|
: this.enterSelectorsByNodeType;
|
297
122
|
|
298
|
-
selector.
|
123
|
+
selector.nodeTypes.forEach(nodeType => {
|
299
124
|
if (!typeMap.has(nodeType)) {
|
300
125
|
typeMap.set(nodeType, []);
|
301
126
|
}
|
@@ -303,6 +128,13 @@ class NodeEventGenerator {
|
|
303
128
|
});
|
304
129
|
return;
|
305
130
|
}
|
131
|
+
|
132
|
+
/*
|
133
|
+
* Remaining selectors are added to the "any type" selectors
|
134
|
+
* list for the appropriate phase of traversal. This ensures
|
135
|
+
* that all selectors will still be applied even if no
|
136
|
+
* specific node type is matched.
|
137
|
+
*/
|
306
138
|
const selectors = selector.isExit
|
307
139
|
? this.anyTypeExitSelectors
|
308
140
|
: this.anyTypeEnterSelectors;
|
@@ -310,6 +142,7 @@ class NodeEventGenerator {
|
|
310
142
|
selectors.push(selector);
|
311
143
|
});
|
312
144
|
|
145
|
+
// sort all selectors by specificity for prioritizing call order
|
313
146
|
this.anyTypeEnterSelectors.sort(compareSpecificity);
|
314
147
|
this.anyTypeExitSelectors.sort(compareSpecificity);
|
315
148
|
this.enterSelectorsByNodeType.forEach(selectorList =>
|
@@ -323,19 +156,19 @@ class NodeEventGenerator {
|
|
323
156
|
/**
|
324
157
|
* Checks a selector against a node, and emits it if it matches
|
325
158
|
* @param {ASTNode} node The node to check
|
326
|
-
* @param {
|
159
|
+
* @param {ESQueryParsedSelector} selector An AST selector descriptor
|
327
160
|
* @returns {void}
|
328
161
|
*/
|
329
162
|
applySelector(node, selector) {
|
330
163
|
if (
|
331
|
-
|
164
|
+
matches(
|
332
165
|
node,
|
333
|
-
selector.
|
166
|
+
selector.root,
|
334
167
|
this.currentAncestry,
|
335
168
|
this.esqueryOptions,
|
336
169
|
)
|
337
170
|
) {
|
338
|
-
this.emitter.emit(selector.
|
171
|
+
this.emitter.emit(selector.source, node);
|
339
172
|
}
|
340
173
|
}
|
341
174
|
|
@@ -348,6 +181,11 @@ class NodeEventGenerator {
|
|
348
181
|
applySelectors(node, isExit) {
|
349
182
|
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
|
350
183
|
|
184
|
+
/*
|
185
|
+
* Get the selectors that may match this node. First, check
|
186
|
+
* to see if the node type has specific selectors,
|
187
|
+
* then gather the "any type" selectors.
|
188
|
+
*/
|
351
189
|
const selectorsByNodeType =
|
352
190
|
(isExit
|
353
191
|
? this.exitSelectorsByNodeType
|
@@ -361,19 +199,23 @@ class NodeEventGenerator {
|
|
361
199
|
* selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
|
362
200
|
* Iterate through each of them, applying selectors in the right order.
|
363
201
|
*/
|
364
|
-
let
|
202
|
+
let selectorsByNodeTypeIndex = 0;
|
365
203
|
let anyTypeSelectorsIndex = 0;
|
366
204
|
|
367
205
|
while (
|
368
|
-
|
206
|
+
selectorsByNodeTypeIndex < selectorsByNodeType.length ||
|
369
207
|
anyTypeSelectorsIndex < anyTypeSelectors.length
|
370
208
|
) {
|
209
|
+
/*
|
210
|
+
* If we've already exhausted the selectors for this node type,
|
211
|
+
* or if the next any type selector is more specific than the
|
212
|
+
* next selector for this node type, apply the any type selector.
|
213
|
+
*/
|
371
214
|
if (
|
372
|
-
|
215
|
+
selectorsByNodeTypeIndex >= selectorsByNodeType.length ||
|
373
216
|
(anyTypeSelectorsIndex < anyTypeSelectors.length &&
|
374
|
-
|
375
|
-
|
376
|
-
selectorsByNodeType[selectorsByTypeIndex],
|
217
|
+
anyTypeSelectors[anyTypeSelectorsIndex].compare(
|
218
|
+
selectorsByNodeType[selectorsByNodeTypeIndex],
|
377
219
|
) < 0)
|
378
220
|
) {
|
379
221
|
this.applySelector(
|
@@ -381,9 +223,10 @@ class NodeEventGenerator {
|
|
381
223
|
anyTypeSelectors[anyTypeSelectorsIndex++],
|
382
224
|
);
|
383
225
|
} else {
|
226
|
+
// otherwise apply the node type selector
|
384
227
|
this.applySelector(
|
385
228
|
node,
|
386
|
-
selectorsByNodeType[
|
229
|
+
selectorsByNodeType[selectorsByNodeTypeIndex++],
|
387
230
|
);
|
388
231
|
}
|
389
232
|
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview MCP Server for handling requests and responses to ESLint.
|
3
|
+
* @author Nicholas C. Zakas
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
//-----------------------------------------------------------------------------
|
9
|
+
// Requirements
|
10
|
+
//-----------------------------------------------------------------------------
|
11
|
+
|
12
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
13
|
+
const { z } = require("zod");
|
14
|
+
const { ESLint } = require("../eslint");
|
15
|
+
const pkg = require("../../package.json");
|
16
|
+
|
17
|
+
//-----------------------------------------------------------------------------
|
18
|
+
// Server
|
19
|
+
//-----------------------------------------------------------------------------
|
20
|
+
|
21
|
+
const mcpServer = new McpServer({
|
22
|
+
name: "ESLint",
|
23
|
+
version: pkg.version,
|
24
|
+
});
|
25
|
+
|
26
|
+
// Important: Cursor throws an error when `describe()` is used in the schema.
|
27
|
+
const filePathsSchema = {
|
28
|
+
filePaths: z.array(z.string().min(1)).nonempty(),
|
29
|
+
};
|
30
|
+
|
31
|
+
//-----------------------------------------------------------------------------
|
32
|
+
// Tools
|
33
|
+
//-----------------------------------------------------------------------------
|
34
|
+
|
35
|
+
mcpServer.tool(
|
36
|
+
"lint-files",
|
37
|
+
"Lint files using ESLint. You must provide a list of absolute file paths to the files you want to lint. The absolute file paths should be in the correct format for your operating system (e.g., forward slashes on Unix-like systems, backslashes on Windows).",
|
38
|
+
filePathsSchema,
|
39
|
+
async ({ filePaths }) => {
|
40
|
+
const eslint = new ESLint({
|
41
|
+
// enable lookup from file rather than from cwd
|
42
|
+
flags: ["unstable_config_lookup_from_file"],
|
43
|
+
});
|
44
|
+
|
45
|
+
const results = await eslint.lintFiles(filePaths);
|
46
|
+
const content = results.map(result => ({
|
47
|
+
type: "text",
|
48
|
+
text: JSON.stringify(result),
|
49
|
+
}));
|
50
|
+
|
51
|
+
content.unshift({
|
52
|
+
type: "text",
|
53
|
+
text: "Here are the results of running ESLint on the provided files:",
|
54
|
+
});
|
55
|
+
content.push({
|
56
|
+
type: "text",
|
57
|
+
text: "Do not automatically fix these issues. You must ask the user for confirmation before attempting to fix the issues found.",
|
58
|
+
});
|
59
|
+
|
60
|
+
return {
|
61
|
+
content,
|
62
|
+
};
|
63
|
+
},
|
64
|
+
);
|
65
|
+
|
66
|
+
module.exports = { mcpServer };
|
package/lib/options.js
CHANGED
@@ -213,6 +213,16 @@ module.exports = function (usingFlatConfig) {
|
|
213
213
|
};
|
214
214
|
}
|
215
215
|
|
216
|
+
let mcpFlag;
|
217
|
+
|
218
|
+
if (usingFlatConfig) {
|
219
|
+
mcpFlag = {
|
220
|
+
option: "mcp",
|
221
|
+
type: "Boolean",
|
222
|
+
description: "Start the ESLint MCP server",
|
223
|
+
};
|
224
|
+
}
|
225
|
+
|
216
226
|
return optionator({
|
217
227
|
prepend: "eslint [options] file.js [file.js] [dir]",
|
218
228
|
defaults: {
|
@@ -499,6 +509,7 @@ module.exports = function (usingFlatConfig) {
|
|
499
509
|
},
|
500
510
|
statsFlag,
|
501
511
|
flagFlag,
|
512
|
+
mcpFlag,
|
502
513
|
].filter(value => !!value),
|
503
514
|
});
|
504
515
|
};
|
@@ -39,9 +39,9 @@ const { SourceCode } = require("../languages/js/source-code");
|
|
39
39
|
// Typedefs
|
40
40
|
//------------------------------------------------------------------------------
|
41
41
|
|
42
|
+
/** @import { LanguageOptions, RuleDefinition } from "@eslint/core" */
|
43
|
+
|
42
44
|
/** @typedef {import("../shared/types").Parser} Parser */
|
43
|
-
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
|
44
|
-
/** @typedef {import("../types").Rule.RuleModule} Rule */
|
45
45
|
|
46
46
|
/**
|
47
47
|
* A test case that is expected to pass lint.
|
@@ -192,16 +192,24 @@ function cloneDeeplyExcludesParent(x) {
|
|
192
192
|
/**
|
193
193
|
* Freezes a given value deeply.
|
194
194
|
* @param {any} x A value to freeze.
|
195
|
+
* @param {Set<Object>} seenObjects Objects already seen during the traversal.
|
195
196
|
* @returns {void}
|
196
197
|
*/
|
197
|
-
function freezeDeeply(x) {
|
198
|
+
function freezeDeeply(x, seenObjects = new Set()) {
|
198
199
|
if (typeof x === "object" && x !== null) {
|
200
|
+
if (seenObjects.has(x)) {
|
201
|
+
return; // skip to avoid infinite recursion
|
202
|
+
}
|
203
|
+
seenObjects.add(x);
|
204
|
+
|
199
205
|
if (Array.isArray(x)) {
|
200
|
-
x.forEach(
|
206
|
+
x.forEach(element => {
|
207
|
+
freezeDeeply(element, seenObjects);
|
208
|
+
});
|
201
209
|
} else {
|
202
210
|
for (const key in x) {
|
203
211
|
if (key !== "parent" && hasOwnProperty(x, key)) {
|
204
|
-
freezeDeeply(x[key]);
|
212
|
+
freezeDeeply(x[key], seenObjects);
|
205
213
|
}
|
206
214
|
}
|
207
215
|
}
|
@@ -545,7 +553,7 @@ class RuleTester {
|
|
545
553
|
/**
|
546
554
|
* Adds a new rule test to execute.
|
547
555
|
* @param {string} ruleName The name of the rule to run.
|
548
|
-
* @param {
|
556
|
+
* @param {RuleDefinition} rule The rule to test.
|
549
557
|
* @param {{
|
550
558
|
* valid: (ValidTestCase | string)[],
|
551
559
|
* invalid: InvalidTestCase[]
|
package/lib/rules/eqeqeq.js
CHANGED
@@ -19,6 +19,7 @@ const astUtils = require("./utils/ast-utils");
|
|
19
19
|
module.exports = {
|
20
20
|
meta: {
|
21
21
|
type: "suggestion",
|
22
|
+
hasSuggestions: true,
|
22
23
|
|
23
24
|
docs: {
|
24
25
|
description: "Require the use of `===` and `!==`",
|
@@ -63,6 +64,8 @@ module.exports = {
|
|
63
64
|
messages: {
|
64
65
|
unexpected:
|
65
66
|
"Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'.",
|
67
|
+
replaceOperator:
|
68
|
+
"Use '{{expectedOperator}}' instead of '{{actualOperator}}'.",
|
66
69
|
},
|
67
70
|
},
|
68
71
|
|
@@ -138,22 +141,42 @@ module.exports = {
|
|
138
141
|
token => token.value === node.operator,
|
139
142
|
);
|
140
143
|
|
141
|
-
|
144
|
+
const commonReportParams = {
|
142
145
|
node,
|
143
146
|
loc: operatorToken.loc,
|
144
147
|
messageId: "unexpected",
|
145
148
|
data: { expectedOperator, actualOperator: node.operator },
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
+
};
|
150
|
+
|
151
|
+
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
|
152
|
+
context.report({
|
153
|
+
...commonReportParams,
|
154
|
+
fix(fixer) {
|
149
155
|
return fixer.replaceText(
|
150
156
|
operatorToken,
|
151
157
|
expectedOperator,
|
152
158
|
);
|
153
|
-
}
|
154
|
-
|
155
|
-
|
156
|
-
|
159
|
+
},
|
160
|
+
});
|
161
|
+
} else {
|
162
|
+
context.report({
|
163
|
+
...commonReportParams,
|
164
|
+
suggest: [
|
165
|
+
{
|
166
|
+
messageId: "replaceOperator",
|
167
|
+
data: {
|
168
|
+
expectedOperator,
|
169
|
+
actualOperator: node.operator,
|
170
|
+
},
|
171
|
+
fix: fixer =>
|
172
|
+
fixer.replaceText(
|
173
|
+
operatorToken,
|
174
|
+
expectedOperator,
|
175
|
+
),
|
176
|
+
},
|
177
|
+
],
|
178
|
+
});
|
179
|
+
}
|
157
180
|
}
|
158
181
|
|
159
182
|
return {
|
package/lib/rules/index.js
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
|
11
11
|
const { LazyLoadingRuleMap } = require("./utils/lazy-loading-rule-map");
|
12
12
|
|
13
|
-
/** @type {Map<string, import("../
|
13
|
+
/** @type {Map<string, import("../types").Rule.RuleModule>} */
|
14
14
|
module.exports = new LazyLoadingRuleMap(
|
15
15
|
Object.entries({
|
16
16
|
"accessor-pairs": () => require("./accessor-pairs"),
|