eslint 9.0.0-beta.1 → 9.0.0-beta.2
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 +17 -5
- package/lib/config/rule-validator.js +15 -2
- package/lib/eslint/eslint.js +8 -1
- package/lib/linter/linter.js +16 -0
- package/lib/rules/complexity.js +13 -0
- package/lib/rules/no-misleading-character-class.js +110 -63
- package/lib/rules/no-restricted-imports.js +183 -47
- package/lib/rules/no-unused-vars.js +14 -1
- package/lib/rules/utils/char-source.js +240 -0
- package/package.json +2 -2
package/README.md
CHANGED
@@ -127,6 +127,18 @@ In other cases (including if rules need to warn on more or fewer cases due to ne
|
|
127
127
|
|
128
128
|
Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/latest/contribute). Until then, please use the appropriate parser and plugin(s) for your experimental feature.
|
129
129
|
|
130
|
+
### Which Node.js versions does ESLint support?
|
131
|
+
|
132
|
+
ESLint updates the supported Node.js versions with each major release of ESLint. At that time, ESLint's supported Node.js versions are updated to be:
|
133
|
+
|
134
|
+
1. The most recent maintenance release of Node.js
|
135
|
+
1. The lowest minor version of the Node.js LTS release that includes the features the ESLint team wants to use.
|
136
|
+
1. The Node.js Current release
|
137
|
+
|
138
|
+
ESLint is also expected to work with Node.js versions released after the Node.js Current release.
|
139
|
+
|
140
|
+
Refer to the [Quick Start Guide](https://eslint.org/docs/latest/use/getting-started#prerequisites) for the officially supported Node.js versions for a given ESLint release.
|
141
|
+
|
130
142
|
### Where to ask for help?
|
131
143
|
|
132
144
|
Open a [discussion](https://github.com/eslint/eslint/discussions) or stop by our [Discord server](https://eslint.org/chat).
|
@@ -213,6 +225,11 @@ The people who manage releases, review feature requests, and meet regularly to e
|
|
213
225
|
Nicholas C. Zakas
|
214
226
|
</a>
|
215
227
|
</td><td align="center" valign="top" width="11%">
|
228
|
+
<a href="https://github.com/fasttime">
|
229
|
+
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
|
230
|
+
Francesco Trotta
|
231
|
+
</a>
|
232
|
+
</td><td align="center" valign="top" width="11%">
|
216
233
|
<a href="https://github.com/mdjermanovic">
|
217
234
|
<img src="https://github.com/mdjermanovic.png?s=75" width="75" height="75" alt="Milos Djermanovic's Avatar"><br />
|
218
235
|
Milos Djermanovic
|
@@ -250,11 +267,6 @@ Bryan Mishkin
|
|
250
267
|
Josh Goldberg ✨
|
251
268
|
</a>
|
252
269
|
</td><td align="center" valign="top" width="11%">
|
253
|
-
<a href="https://github.com/fasttime">
|
254
|
-
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
|
255
|
-
Francesco Trotta
|
256
|
-
</a>
|
257
|
-
</td><td align="center" valign="top" width="11%">
|
258
270
|
<a href="https://github.com/Tanujkanti4441">
|
259
271
|
<img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
|
260
272
|
Tanuj Kanti
|
@@ -167,9 +167,22 @@ class RuleValidator {
|
|
167
167
|
validateRule(ruleOptions.slice(1));
|
168
168
|
|
169
169
|
if (validateRule.errors) {
|
170
|
-
throw new Error(`Key "rules": Key "${ruleId}"
|
170
|
+
throw new Error(`Key "rules": Key "${ruleId}":\n${
|
171
171
|
validateRule.errors.map(
|
172
|
-
error =>
|
172
|
+
error => {
|
173
|
+
if (
|
174
|
+
error.keyword === "additionalProperties" &&
|
175
|
+
error.schema === false &&
|
176
|
+
typeof error.parentSchema?.properties === "object" &&
|
177
|
+
typeof error.params?.additionalProperty === "string"
|
178
|
+
) {
|
179
|
+
const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`);
|
180
|
+
|
181
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
|
182
|
+
}
|
183
|
+
|
184
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
|
185
|
+
}
|
173
186
|
).join("")
|
174
187
|
}`);
|
175
188
|
}
|
package/lib/eslint/eslint.js
CHANGED
@@ -838,6 +838,7 @@ class ESLint {
|
|
838
838
|
configs,
|
839
839
|
errorOnUnmatchedPattern
|
840
840
|
});
|
841
|
+
const controller = new AbortController();
|
841
842
|
|
842
843
|
debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
|
843
844
|
|
@@ -906,9 +907,12 @@ class ESLint {
|
|
906
907
|
fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
|
907
908
|
}
|
908
909
|
|
909
|
-
return fs.readFile(filePath, "utf8")
|
910
|
+
return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
|
910
911
|
.then(text => {
|
911
912
|
|
913
|
+
// fail immediately if an error occurred in another file
|
914
|
+
controller.signal.throwIfAborted();
|
915
|
+
|
912
916
|
// do the linting
|
913
917
|
const result = verifyText({
|
914
918
|
text,
|
@@ -932,6 +936,9 @@ class ESLint {
|
|
932
936
|
}
|
933
937
|
|
934
938
|
return result;
|
939
|
+
}).catch(error => {
|
940
|
+
controller.abort(error);
|
941
|
+
throw error;
|
935
942
|
});
|
936
943
|
|
937
944
|
})
|
package/lib/linter/linter.js
CHANGED
@@ -439,6 +439,14 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
|
|
439
439
|
return;
|
440
440
|
}
|
441
441
|
|
442
|
+
if (Object.hasOwn(configuredRules, name)) {
|
443
|
+
problems.push(createLintingProblem({
|
444
|
+
message: `Rule "${name}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
|
445
|
+
loc: comment.loc
|
446
|
+
}));
|
447
|
+
return;
|
448
|
+
}
|
449
|
+
|
442
450
|
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
443
451
|
|
444
452
|
/*
|
@@ -1706,6 +1714,14 @@ class Linter {
|
|
1706
1714
|
return;
|
1707
1715
|
}
|
1708
1716
|
|
1717
|
+
if (Object.hasOwn(mergedInlineConfig.rules, ruleId)) {
|
1718
|
+
inlineConfigProblems.push(createLintingProblem({
|
1719
|
+
message: `Rule "${ruleId}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
|
1720
|
+
loc: node.loc
|
1721
|
+
}));
|
1722
|
+
return;
|
1723
|
+
}
|
1724
|
+
|
1709
1725
|
try {
|
1710
1726
|
|
1711
1727
|
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
package/lib/rules/complexity.js
CHANGED
@@ -109,6 +109,7 @@ module.exports = {
|
|
109
109
|
IfStatement: increaseComplexity,
|
110
110
|
WhileStatement: increaseComplexity,
|
111
111
|
DoWhileStatement: increaseComplexity,
|
112
|
+
AssignmentPattern: increaseComplexity,
|
112
113
|
|
113
114
|
// Avoid `default`
|
114
115
|
"SwitchCase[test]": increaseComplexity,
|
@@ -120,6 +121,18 @@ module.exports = {
|
|
120
121
|
}
|
121
122
|
},
|
122
123
|
|
124
|
+
MemberExpression(node) {
|
125
|
+
if (node.optional === true) {
|
126
|
+
increaseComplexity();
|
127
|
+
}
|
128
|
+
},
|
129
|
+
|
130
|
+
CallExpression(node) {
|
131
|
+
if (node.optional === true) {
|
132
|
+
increaseComplexity();
|
133
|
+
}
|
134
|
+
},
|
135
|
+
|
123
136
|
onCodePathEnd(codePath, node) {
|
124
137
|
const complexity = complexities.pop();
|
125
138
|
|
@@ -3,11 +3,18 @@
|
|
3
3
|
*/
|
4
4
|
"use strict";
|
5
5
|
|
6
|
-
const {
|
6
|
+
const {
|
7
|
+
CALL,
|
8
|
+
CONSTRUCT,
|
9
|
+
ReferenceTracker,
|
10
|
+
getStaticValue,
|
11
|
+
getStringIfConstant
|
12
|
+
} = require("@eslint-community/eslint-utils");
|
7
13
|
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
|
8
14
|
const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode");
|
9
15
|
const astUtils = require("./utils/ast-utils.js");
|
10
16
|
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
|
17
|
+
const { parseStringLiteral, parseTemplateToken } = require("./utils/char-source");
|
11
18
|
|
12
19
|
//------------------------------------------------------------------------------
|
13
20
|
// Helpers
|
@@ -193,6 +200,33 @@ const findCharacterSequences = {
|
|
193
200
|
|
194
201
|
const kinds = Object.keys(findCharacterSequences);
|
195
202
|
|
203
|
+
/**
|
204
|
+
* Gets the value of the given node if it's a static value other than a regular expression object,
|
205
|
+
* or the node's `regex` property.
|
206
|
+
* The purpose of this method is to provide a replacement for `getStaticValue` in environments where certain regular expressions cannot be evaluated.
|
207
|
+
* A known example is Node.js 18 which does not support the `v` flag.
|
208
|
+
* Calling `getStaticValue` on a regular expression node with the `v` flag on Node.js 18 always returns `null`.
|
209
|
+
* A limitation of this method is that it can only detect a regular expression if the specified node is itself a regular expression literal node.
|
210
|
+
* @param {ASTNode | undefined} node The node to be inspected.
|
211
|
+
* @param {Scope} initialScope Scope to start finding variables. This function tries to resolve identifier references which are in the given scope.
|
212
|
+
* @returns {{ value: any } | { regex: { pattern: string, flags: string } } | null} The static value of the node, or `null`.
|
213
|
+
*/
|
214
|
+
function getStaticValueOrRegex(node, initialScope) {
|
215
|
+
if (!node) {
|
216
|
+
return null;
|
217
|
+
}
|
218
|
+
if (node.type === "Literal" && node.regex) {
|
219
|
+
return { regex: node.regex };
|
220
|
+
}
|
221
|
+
|
222
|
+
const staticValue = getStaticValue(node, initialScope);
|
223
|
+
|
224
|
+
if (staticValue?.value instanceof RegExp) {
|
225
|
+
return null;
|
226
|
+
}
|
227
|
+
return staticValue;
|
228
|
+
}
|
229
|
+
|
196
230
|
//------------------------------------------------------------------------------
|
197
231
|
// Rule Definition
|
198
232
|
//------------------------------------------------------------------------------
|
@@ -225,62 +259,7 @@ module.exports = {
|
|
225
259
|
create(context) {
|
226
260
|
const sourceCode = context.sourceCode;
|
227
261
|
const parser = new RegExpParser();
|
228
|
-
|
229
|
-
/**
|
230
|
-
* Generates a granular loc for context.report, if directly calculable.
|
231
|
-
* @param {Character[]} chars Individual characters being reported on.
|
232
|
-
* @param {Node} node Parent string node to report within.
|
233
|
-
* @returns {Object | null} Granular loc for context.report, if directly calculable.
|
234
|
-
* @see https://github.com/eslint/eslint/pull/17515
|
235
|
-
*/
|
236
|
-
function generateReportLocation(chars, node) {
|
237
|
-
|
238
|
-
// Limit to to literals and expression-less templates with raw values === their value.
|
239
|
-
switch (node.type) {
|
240
|
-
case "TemplateLiteral":
|
241
|
-
if (node.expressions.length || sourceCode.getText(node).slice(1, -1) !== node.quasis[0].value.cooked) {
|
242
|
-
return null;
|
243
|
-
}
|
244
|
-
break;
|
245
|
-
|
246
|
-
case "Literal":
|
247
|
-
if (typeof node.value === "string" && node.value !== node.raw.slice(1, -1)) {
|
248
|
-
return null;
|
249
|
-
}
|
250
|
-
break;
|
251
|
-
|
252
|
-
default:
|
253
|
-
return null;
|
254
|
-
}
|
255
|
-
|
256
|
-
return {
|
257
|
-
start: sourceCode.getLocFromIndex(node.range[0] + 1 + chars[0].start),
|
258
|
-
end: sourceCode.getLocFromIndex(node.range[0] + 1 + chars.at(-1).end)
|
259
|
-
};
|
260
|
-
}
|
261
|
-
|
262
|
-
/**
|
263
|
-
* Finds the report loc(s) for a range of matches.
|
264
|
-
* @param {Character[][]} matches Characters that should trigger a report.
|
265
|
-
* @param {Node} node The node to report.
|
266
|
-
* @returns {Object | null} Node loc(s) for context.report.
|
267
|
-
*/
|
268
|
-
function getNodeReportLocations(matches, node) {
|
269
|
-
const locs = [];
|
270
|
-
|
271
|
-
for (const chars of matches) {
|
272
|
-
const loc = generateReportLocation(chars, node);
|
273
|
-
|
274
|
-
// If a report can't match to a range, don't report any others
|
275
|
-
if (!loc) {
|
276
|
-
return [node.loc];
|
277
|
-
}
|
278
|
-
|
279
|
-
locs.push(loc);
|
280
|
-
}
|
281
|
-
|
282
|
-
return locs;
|
283
|
-
}
|
262
|
+
const checkedPatternNodes = new Set();
|
284
263
|
|
285
264
|
/**
|
286
265
|
* Verify a given regular expression.
|
@@ -320,12 +299,58 @@ module.exports = {
|
|
320
299
|
} else {
|
321
300
|
foundKindMatches.set(kind, [...findCharacterSequences[kind](chars)]);
|
322
301
|
}
|
323
|
-
|
324
302
|
}
|
325
303
|
}
|
326
304
|
}
|
327
305
|
});
|
328
306
|
|
307
|
+
let codeUnits = null;
|
308
|
+
|
309
|
+
/**
|
310
|
+
* Finds the report loc(s) for a range of matches.
|
311
|
+
* Only literals and expression-less templates generate granular errors.
|
312
|
+
* @param {Character[][]} matches Lists of individual characters being reported on.
|
313
|
+
* @returns {Location[]} locs for context.report.
|
314
|
+
* @see https://github.com/eslint/eslint/pull/17515
|
315
|
+
*/
|
316
|
+
function getNodeReportLocations(matches) {
|
317
|
+
if (!astUtils.isStaticTemplateLiteral(node) && node.type !== "Literal") {
|
318
|
+
return matches.length ? [node.loc] : [];
|
319
|
+
}
|
320
|
+
return matches.map(chars => {
|
321
|
+
const firstIndex = chars[0].start;
|
322
|
+
const lastIndex = chars.at(-1).end - 1;
|
323
|
+
let start;
|
324
|
+
let end;
|
325
|
+
|
326
|
+
if (node.type === "TemplateLiteral") {
|
327
|
+
const source = sourceCode.getText(node);
|
328
|
+
const offset = node.range[0];
|
329
|
+
|
330
|
+
codeUnits ??= parseTemplateToken(source);
|
331
|
+
start = offset + codeUnits[firstIndex].start;
|
332
|
+
end = offset + codeUnits[lastIndex].end;
|
333
|
+
} else if (typeof node.value === "string") { // String Literal
|
334
|
+
const source = node.raw;
|
335
|
+
const offset = node.range[0];
|
336
|
+
|
337
|
+
codeUnits ??= parseStringLiteral(source);
|
338
|
+
start = offset + codeUnits[firstIndex].start;
|
339
|
+
end = offset + codeUnits[lastIndex].end;
|
340
|
+
} else { // RegExp Literal
|
341
|
+
const offset = node.range[0] + 1; // Add 1 to skip the leading slash.
|
342
|
+
|
343
|
+
start = offset + firstIndex;
|
344
|
+
end = offset + lastIndex + 1;
|
345
|
+
}
|
346
|
+
|
347
|
+
return {
|
348
|
+
start: sourceCode.getLocFromIndex(start),
|
349
|
+
end: sourceCode.getLocFromIndex(end)
|
350
|
+
};
|
351
|
+
});
|
352
|
+
}
|
353
|
+
|
329
354
|
for (const [kind, matches] of foundKindMatches) {
|
330
355
|
let suggest;
|
331
356
|
|
@@ -336,7 +361,7 @@ module.exports = {
|
|
336
361
|
}];
|
337
362
|
}
|
338
363
|
|
339
|
-
const locs = getNodeReportLocations(matches
|
364
|
+
const locs = getNodeReportLocations(matches);
|
340
365
|
|
341
366
|
for (const loc of locs) {
|
342
367
|
context.report({
|
@@ -351,6 +376,9 @@ module.exports = {
|
|
351
376
|
|
352
377
|
return {
|
353
378
|
"Literal[regex]"(node) {
|
379
|
+
if (checkedPatternNodes.has(node)) {
|
380
|
+
return;
|
381
|
+
}
|
354
382
|
verify(node, node.regex.pattern, node.regex.flags, fixer => {
|
355
383
|
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) {
|
356
384
|
return null;
|
@@ -371,12 +399,31 @@ module.exports = {
|
|
371
399
|
for (const { node: refNode } of tracker.iterateGlobalReferences({
|
372
400
|
RegExp: { [CALL]: true, [CONSTRUCT]: true }
|
373
401
|
})) {
|
402
|
+
let pattern, flags;
|
374
403
|
const [patternNode, flagsNode] = refNode.arguments;
|
375
|
-
const
|
376
|
-
|
404
|
+
const evaluatedPattern = getStaticValueOrRegex(patternNode, scope);
|
405
|
+
|
406
|
+
if (!evaluatedPattern) {
|
407
|
+
continue;
|
408
|
+
}
|
409
|
+
if (flagsNode) {
|
410
|
+
if (evaluatedPattern.regex) {
|
411
|
+
pattern = evaluatedPattern.regex.pattern;
|
412
|
+
checkedPatternNodes.add(patternNode);
|
413
|
+
} else {
|
414
|
+
pattern = String(evaluatedPattern.value);
|
415
|
+
}
|
416
|
+
flags = getStringIfConstant(flagsNode, scope);
|
417
|
+
} else {
|
418
|
+
if (evaluatedPattern.regex) {
|
419
|
+
continue;
|
420
|
+
}
|
421
|
+
pattern = String(evaluatedPattern.value);
|
422
|
+
flags = "";
|
423
|
+
}
|
377
424
|
|
378
|
-
if (typeof
|
379
|
-
verify(patternNode, pattern, flags
|
425
|
+
if (typeof flags === "string") {
|
426
|
+
verify(patternNode, pattern, flags, fixer => {
|
380
427
|
|
381
428
|
if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) {
|
382
429
|
return null;
|
@@ -34,10 +34,17 @@ const arrayOfStringsOrObjects = {
|
|
34
34
|
items: {
|
35
35
|
type: "string"
|
36
36
|
}
|
37
|
+
},
|
38
|
+
allowImportNames: {
|
39
|
+
type: "array",
|
40
|
+
items: {
|
41
|
+
type: "string"
|
42
|
+
}
|
37
43
|
}
|
38
44
|
},
|
39
45
|
additionalProperties: false,
|
40
|
-
required: ["name"]
|
46
|
+
required: ["name"],
|
47
|
+
not: { required: ["importNames", "allowImportNames"] }
|
41
48
|
}
|
42
49
|
]
|
43
50
|
},
|
@@ -66,6 +73,14 @@ const arrayOfStringsOrObjectPatterns = {
|
|
66
73
|
minItems: 1,
|
67
74
|
uniqueItems: true
|
68
75
|
},
|
76
|
+
allowImportNames: {
|
77
|
+
type: "array",
|
78
|
+
items: {
|
79
|
+
type: "string"
|
80
|
+
},
|
81
|
+
minItems: 1,
|
82
|
+
uniqueItems: true
|
83
|
+
},
|
69
84
|
group: {
|
70
85
|
type: "array",
|
71
86
|
items: {
|
@@ -77,6 +92,9 @@ const arrayOfStringsOrObjectPatterns = {
|
|
77
92
|
importNamePattern: {
|
78
93
|
type: "string"
|
79
94
|
},
|
95
|
+
allowImportNamePattern: {
|
96
|
+
type: "string"
|
97
|
+
},
|
80
98
|
message: {
|
81
99
|
type: "string",
|
82
100
|
minLength: 1
|
@@ -86,7 +104,16 @@ const arrayOfStringsOrObjectPatterns = {
|
|
86
104
|
}
|
87
105
|
},
|
88
106
|
additionalProperties: false,
|
89
|
-
required: ["group"]
|
107
|
+
required: ["group"],
|
108
|
+
not: {
|
109
|
+
anyOf: [
|
110
|
+
{ required: ["importNames", "allowImportNames"] },
|
111
|
+
{ required: ["importNamePattern", "allowImportNamePattern"] },
|
112
|
+
{ required: ["importNames", "allowImportNamePattern"] },
|
113
|
+
{ required: ["importNamePattern", "allowImportNames"] },
|
114
|
+
{ required: ["allowImportNames", "allowImportNamePattern"] }
|
115
|
+
]
|
116
|
+
}
|
90
117
|
},
|
91
118
|
uniqueItems: true
|
92
119
|
}
|
@@ -131,7 +158,23 @@ module.exports = {
|
|
131
158
|
|
132
159
|
importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
|
133
160
|
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
134
|
-
importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
|
161
|
+
importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}",
|
162
|
+
|
163
|
+
allowedImportName: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed.",
|
164
|
+
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
165
|
+
allowedImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed. {{customMessage}}",
|
166
|
+
|
167
|
+
everythingWithAllowImportNames: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed.",
|
168
|
+
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
169
|
+
everythingWithAllowImportNamesAndCustomMessage: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed. {{customMessage}}",
|
170
|
+
|
171
|
+
allowedImportNamePattern: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'.",
|
172
|
+
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
173
|
+
allowedImportNamePatternWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'. {{customMessage}}",
|
174
|
+
|
175
|
+
everythingWithAllowedImportNamePattern: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed.",
|
176
|
+
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
|
177
|
+
everythingWithAllowedImportNamePatternWithCustomMessage: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed. {{customMessage}}"
|
135
178
|
},
|
136
179
|
|
137
180
|
schema: {
|
@@ -175,7 +218,8 @@ module.exports = {
|
|
175
218
|
} else {
|
176
219
|
memo[path].push({
|
177
220
|
message: importSource.message,
|
178
|
-
importNames: importSource.importNames
|
221
|
+
importNames: importSource.importNames,
|
222
|
+
allowImportNames: importSource.allowImportNames
|
179
223
|
});
|
180
224
|
}
|
181
225
|
return memo;
|
@@ -190,12 +234,18 @@ module.exports = {
|
|
190
234
|
}
|
191
235
|
|
192
236
|
// relative paths are supported for this rule
|
193
|
-
const restrictedPatternGroups = restrictedPatterns.map(
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
237
|
+
const restrictedPatternGroups = restrictedPatterns.map(
|
238
|
+
({ group, message, caseSensitive, importNames, importNamePattern, allowImportNames, allowImportNamePattern }) => (
|
239
|
+
{
|
240
|
+
matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
|
241
|
+
customMessage: message,
|
242
|
+
importNames,
|
243
|
+
importNamePattern,
|
244
|
+
allowImportNames,
|
245
|
+
allowImportNamePattern
|
246
|
+
}
|
247
|
+
)
|
248
|
+
);
|
199
249
|
|
200
250
|
// if no imports are restricted we don't need to check
|
201
251
|
if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
|
@@ -218,42 +268,9 @@ module.exports = {
|
|
218
268
|
groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => {
|
219
269
|
const customMessage = restrictedPathEntry.message;
|
220
270
|
const restrictedImportNames = restrictedPathEntry.importNames;
|
271
|
+
const allowedImportNames = restrictedPathEntry.allowImportNames;
|
221
272
|
|
222
|
-
if (restrictedImportNames) {
|
223
|
-
if (importNames.has("*")) {
|
224
|
-
const specifierData = importNames.get("*")[0];
|
225
|
-
|
226
|
-
context.report({
|
227
|
-
node,
|
228
|
-
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
|
229
|
-
loc: specifierData.loc,
|
230
|
-
data: {
|
231
|
-
importSource,
|
232
|
-
importNames: restrictedImportNames,
|
233
|
-
customMessage
|
234
|
-
}
|
235
|
-
});
|
236
|
-
}
|
237
|
-
|
238
|
-
restrictedImportNames.forEach(importName => {
|
239
|
-
if (importNames.has(importName)) {
|
240
|
-
const specifiers = importNames.get(importName);
|
241
|
-
|
242
|
-
specifiers.forEach(specifier => {
|
243
|
-
context.report({
|
244
|
-
node,
|
245
|
-
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
|
246
|
-
loc: specifier.loc,
|
247
|
-
data: {
|
248
|
-
importSource,
|
249
|
-
customMessage,
|
250
|
-
importName
|
251
|
-
}
|
252
|
-
});
|
253
|
-
});
|
254
|
-
}
|
255
|
-
});
|
256
|
-
} else {
|
273
|
+
if (!restrictedImportNames && !allowedImportNames) {
|
257
274
|
context.report({
|
258
275
|
node,
|
259
276
|
messageId: customMessage ? "pathWithCustomMessage" : "path",
|
@@ -262,7 +279,72 @@ module.exports = {
|
|
262
279
|
customMessage
|
263
280
|
}
|
264
281
|
});
|
282
|
+
|
283
|
+
return;
|
265
284
|
}
|
285
|
+
|
286
|
+
importNames.forEach((specifiers, importName) => {
|
287
|
+
if (importName === "*") {
|
288
|
+
const [specifier] = specifiers;
|
289
|
+
|
290
|
+
if (restrictedImportNames) {
|
291
|
+
context.report({
|
292
|
+
node,
|
293
|
+
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
|
294
|
+
loc: specifier.loc,
|
295
|
+
data: {
|
296
|
+
importSource,
|
297
|
+
importNames: restrictedImportNames,
|
298
|
+
customMessage
|
299
|
+
}
|
300
|
+
});
|
301
|
+
} else if (allowedImportNames) {
|
302
|
+
context.report({
|
303
|
+
node,
|
304
|
+
messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames",
|
305
|
+
loc: specifier.loc,
|
306
|
+
data: {
|
307
|
+
importSource,
|
308
|
+
allowedImportNames,
|
309
|
+
customMessage
|
310
|
+
}
|
311
|
+
});
|
312
|
+
}
|
313
|
+
|
314
|
+
return;
|
315
|
+
}
|
316
|
+
|
317
|
+
if (restrictedImportNames && restrictedImportNames.includes(importName)) {
|
318
|
+
specifiers.forEach(specifier => {
|
319
|
+
context.report({
|
320
|
+
node,
|
321
|
+
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
|
322
|
+
loc: specifier.loc,
|
323
|
+
data: {
|
324
|
+
importSource,
|
325
|
+
customMessage,
|
326
|
+
importName
|
327
|
+
}
|
328
|
+
});
|
329
|
+
});
|
330
|
+
}
|
331
|
+
|
332
|
+
if (allowedImportNames && !allowedImportNames.includes(importName)) {
|
333
|
+
specifiers.forEach(specifier => {
|
334
|
+
context.report({
|
335
|
+
node,
|
336
|
+
loc: specifier.loc,
|
337
|
+
messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName",
|
338
|
+
data: {
|
339
|
+
importSource,
|
340
|
+
customMessage,
|
341
|
+
importName,
|
342
|
+
allowedImportNames
|
343
|
+
}
|
344
|
+
});
|
345
|
+
});
|
346
|
+
}
|
347
|
+
});
|
266
348
|
});
|
267
349
|
}
|
268
350
|
|
@@ -281,12 +363,14 @@ module.exports = {
|
|
281
363
|
const customMessage = group.customMessage;
|
282
364
|
const restrictedImportNames = group.importNames;
|
283
365
|
const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null;
|
366
|
+
const allowedImportNames = group.allowImportNames;
|
367
|
+
const allowedImportNamePattern = group.allowImportNamePattern ? new RegExp(group.allowImportNamePattern, "u") : null;
|
284
368
|
|
285
|
-
|
369
|
+
/**
|
286
370
|
* If we are not restricting to any specific import names and just the pattern itself,
|
287
371
|
* report the error and move on
|
288
372
|
*/
|
289
|
-
if (!restrictedImportNames && !restrictedImportNamePattern) {
|
373
|
+
if (!restrictedImportNames && !allowedImportNames && !restrictedImportNamePattern && !allowedImportNamePattern) {
|
290
374
|
context.report({
|
291
375
|
node,
|
292
376
|
messageId: customMessage ? "patternWithCustomMessage" : "patterns",
|
@@ -313,6 +397,28 @@ module.exports = {
|
|
313
397
|
customMessage
|
314
398
|
}
|
315
399
|
});
|
400
|
+
} else if (allowedImportNames) {
|
401
|
+
context.report({
|
402
|
+
node,
|
403
|
+
messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames",
|
404
|
+
loc: specifier.loc,
|
405
|
+
data: {
|
406
|
+
importSource,
|
407
|
+
allowedImportNames,
|
408
|
+
customMessage
|
409
|
+
}
|
410
|
+
});
|
411
|
+
} else if (allowedImportNamePattern) {
|
412
|
+
context.report({
|
413
|
+
node,
|
414
|
+
messageId: customMessage ? "everythingWithAllowedImportNamePatternWithCustomMessage" : "everythingWithAllowedImportNamePattern",
|
415
|
+
loc: specifier.loc,
|
416
|
+
data: {
|
417
|
+
importSource,
|
418
|
+
allowedImportNamePattern,
|
419
|
+
customMessage
|
420
|
+
}
|
421
|
+
});
|
316
422
|
} else {
|
317
423
|
context.report({
|
318
424
|
node,
|
@@ -346,6 +452,36 @@ module.exports = {
|
|
346
452
|
});
|
347
453
|
});
|
348
454
|
}
|
455
|
+
|
456
|
+
if (allowedImportNames && !allowedImportNames.includes(importName)) {
|
457
|
+
specifiers.forEach(specifier => {
|
458
|
+
context.report({
|
459
|
+
node,
|
460
|
+
messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName",
|
461
|
+
loc: specifier.loc,
|
462
|
+
data: {
|
463
|
+
importSource,
|
464
|
+
customMessage,
|
465
|
+
importName,
|
466
|
+
allowedImportNames
|
467
|
+
}
|
468
|
+
});
|
469
|
+
});
|
470
|
+
} else if (allowedImportNamePattern && !allowedImportNamePattern.test(importName)) {
|
471
|
+
specifiers.forEach(specifier => {
|
472
|
+
context.report({
|
473
|
+
node,
|
474
|
+
messageId: customMessage ? "allowedImportNamePatternWithCustomMessage" : "allowedImportNamePattern",
|
475
|
+
loc: specifier.loc,
|
476
|
+
data: {
|
477
|
+
importSource,
|
478
|
+
customMessage,
|
479
|
+
importName,
|
480
|
+
allowedImportNamePattern
|
481
|
+
}
|
482
|
+
});
|
483
|
+
});
|
484
|
+
}
|
349
485
|
});
|
350
486
|
}
|
351
487
|
|
@@ -70,6 +70,9 @@ module.exports = {
|
|
70
70
|
},
|
71
71
|
destructuredArrayIgnorePattern: {
|
72
72
|
type: "string"
|
73
|
+
},
|
74
|
+
ignoreClassWithStaticInitBlock: {
|
75
|
+
type: "boolean"
|
73
76
|
}
|
74
77
|
},
|
75
78
|
additionalProperties: false
|
@@ -92,7 +95,8 @@ module.exports = {
|
|
92
95
|
vars: "all",
|
93
96
|
args: "after-used",
|
94
97
|
ignoreRestSiblings: false,
|
95
|
-
caughtErrors: "all"
|
98
|
+
caughtErrors: "all",
|
99
|
+
ignoreClassWithStaticInitBlock: false
|
96
100
|
};
|
97
101
|
|
98
102
|
const firstOption = context.options[0];
|
@@ -105,6 +109,7 @@ module.exports = {
|
|
105
109
|
config.args = firstOption.args || config.args;
|
106
110
|
config.ignoreRestSiblings = firstOption.ignoreRestSiblings || config.ignoreRestSiblings;
|
107
111
|
config.caughtErrors = firstOption.caughtErrors || config.caughtErrors;
|
112
|
+
config.ignoreClassWithStaticInitBlock = firstOption.ignoreClassWithStaticInitBlock || config.ignoreClassWithStaticInitBlock;
|
108
113
|
|
109
114
|
if (firstOption.varsIgnorePattern) {
|
110
115
|
config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern, "u");
|
@@ -613,6 +618,14 @@ module.exports = {
|
|
613
618
|
continue;
|
614
619
|
}
|
615
620
|
|
621
|
+
if (type === "ClassName") {
|
622
|
+
const hasStaticBlock = def.node.body.body.some(node => node.type === "StaticBlock");
|
623
|
+
|
624
|
+
if (config.ignoreClassWithStaticInitBlock && hasStaticBlock) {
|
625
|
+
continue;
|
626
|
+
}
|
627
|
+
}
|
628
|
+
|
616
629
|
// skip catch variables
|
617
630
|
if (type === "CatchClause") {
|
618
631
|
if (config.caughtErrors === "none") {
|
@@ -0,0 +1,240 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Utility functions to locate the source text of each code unit in the value of a string literal or template token.
|
3
|
+
* @author Francesco Trotta
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Represents a code unit produced by the evaluation of a JavaScript common token like a string
|
10
|
+
* literal or template token.
|
11
|
+
*/
|
12
|
+
class CodeUnit {
|
13
|
+
constructor(start, source) {
|
14
|
+
this.start = start;
|
15
|
+
this.source = source;
|
16
|
+
}
|
17
|
+
|
18
|
+
get end() {
|
19
|
+
return this.start + this.length;
|
20
|
+
}
|
21
|
+
|
22
|
+
get length() {
|
23
|
+
return this.source.length;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* An object used to keep track of the position in a source text where the next characters will be read.
|
29
|
+
*/
|
30
|
+
class TextReader {
|
31
|
+
constructor(source) {
|
32
|
+
this.source = source;
|
33
|
+
this.pos = 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Advances the reading position of the specified number of characters.
|
38
|
+
* @param {number} length Number of characters to advance.
|
39
|
+
* @returns {void}
|
40
|
+
*/
|
41
|
+
advance(length) {
|
42
|
+
this.pos += length;
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Reads characters from the source.
|
47
|
+
* @param {number} [offset=0] The offset where reading starts, relative to the current position.
|
48
|
+
* @param {number} [length=1] Number of characters to read.
|
49
|
+
* @returns {string} A substring of source characters.
|
50
|
+
*/
|
51
|
+
read(offset = 0, length = 1) {
|
52
|
+
const start = offset + this.pos;
|
53
|
+
|
54
|
+
return this.source.slice(start, start + length);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
const SIMPLE_ESCAPE_SEQUENCES =
|
59
|
+
{ __proto__: null, b: "\b", f: "\f", n: "\n", r: "\r", t: "\t", v: "\v" };
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Reads a hex escape sequence.
|
63
|
+
* @param {TextReader} reader The reader should be positioned on the first hexadecimal digit.
|
64
|
+
* @param {number} length The number of hexadecimal digits.
|
65
|
+
* @returns {string} A code unit.
|
66
|
+
*/
|
67
|
+
function readHexSequence(reader, length) {
|
68
|
+
const str = reader.read(0, length);
|
69
|
+
const charCode = parseInt(str, 16);
|
70
|
+
|
71
|
+
reader.advance(length);
|
72
|
+
return String.fromCharCode(charCode);
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Reads a Unicode escape sequence.
|
77
|
+
* @param {TextReader} reader The reader should be positioned after the "u".
|
78
|
+
* @returns {string} A code unit.
|
79
|
+
*/
|
80
|
+
function readUnicodeSequence(reader) {
|
81
|
+
const regExp = /\{(?<hexDigits>[\dA-Fa-f]+)\}/uy;
|
82
|
+
|
83
|
+
regExp.lastIndex = reader.pos;
|
84
|
+
const match = regExp.exec(reader.source);
|
85
|
+
|
86
|
+
if (match) {
|
87
|
+
const codePoint = parseInt(match.groups.hexDigits, 16);
|
88
|
+
|
89
|
+
reader.pos = regExp.lastIndex;
|
90
|
+
return String.fromCodePoint(codePoint);
|
91
|
+
}
|
92
|
+
return readHexSequence(reader, 4);
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Reads an octal escape sequence.
|
97
|
+
* @param {TextReader} reader The reader should be positioned after the first octal digit.
|
98
|
+
* @param {number} maxLength The maximum number of octal digits.
|
99
|
+
* @returns {string} A code unit.
|
100
|
+
*/
|
101
|
+
function readOctalSequence(reader, maxLength) {
|
102
|
+
const [octalStr] = reader.read(-1, maxLength).match(/^[0-7]+/u);
|
103
|
+
|
104
|
+
reader.advance(octalStr.length - 1);
|
105
|
+
const octal = parseInt(octalStr, 8);
|
106
|
+
|
107
|
+
return String.fromCharCode(octal);
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Reads an escape sequence or line continuation.
|
112
|
+
* @param {TextReader} reader The reader should be positioned on the backslash.
|
113
|
+
* @returns {string} A string of zero, one or two code units.
|
114
|
+
*/
|
115
|
+
function readEscapeSequenceOrLineContinuation(reader) {
|
116
|
+
const char = reader.read(1);
|
117
|
+
|
118
|
+
reader.advance(2);
|
119
|
+
const unitChar = SIMPLE_ESCAPE_SEQUENCES[char];
|
120
|
+
|
121
|
+
if (unitChar) {
|
122
|
+
return unitChar;
|
123
|
+
}
|
124
|
+
switch (char) {
|
125
|
+
case "x":
|
126
|
+
return readHexSequence(reader, 2);
|
127
|
+
case "u":
|
128
|
+
return readUnicodeSequence(reader);
|
129
|
+
case "\r":
|
130
|
+
if (reader.read() === "\n") {
|
131
|
+
reader.advance(1);
|
132
|
+
}
|
133
|
+
|
134
|
+
// fallthrough
|
135
|
+
case "\n":
|
136
|
+
case "\u2028":
|
137
|
+
case "\u2029":
|
138
|
+
return "";
|
139
|
+
case "0":
|
140
|
+
case "1":
|
141
|
+
case "2":
|
142
|
+
case "3":
|
143
|
+
return readOctalSequence(reader, 3);
|
144
|
+
case "4":
|
145
|
+
case "5":
|
146
|
+
case "6":
|
147
|
+
case "7":
|
148
|
+
return readOctalSequence(reader, 2);
|
149
|
+
default:
|
150
|
+
return char;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
/**
|
155
|
+
* Reads an escape sequence or line continuation and generates the respective `CodeUnit` elements.
|
156
|
+
* @param {TextReader} reader The reader should be positioned on the backslash.
|
157
|
+
* @returns {Generator<CodeUnit>} Zero, one or two `CodeUnit` elements.
|
158
|
+
*/
|
159
|
+
function *mapEscapeSequenceOrLineContinuation(reader) {
|
160
|
+
const start = reader.pos;
|
161
|
+
const str = readEscapeSequenceOrLineContinuation(reader);
|
162
|
+
const end = reader.pos;
|
163
|
+
const source = reader.source.slice(start, end);
|
164
|
+
|
165
|
+
switch (str.length) {
|
166
|
+
case 0:
|
167
|
+
break;
|
168
|
+
case 1:
|
169
|
+
yield new CodeUnit(start, source);
|
170
|
+
break;
|
171
|
+
default:
|
172
|
+
yield new CodeUnit(start, source);
|
173
|
+
yield new CodeUnit(start, source);
|
174
|
+
break;
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
/**
|
179
|
+
* Parses a string literal.
|
180
|
+
* @param {string} source The string literal to parse, including the delimiting quotes.
|
181
|
+
* @returns {CodeUnit[]} A list of code units produced by the string literal.
|
182
|
+
*/
|
183
|
+
function parseStringLiteral(source) {
|
184
|
+
const reader = new TextReader(source);
|
185
|
+
const quote = reader.read();
|
186
|
+
|
187
|
+
reader.advance(1);
|
188
|
+
const codeUnits = [];
|
189
|
+
|
190
|
+
for (;;) {
|
191
|
+
const char = reader.read();
|
192
|
+
|
193
|
+
if (char === quote) {
|
194
|
+
break;
|
195
|
+
}
|
196
|
+
if (char === "\\") {
|
197
|
+
codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader));
|
198
|
+
} else {
|
199
|
+
codeUnits.push(new CodeUnit(reader.pos, char));
|
200
|
+
reader.advance(1);
|
201
|
+
}
|
202
|
+
}
|
203
|
+
return codeUnits;
|
204
|
+
}
|
205
|
+
|
206
|
+
/**
|
207
|
+
* Parses a template token.
|
208
|
+
* @param {string} source The template token to parse, including the delimiting sequences `` ` ``, `${` and `}`.
|
209
|
+
* @returns {CodeUnit[]} A list of code units produced by the template token.
|
210
|
+
*/
|
211
|
+
function parseTemplateToken(source) {
|
212
|
+
const reader = new TextReader(source);
|
213
|
+
|
214
|
+
reader.advance(1);
|
215
|
+
const codeUnits = [];
|
216
|
+
|
217
|
+
for (;;) {
|
218
|
+
const char = reader.read();
|
219
|
+
|
220
|
+
if (char === "`" || char === "$" && reader.read(1) === "{") {
|
221
|
+
break;
|
222
|
+
}
|
223
|
+
if (char === "\\") {
|
224
|
+
codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader));
|
225
|
+
} else {
|
226
|
+
let unitSource;
|
227
|
+
|
228
|
+
if (char === "\r" && reader.read(1) === "\n") {
|
229
|
+
unitSource = "\r\n";
|
230
|
+
} else {
|
231
|
+
unitSource = char;
|
232
|
+
}
|
233
|
+
codeUnits.push(new CodeUnit(reader.pos, unitSource));
|
234
|
+
reader.advance(unitSource.length);
|
235
|
+
}
|
236
|
+
}
|
237
|
+
return codeUnits;
|
238
|
+
}
|
239
|
+
|
240
|
+
module.exports = { parseStringLiteral, parseTemplateToken };
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "eslint",
|
3
|
-
"version": "9.0.0-beta.
|
3
|
+
"version": "9.0.0-beta.2",
|
4
4
|
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
|
5
5
|
"description": "An AST-based pattern checker for JavaScript.",
|
6
6
|
"bin": {
|
@@ -66,7 +66,7 @@
|
|
66
66
|
"@eslint-community/eslint-utils": "^4.2.0",
|
67
67
|
"@eslint-community/regexpp": "^4.6.1",
|
68
68
|
"@eslint/eslintrc": "^3.0.2",
|
69
|
-
"@eslint/js": "9.0.0-beta.
|
69
|
+
"@eslint/js": "9.0.0-beta.2",
|
70
70
|
"@humanwhocodes/config-array": "^0.11.14",
|
71
71
|
"@humanwhocodes/module-importer": "^1.0.1",
|
72
72
|
"@nodelib/fs.walk": "^1.2.8",
|