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 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 => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
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
  }
@@ -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
  })
@@ -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];
@@ -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 { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils");
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, node);
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 pattern = getStringIfConstant(patternNode, scope);
376
- const flags = getStringIfConstant(flagsNode, scope);
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 pattern === "string") {
379
- verify(patternNode, pattern, flags || "", fixer => {
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(({ group, message, caseSensitive, importNames, importNamePattern }) => ({
194
- matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
195
- customMessage: message,
196
- importNames,
197
- importNamePattern
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.1",
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.1",
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",