eslint 8.0.0-beta.1 → 8.0.1

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
@@ -296,9 +296,9 @@ The following companies, organizations, and individuals support ESLint's ongoing
296
296
  <!--sponsorsstart-->
297
297
  <h3>Platinum Sponsors</h3>
298
298
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/photomatt/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
299
- <p><a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="96"></a> <a href="https://google.com/chrome"><img src="https://images.opencollective.com/chrome/dc55bd4/logo.png" alt="Chrome's Web Framework & Tools Performance Fund" height="96"></a> <a href="https://www.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://coinbase.com"><img src="https://avatars.githubusercontent.com/u/1885080?v=4" alt="Coinbase" height="96"></a> <a href="https://substack.com/"><img src="https://avatars.githubusercontent.com/u/53023767?v=4" alt="Substack" height="96"></a></p><h3>Silver Sponsors</h3>
300
- <p><a href="https://retool.com/"><img src="https://images.opencollective.com/retool/98ea68e/logo.png" alt="Retool" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a></p><h3>Bronze Sponsors</h3>
301
- <p><a href="https://troypoint.com"><img src="https://images.opencollective.com/troypoint/080f96f/avatar.png" alt="TROYPOINT" height="32"></a> <a href="https://mobilen.nu"><img src="https://images.opencollective.com/mobilen/e19860d/logo.png" alt="Mobilen" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="null"><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://www.practiceignition.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Practice Ignition" height="32"></a></p>
299
+ <p><a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="96"></a> <a href="https://google.com/chrome"><img src="https://images.opencollective.com/chrome/dc55bd4/logo.png" alt="Chrome's Web Framework & Tools Performance Fund" height="96"></a> <a href="https://www.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://coinbase.com"><img src="https://avatars.githubusercontent.com/u/1885080?v=4" alt="Coinbase" height="96"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="96"></a> <a href="https://substack.com/"><img src="https://avatars.githubusercontent.com/u/53023767?v=4" alt="Substack" height="96"></a></p><h3>Silver Sponsors</h3>
300
+ <p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a></p><h3>Bronze Sponsors</h3>
301
+ <p><a href="https://launchdarkly.com"><img src="https://images.opencollective.com/launchdarkly/574bb9e/logo.png" alt="launchdarkly" height="32"></a> <a href="https://troypoint.com"><img src="https://images.opencollective.com/troypoint/080f96f/avatar.png" alt="TROYPOINT" height="32"></a> <a href="https://mobilen.nu"><img src="https://images.opencollective.com/mobilen/e19860d/logo.png" alt="Mobilen" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="null"><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://www.practiceignition.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Practice Ignition" height="32"></a></p>
302
302
  <!--sponsorsend-->
303
303
 
304
304
  ## <a name="technology-sponsors"></a>Technology Sponsors
@@ -35,16 +35,33 @@ function findRuleDefinition(ruleId, config) {
35
35
  pluginName = ruleIdParts.join("/");
36
36
  }
37
37
 
38
- if (!config.plugins || !config.plugins[pluginName]) {
39
- throw new TypeError(`Key "rules": Key "${ruleId}": Could not find plugin "${pluginName}".`);
40
- }
38
+ const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
39
+ let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}".`;
41
40
 
42
- if (!config.plugins[pluginName].rules || !config.plugins[pluginName].rules[ruleName]) {
43
- throw new TypeError(`Key "rules": Key "${ruleId}": Could not find "${ruleName}" in plugin "${pluginName}".`);
44
- }
41
+ // if the plugin exists then we need to check if the rule exists
42
+ if (config.plugins && config.plugins[pluginName]) {
43
+
44
+ const plugin = config.plugins[pluginName];
45
+
46
+ // first check for exact rule match
47
+ if (plugin.rules && plugin.rules[ruleName]) {
48
+ return config.plugins[pluginName].rules[ruleName];
49
+ }
45
50
 
46
- return config.plugins[pluginName].rules[ruleName];
51
+ errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;
52
+
53
+ // otherwise, let's see if we can find the rule name elsewhere
54
+ for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) {
55
+ if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
56
+ errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
57
+ break;
58
+ }
59
+ }
60
+
61
+ // falls through to throw error
62
+ }
47
63
 
64
+ throw new TypeError(errorMessage);
48
65
  }
49
66
 
50
67
  /**
@@ -46,26 +46,95 @@ function groupByParentComment(directives) {
46
46
  * @returns {{ description, fix, position }[]} Details for later creation of output Problems.
47
47
  */
48
48
  function createIndividualDirectivesRemoval(directives, commentToken) {
49
- const listOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
49
+
50
+ /*
51
+ * `commentToken.value` starts right after `//` or `/*`.
52
+ * All calculated offsets will be relative to this index.
53
+ */
54
+ const commentValueStart = commentToken.range[0] + "//".length;
55
+
56
+ // Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
57
+ const listStartOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
58
+
59
+ /*
60
+ * Get the list text without any surrounding whitespace. In order to preserve the original
61
+ * formatting, we don't want to change that whitespace.
62
+ *
63
+ * // eslint-disable-line rule-one , rule-two , rule-three -- comment
64
+ * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
65
+ */
50
66
  const listText = commentToken.value
51
- .slice(listOffset) // remove eslint-*
52
- .split(/\s-{2,}\s/u)[0] // remove -- directive comment
53
- .trimRight();
54
- const listStart = commentToken.range[0] + 2 + listOffset;
67
+ .slice(listStartOffset) // remove directive name and all whitespace before the list
68
+ .split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
69
+ .trimRight(); // remove all whitespace after the list
70
+
71
+ /*
72
+ * We can assume that `listText` contains multiple elements.
73
+ * Otherwise, this function wouldn't be called - if there is
74
+ * only one rule in the list, then the whole comment must be removed.
75
+ */
55
76
 
56
77
  return directives.map(directive => {
57
78
  const { ruleId } = directive;
58
- const match = new RegExp(String.raw`(?:^|,)\s*${escapeRegExp(ruleId)}\s*(?:$|,)`, "u").exec(listText);
59
- const ruleOffset = match.index;
60
- const ruleEndOffset = ruleOffset + match[0].length;
61
- const ruleText = listText.slice(ruleOffset, ruleEndOffset);
79
+
80
+ const regex = new RegExp(String.raw`(?:^|\s*,\s*)${escapeRegExp(ruleId)}(?:\s*,\s*|$)`, "u");
81
+ const match = regex.exec(listText);
82
+ const matchedText = match[0];
83
+ const matchStartOffset = listStartOffset + match.index;
84
+ const matchEndOffset = matchStartOffset + matchedText.length;
85
+
86
+ const firstIndexOfComma = matchedText.indexOf(",");
87
+ const lastIndexOfComma = matchedText.lastIndexOf(",");
88
+
89
+ let removalStartOffset, removalEndOffset;
90
+
91
+ if (firstIndexOfComma !== lastIndexOfComma) {
92
+
93
+ /*
94
+ * Since there are two commas, this must one of the elements in the middle of the list.
95
+ * Matched range starts where the previous rule name ends, and ends where the next rule name starts.
96
+ *
97
+ * // eslint-disable-line rule-one , rule-two , rule-three -- comment
98
+ * ^^^^^^^^^^^^^^
99
+ *
100
+ * We want to remove only the content between the two commas, and also one of the commas.
101
+ *
102
+ * // eslint-disable-line rule-one , rule-two , rule-three -- comment
103
+ * ^^^^^^^^^^^
104
+ */
105
+ removalStartOffset = matchStartOffset + firstIndexOfComma;
106
+ removalEndOffset = matchStartOffset + lastIndexOfComma;
107
+
108
+ } else {
109
+
110
+ /*
111
+ * This is either the first element or the last element.
112
+ *
113
+ * If this is the first element, matched range starts where the first rule name starts
114
+ * and ends where the second rule name starts. This is exactly the range we want
115
+ * to remove so that the second rule name will start where the first one was starting
116
+ * and thus preserve the original formatting.
117
+ *
118
+ * // eslint-disable-line rule-one , rule-two , rule-three -- comment
119
+ * ^^^^^^^^^^^
120
+ *
121
+ * Similarly, if this is the last element, we've already matched the range we want to
122
+ * remove. The previous rule name will end where the last one was ending, relative
123
+ * to the content on the right side.
124
+ *
125
+ * // eslint-disable-line rule-one , rule-two , rule-three -- comment
126
+ * ^^^^^^^^^^^^^
127
+ */
128
+ removalStartOffset = matchStartOffset;
129
+ removalEndOffset = matchEndOffset;
130
+ }
62
131
 
63
132
  return {
64
133
  description: `'${ruleId}'`,
65
134
  fix: {
66
135
  range: [
67
- listStart + ruleOffset + (ruleText.startsWith(",") && ruleText.endsWith(",") ? 1 : 0),
68
- listStart + ruleEndOffset
136
+ commentValueStart + removalStartOffset,
137
+ commentValueStart + removalEndOffset
69
138
  ],
70
139
  text: ""
71
140
  },
@@ -15,7 +15,7 @@ const levn = require("levn"),
15
15
  Legacy: {
16
16
  ConfigOps
17
17
  }
18
- } = require("@eslint/eslintrc/universal"); // eslint-disable-line node/no-missing-require -- false positive
18
+ } = require("@eslint/eslintrc/universal");
19
19
 
20
20
  const debug = require("debug")("eslint:config-comment-parser");
21
21
 
@@ -24,7 +24,7 @@ const
24
24
  ConfigValidator,
25
25
  environments: BuiltInEnvironments
26
26
  }
27
- } = require("@eslint/eslintrc/universal"), // eslint-disable-line node/no-missing-require -- false positive
27
+ } = require("@eslint/eslintrc/universal"),
28
28
  Traverser = require("../shared/traverser"),
29
29
  { SourceCode } = require("../source-code"),
30
30
  CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
@@ -955,13 +955,31 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
955
955
 
956
956
  const ruleListeners = createRuleListeners(rule, ruleContext);
957
957
 
958
+ /**
959
+ * Include `ruleId` in error logs
960
+ * @param {Function} ruleListener A rule method that listens for a node.
961
+ * @returns {Function} ruleListener wrapped in error handler
962
+ */
963
+ function addRuleErrorHandler(ruleListener) {
964
+ return function ruleErrorHandler(...listenerArgs) {
965
+ try {
966
+ return ruleListener(...listenerArgs);
967
+ } catch (e) {
968
+ e.ruleId = ruleId;
969
+ throw e;
970
+ }
971
+ };
972
+ }
973
+
958
974
  // add all the selectors from the rule as listeners
959
975
  Object.keys(ruleListeners).forEach(selector => {
976
+ const ruleListener = timing.enabled
977
+ ? timing.time(ruleId, ruleListeners[selector])
978
+ : ruleListeners[selector];
979
+
960
980
  emitter.on(
961
981
  selector,
962
- timing.enabled
963
- ? timing.time(ruleId, ruleListeners[selector])
964
- : ruleListeners[selector]
982
+ addRuleErrorHandler(ruleListener)
965
983
  );
966
984
  });
967
985
  });
@@ -1223,6 +1241,11 @@ class Linter {
1223
1241
  debug("Parser Options:", parserOptions);
1224
1242
  debug("Parser Path:", parserName);
1225
1243
  debug("Settings:", settings);
1244
+
1245
+ if (err.ruleId) {
1246
+ err.message += `\nRule: "${err.ruleId}"`;
1247
+ }
1248
+
1226
1249
  throw err;
1227
1250
  }
1228
1251
 
@@ -33,6 +33,10 @@ module.exports = {
33
33
  items: {
34
34
  type: "string"
35
35
  }
36
+ },
37
+ enforceForClassFields: {
38
+ type: "boolean",
39
+ default: true
36
40
  }
37
41
  },
38
42
  additionalProperties: false
@@ -44,10 +48,27 @@ module.exports = {
44
48
  },
45
49
  create(context) {
46
50
  const config = Object.assign({}, context.options[0]);
51
+ const enforceForClassFields = config.enforceForClassFields !== false;
47
52
  const exceptMethods = new Set(config.exceptMethods || []);
48
53
 
49
54
  const stack = [];
50
55
 
56
+ /**
57
+ * Push `this` used flag initialized with `false` onto the stack.
58
+ * @returns {void}
59
+ */
60
+ function pushContext() {
61
+ stack.push(false);
62
+ }
63
+
64
+ /**
65
+ * Pop `this` used flag from the stack.
66
+ * @returns {boolean | undefined} `this` used flag
67
+ */
68
+ function popContext() {
69
+ return stack.pop();
70
+ }
71
+
51
72
  /**
52
73
  * Initializes the current context to false and pushes it onto the stack.
53
74
  * These booleans represent whether 'this' has been used in the context.
@@ -55,7 +76,7 @@ module.exports = {
55
76
  * @private
56
77
  */
57
78
  function enterFunction() {
58
- stack.push(false);
79
+ pushContext();
59
80
  }
60
81
 
61
82
  /**
@@ -69,7 +90,7 @@ module.exports = {
69
90
  case "MethodDefinition":
70
91
  return !node.static && node.kind !== "constructor";
71
92
  case "PropertyDefinition":
72
- return !node.static;
93
+ return !node.static && enforceForClassFields;
73
94
  default:
74
95
  return false;
75
96
  }
@@ -82,8 +103,19 @@ module.exports = {
82
103
  * @private
83
104
  */
84
105
  function isIncludedInstanceMethod(node) {
85
- return isInstanceMethod(node) &&
86
- (node.computed || !exceptMethods.has(node.key.name));
106
+ if (isInstanceMethod(node)) {
107
+ if (node.computed) {
108
+ return true;
109
+ }
110
+
111
+ const hashIfNeeded = node.key.type === "PrivateIdentifier" ? "#" : "";
112
+ const name = node.key.type === "Literal"
113
+ ? astUtils.getStaticStringValue(node.key)
114
+ : (node.key.name || "");
115
+
116
+ return !exceptMethods.has(hashIfNeeded + name);
117
+ }
118
+ return false;
87
119
  }
88
120
 
89
121
  /**
@@ -95,7 +127,7 @@ module.exports = {
95
127
  * @private
96
128
  */
97
129
  function exitFunction(node) {
98
- const methodUsesThis = stack.pop();
130
+ const methodUsesThis = popContext();
99
131
 
100
132
  if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
101
133
  context.report({
@@ -125,10 +157,21 @@ module.exports = {
125
157
  "FunctionDeclaration:exit": exitFunction,
126
158
  FunctionExpression: enterFunction,
127
159
  "FunctionExpression:exit": exitFunction,
128
- "PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
129
- "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction,
160
+
161
+ /*
162
+ * Class field value are implicit functions.
163
+ */
164
+ "PropertyDefinition:exit": popContext,
165
+ "PropertyDefinition > *.key:exit": pushContext,
166
+
130
167
  ThisExpression: markThisUsed,
131
- Super: markThisUsed
168
+ Super: markThisUsed,
169
+ ...(
170
+ enforceForClassFields && {
171
+ "PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
172
+ "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction
173
+ }
174
+ )
132
175
  };
133
176
  }
134
177
  };
@@ -74,60 +74,16 @@ module.exports = {
74
74
  // Helpers
75
75
  //--------------------------------------------------------------------------
76
76
 
77
- // Using a stack to store complexity (handling nested functions)
78
- const fns = [];
77
+ // Using a stack to store complexity per code path
78
+ const complexities = [];
79
79
 
80
80
  /**
81
- * When parsing a new function, store it in our function stack
82
- * @returns {void}
83
- * @private
84
- */
85
- function startFunction() {
86
- fns.push(1);
87
- }
88
-
89
- /**
90
- * Evaluate the node at the end of function
91
- * @param {ASTNode} node node to evaluate
92
- * @returns {void}
93
- * @private
94
- */
95
- function endFunction(node) {
96
- const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
97
- const complexity = fns.pop();
98
-
99
- if (complexity > THRESHOLD) {
100
- context.report({
101
- node,
102
- messageId: "complex",
103
- data: { name, complexity, max: THRESHOLD }
104
- });
105
- }
106
- }
107
-
108
- /**
109
- * Increase the complexity of the function in context
81
+ * Increase the complexity of the code path in context
110
82
  * @returns {void}
111
83
  * @private
112
84
  */
113
85
  function increaseComplexity() {
114
- if (fns.length) {
115
- fns[fns.length - 1]++;
116
- }
117
- }
118
-
119
- /**
120
- * Increase the switch complexity in context
121
- * @param {ASTNode} node node to evaluate
122
- * @returns {void}
123
- * @private
124
- */
125
- function increaseSwitchComplexity(node) {
126
-
127
- // Avoiding `default`
128
- if (node.test) {
129
- increaseComplexity();
130
- }
86
+ complexities[complexities.length - 1]++;
131
87
  }
132
88
 
133
89
  //--------------------------------------------------------------------------
@@ -135,13 +91,14 @@ module.exports = {
135
91
  //--------------------------------------------------------------------------
136
92
 
137
93
  return {
138
- FunctionDeclaration: startFunction,
139
- FunctionExpression: startFunction,
140
- ArrowFunctionExpression: startFunction,
141
- "FunctionDeclaration:exit": endFunction,
142
- "FunctionExpression:exit": endFunction,
143
- "ArrowFunctionExpression:exit": endFunction,
144
94
 
95
+ onCodePathStart() {
96
+
97
+ // The initial complexity is 1, representing one execution path in the CodePath
98
+ complexities.push(1);
99
+ },
100
+
101
+ // Each branching in the code adds 1 to the complexity
145
102
  CatchClause: increaseComplexity,
146
103
  ConditionalExpression: increaseComplexity,
147
104
  LogicalExpression: increaseComplexity,
@@ -149,14 +106,49 @@ module.exports = {
149
106
  ForInStatement: increaseComplexity,
150
107
  ForOfStatement: increaseComplexity,
151
108
  IfStatement: increaseComplexity,
152
- SwitchCase: increaseSwitchComplexity,
153
109
  WhileStatement: increaseComplexity,
154
110
  DoWhileStatement: increaseComplexity,
155
111
 
112
+ // Avoid `default`
113
+ "SwitchCase[test]": increaseComplexity,
114
+
115
+ // Logical assignment operators have short-circuiting behavior
156
116
  AssignmentExpression(node) {
157
117
  if (astUtils.isLogicalAssignmentOperator(node.operator)) {
158
118
  increaseComplexity();
159
119
  }
120
+ },
121
+
122
+ onCodePathEnd(codePath, node) {
123
+ const complexity = complexities.pop();
124
+
125
+ /*
126
+ * This rule only evaluates complexity of functions, so "program" is excluded.
127
+ * Class field initializers are implicit functions. Therefore, they shouldn't contribute
128
+ * to the enclosing function's complexity, but their own complexity should be evaluated.
129
+ */
130
+ if (
131
+ codePath.origin !== "function" &&
132
+ codePath.origin !== "class-field-initializer"
133
+ ) {
134
+ return;
135
+ }
136
+
137
+ if (complexity > THRESHOLD) {
138
+ const name = codePath.origin === "class-field-initializer"
139
+ ? "class field initializer"
140
+ : astUtils.getFunctionNameWithKind(node);
141
+
142
+ context.report({
143
+ node,
144
+ messageId: "complex",
145
+ data: {
146
+ name: upperCaseFirst(name),
147
+ complexity,
148
+ max: THRESHOLD
149
+ }
150
+ });
151
+ }
160
152
  }
161
153
  };
162
154
 
@@ -4,6 +4,10 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
7
11
  const astUtils = require("./utils/ast-utils");
8
12
 
9
13
  //------------------------------------------------------------------------------
@@ -52,6 +56,51 @@ module.exports = {
52
56
 
53
57
  const sourceCode = context.getSourceCode();
54
58
 
59
+ /**
60
+ * Gets a pair of tokens that should be used to check lines between two class member nodes.
61
+ *
62
+ * In most cases, this returns the very last token of the current node and
63
+ * the very first token of the next node.
64
+ * For example:
65
+ *
66
+ * class C {
67
+ * x = 1; // curLast: `;` nextFirst: `in`
68
+ * in = 2
69
+ * }
70
+ *
71
+ * There is only one exception. If the given node ends with a semicolon, and it looks like
72
+ * a semicolon-less style's semicolon - one that is not on the same line as the preceding
73
+ * token, but is on the line where the next class member starts - this returns the preceding
74
+ * token and the semicolon as boundary tokens.
75
+ * For example:
76
+ *
77
+ * class C {
78
+ * x = 1 // curLast: `1` nextFirst: `;`
79
+ * ;in = 2
80
+ * }
81
+ * When determining the desired layout of the code, we should treat this semicolon as
82
+ * a part of the next class member node instead of the one it technically belongs to.
83
+ * @param {ASTNode} curNode Current class member node.
84
+ * @param {ASTNode} nextNode Next class member node.
85
+ * @returns {Token} The actual last token of `node`.
86
+ * @private
87
+ */
88
+ function getBoundaryTokens(curNode, nextNode) {
89
+ const lastToken = sourceCode.getLastToken(curNode);
90
+ const prevToken = sourceCode.getTokenBefore(lastToken);
91
+ const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes
92
+
93
+ const isSemicolonLessStyle = (
94
+ astUtils.isSemicolonToken(lastToken) &&
95
+ !astUtils.isTokenOnSameLine(prevToken, lastToken) &&
96
+ astUtils.isTokenOnSameLine(lastToken, nextToken)
97
+ );
98
+
99
+ return isSemicolonLessStyle
100
+ ? { curLast: prevToken, nextFirst: lastToken }
101
+ : { curLast: lastToken, nextFirst: nextToken };
102
+ }
103
+
55
104
  /**
56
105
  * Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member.
57
106
  * @param {Token} prevLastToken The last token in the previous member node.
@@ -100,8 +149,7 @@ module.exports = {
100
149
 
101
150
  for (let i = 0; i < body.length - 1; i++) {
102
151
  const curFirst = sourceCode.getFirstToken(body[i]);
103
- const curLast = sourceCode.getLastToken(body[i]);
104
- const nextFirst = sourceCode.getFirstToken(body[i + 1]);
152
+ const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]);
105
153
  const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast);
106
154
  const skip = !isMulti && options[1].exceptAfterSingleLine;
107
155
  const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1);
@@ -25,8 +25,25 @@ module.exports = {
25
25
 
26
26
  schema: [
27
27
  {
28
- type: "integer",
29
- minimum: 1
28
+ oneOf: [
29
+ {
30
+ type: "integer",
31
+ minimum: 1
32
+ },
33
+ {
34
+ type: "object",
35
+ properties: {
36
+ ignoreExpressions: {
37
+ type: "boolean"
38
+ },
39
+ max: {
40
+ type: "integer",
41
+ minimum: 1
42
+ }
43
+ },
44
+ additionalProperties: false
45
+ }
46
+ ]
30
47
  }
31
48
  ],
32
49
 
@@ -35,8 +52,10 @@ module.exports = {
35
52
  }
36
53
  },
37
54
  create(context) {
38
-
39
- const maxClasses = context.options[0] || 1;
55
+ const [option = {}] = context.options;
56
+ const [ignoreExpressions, max] = typeof option === "number"
57
+ ? [false, option || 1]
58
+ : [option.ignoreExpressions, option.max || 1];
40
59
 
41
60
  let classCount = 0;
42
61
 
@@ -45,19 +64,24 @@ module.exports = {
45
64
  classCount = 0;
46
65
  },
47
66
  "Program:exit"(node) {
48
- if (classCount > maxClasses) {
67
+ if (classCount > max) {
49
68
  context.report({
50
69
  node,
51
70
  messageId: "maximumExceeded",
52
71
  data: {
53
72
  classCount,
54
- max: maxClasses
73
+ max
55
74
  }
56
75
  });
57
76
  }
58
77
  },
59
- "ClassDeclaration, ClassExpression"() {
78
+ "ClassDeclaration"() {
60
79
  classCount++;
80
+ },
81
+ "ClassExpression"() {
82
+ if (!ignoreExpressions) {
83
+ classCount++;
84
+ }
61
85
  }
62
86
  };
63
87
  }
@@ -5,6 +5,18 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const astUtils = require("./utils/ast-utils");
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Helpers
16
+ //------------------------------------------------------------------------------
17
+
18
+ const callMethods = new Set(["apply", "bind", "call"]);
19
+
8
20
  //------------------------------------------------------------------------------
9
21
  // Rule Definition
10
22
  //------------------------------------------------------------------------------
@@ -37,14 +49,30 @@ module.exports = {
37
49
  variable.references.forEach(ref => {
38
50
  const node = ref.identifier;
39
51
  const { parent } = node;
52
+ let evalNode;
53
+
54
+ if (parent) {
55
+ if (node === parent.callee && (
56
+ parent.type === "NewExpression" ||
57
+ parent.type === "CallExpression"
58
+ )) {
59
+ evalNode = parent;
60
+ } else if (
61
+ parent.type === "MemberExpression" &&
62
+ node === parent.object &&
63
+ callMethods.has(astUtils.getStaticPropertyName(parent))
64
+ ) {
65
+ const maybeCallee = parent.parent.type === "ChainExpression" ? parent.parent : parent;
66
+
67
+ if (maybeCallee.parent.type === "CallExpression" && maybeCallee.parent.callee === maybeCallee) {
68
+ evalNode = maybeCallee.parent;
69
+ }
70
+ }
71
+ }
40
72
 
41
- if (
42
- parent &&
43
- (parent.type === "NewExpression" || parent.type === "CallExpression") &&
44
- node === parent.callee
45
- ) {
73
+ if (evalNode) {
46
74
  context.report({
47
- node: parent,
75
+ node: evalNode,
48
76
  messageId: "noFunctionConstructor"
49
77
  });
50
78
  }
@@ -33,91 +33,37 @@ module.exports = {
33
33
 
34
34
  const sourceCode = context.getSourceCode();
35
35
 
36
- /**
37
- * Get the node of init target.
38
- * @param {ASTNode} node The node to get.
39
- * @throws {Error} (Unreachable.)
40
- * @returns {ASTNode} The node of init target.
41
- */
42
- function getIdNode(node) {
43
- switch (node.type) {
44
- case "VariableDeclarator":
45
- return node.id;
46
- case "PropertyDefinition":
47
- return node.key;
48
- default:
49
- throw new Error("unreachable");
50
- }
51
- }
52
-
53
- /**
54
- * Get the node of init value.
55
- * @param {ASTNode} node The node to get.
56
- * @throws {Error} (Unreachable.)
57
- * @returns {ASTNode} The node of init value.
58
- */
59
- function getInitNode(node) {
60
- switch (node.type) {
61
- case "VariableDeclarator":
62
- return node.init;
63
- case "PropertyDefinition":
64
- return node.value;
65
- default:
66
- throw new Error("unreachable");
67
- }
68
- }
69
-
70
- /**
71
- * Get the parent kind of the node.
72
- * @param {ASTNode} node The node to get.
73
- * @throws {Error} (Unreachable.)
74
- * @returns {string} The parent kind.
75
- */
76
- function getParentKind(node) {
77
- switch (node.type) {
78
- case "VariableDeclarator":
79
- return node.parent.kind;
80
- case "PropertyDefinition":
81
- return "field";
82
- default:
83
- throw new Error("unreachable");
84
- }
85
- }
86
-
87
36
  return {
88
37
 
89
- "VariableDeclarator, PropertyDefinition"(node) {
90
- const idNode = getIdNode(node),
91
- name = sourceCode.getText(idNode),
92
- initNode = getInitNode(node),
93
- initIsUndefined = initNode && initNode.type === "Identifier" && initNode.name === "undefined",
94
- parentKind = getParentKind(node),
38
+ VariableDeclarator(node) {
39
+ const name = sourceCode.getText(node.id),
40
+ init = node.init && node.init.name,
95
41
  scope = context.getScope(),
96
42
  undefinedVar = astUtils.getVariableByName(scope, "undefined"),
97
43
  shadowed = undefinedVar && undefinedVar.defs.length > 0,
98
- lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
44
+ lastToken = sourceCode.getLastToken(node);
99
45
 
100
- if (initIsUndefined && parentKind !== "const" && !shadowed) {
46
+ if (init === "undefined" && node.parent.kind !== "const" && !shadowed) {
101
47
  context.report({
102
48
  node,
103
49
  messageId: "unnecessaryUndefinedInit",
104
50
  data: { name },
105
51
  fix(fixer) {
106
- if (parentKind === "var") {
52
+ if (node.parent.kind === "var") {
107
53
  return null;
108
54
  }
109
55
 
110
- if (idNode.type === "ArrayPattern" || idNode.type === "ObjectPattern") {
56
+ if (node.id.type === "ArrayPattern" || node.id.type === "ObjectPattern") {
111
57
 
112
58
  // Don't fix destructuring assignment to `undefined`.
113
59
  return null;
114
60
  }
115
61
 
116
- if (sourceCode.commentsExistBetween(idNode, lastToken)) {
62
+ if (sourceCode.commentsExistBetween(node.id, lastToken)) {
117
63
  return null;
118
64
  }
119
65
 
120
- return fixer.removeRange([idNode.range[1], lastToken.range[1]]);
66
+ return fixer.removeRange([node.id.range[1], node.range[1]]);
121
67
  }
122
68
  });
123
69
  }
@@ -179,7 +179,8 @@ module.exports = {
179
179
  schema: [],
180
180
 
181
181
  messages: {
182
- nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
182
+ nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.",
183
+ nonAtomicObjectUpdate: "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`."
183
184
  }
184
185
  },
185
186
 
@@ -275,13 +276,25 @@ module.exports = {
275
276
  const variable = reference.resolved;
276
277
 
277
278
  if (segmentInfo.isOutdated(codePath.currentSegments, variable)) {
278
- context.report({
279
- node: node.parent,
280
- messageId: "nonAtomicUpdate",
281
- data: {
282
- value: sourceCode.getText(node.parent.left)
283
- }
284
- });
279
+ if (node.parent.left === reference.identifier) {
280
+ context.report({
281
+ node: node.parent,
282
+ messageId: "nonAtomicUpdate",
283
+ data: {
284
+ value: variable.name
285
+ }
286
+ });
287
+ } else {
288
+ context.report({
289
+ node: node.parent,
290
+ messageId: "nonAtomicObjectUpdate",
291
+ data: {
292
+ value: sourceCode.getText(node.parent.left),
293
+ object: variable.name
294
+ }
295
+ });
296
+ }
297
+
285
298
  }
286
299
  }
287
300
  }
package/lib/rules/semi.js CHANGED
@@ -77,6 +77,8 @@ module.exports = {
77
77
  create(context) {
78
78
 
79
79
  const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-`
80
+ const unsafeClassFieldNames = new Set(["get", "set", "static"]);
81
+ const unsafeClassFieldFollowers = new Set(["*", "in", "instanceof"]);
80
82
  const options = context.options[1];
81
83
  const never = context.options[0] === "never";
82
84
  const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock);
@@ -165,6 +167,55 @@ module.exports = {
165
167
  );
166
168
  }
167
169
 
170
+ /**
171
+ * Checks if a given PropertyDefinition node followed by a semicolon
172
+ * can safely remove that semicolon. It is not to safe to remove if
173
+ * the class field name is "get", "set", or "static", or if
174
+ * followed by a generator method.
175
+ * @param {ASTNode} node The node to check.
176
+ * @returns {boolean} `true` if the node cannot have the semicolon
177
+ * removed.
178
+ */
179
+ function maybeClassFieldAsiHazard(node) {
180
+
181
+ if (node.type !== "PropertyDefinition") {
182
+ return false;
183
+ }
184
+
185
+ /*
186
+ * Computed property names and non-identifiers are always safe
187
+ * as they can be distinguished from keywords easily.
188
+ */
189
+ const needsNameCheck = !node.computed && node.key.type === "Identifier";
190
+
191
+ /*
192
+ * Certain names are problematic unless they also have a
193
+ * a way to distinguish between keywords and property
194
+ * names.
195
+ */
196
+ if (needsNameCheck && unsafeClassFieldNames.has(node.key.name)) {
197
+
198
+ /*
199
+ * Special case: If the field name is `static`,
200
+ * it is only valid if the field is marked as static,
201
+ * so "static static" is okay but "static" is not.
202
+ */
203
+ const isStaticStatic = node.static && node.key.name === "static";
204
+
205
+ /*
206
+ * For other unsafe names, we only care if there is no
207
+ * initializer. No initializer = hazard.
208
+ */
209
+ if (!isStaticStatic && !node.value) {
210
+ return true;
211
+ }
212
+ }
213
+
214
+ const followingToken = sourceCode.getTokenAfter(node);
215
+
216
+ return unsafeClassFieldFollowers.has(followingToken.value);
217
+ }
218
+
168
219
  /**
169
220
  * Check whether a given node is on the same line with the next token.
170
221
  * @param {Node} node A statement node to check.
@@ -203,9 +254,6 @@ module.exports = {
203
254
  if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) {
204
255
  return false;
205
256
  }
206
- if (t === "PropertyDefinition") {
207
- return Boolean(t.value);
208
- }
209
257
 
210
258
  return true;
211
259
  }
@@ -235,10 +283,19 @@ module.exports = {
235
283
  if (isRedundantSemi(sourceCode.getLastToken(node))) {
236
284
  return true; // `;;` or `;}`
237
285
  }
286
+ if (maybeClassFieldAsiHazard(node)) {
287
+ return false;
288
+ }
238
289
  if (isOnSameLineWithNextToken(node)) {
239
290
  return false; // One liner.
240
291
  }
241
- if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) {
292
+
293
+ // continuation characters should not apply to class fields
294
+ if (
295
+ node.type !== "PropertyDefinition" &&
296
+ beforeStatementContinuationChars === "never" &&
297
+ !maybeAsiHazardAfter(node)
298
+ ) {
242
299
  return true; // ASI works. This statement doesn't connect to the next.
243
300
  }
244
301
  if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
@@ -278,7 +335,11 @@ module.exports = {
278
335
  if (never) {
279
336
  if (isSemi && canRemoveSemicolon(node)) {
280
337
  report(node, true);
281
- } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
338
+ } else if (
339
+ !isSemi && beforeStatementContinuationChars === "always" &&
340
+ node.type !== "PropertyDefinition" &&
341
+ maybeAsiHazardBefore(sourceCode.getTokenAfter(node))
342
+ ) {
282
343
  report(node);
283
344
  }
284
345
  } else {
@@ -107,13 +107,25 @@ module.exports = {
107
107
  * Checks whether the spacing before the given block is already controlled by another rule:
108
108
  * - `arrow-spacing` checks spaces after `=>`.
109
109
  * - `keyword-spacing` checks spaces after keywords in certain contexts.
110
+ * - `switch-colon-spacing` checks spaces after `:` of switch cases.
110
111
  * @param {Token} precedingToken first token before the block.
111
112
  * @param {ASTNode|Token} node `BlockStatement` node or `{` token of a `SwitchStatement` node.
112
113
  * @returns {boolean} `true` if requiring or disallowing spaces before the given block could produce conflicts with other rules.
113
114
  */
114
115
  function isConflicted(precedingToken, node) {
115
- return astUtils.isArrowToken(precedingToken) ||
116
- astUtils.isKeywordToken(precedingToken) && !isFunctionBody(node);
116
+ return (
117
+ astUtils.isArrowToken(precedingToken) ||
118
+ (
119
+ astUtils.isKeywordToken(precedingToken) &&
120
+ !isFunctionBody(node)
121
+ ) ||
122
+ (
123
+ astUtils.isColonToken(precedingToken) &&
124
+ node.parent &&
125
+ node.parent.type === "SwitchCase" &&
126
+ precedingToken === astUtils.getSwitchCaseColonToken(node.parent, sourceCode)
127
+ )
128
+ );
117
129
  }
118
130
 
119
131
  /**
@@ -50,18 +50,6 @@ module.exports = {
50
50
  const beforeSpacing = options.before === true; // false by default
51
51
  const afterSpacing = options.after !== false; // true by default
52
52
 
53
- /**
54
- * Get the colon token of the given SwitchCase node.
55
- * @param {ASTNode} node The SwitchCase node to get.
56
- * @returns {Token} The colon token of the node.
57
- */
58
- function getColonToken(node) {
59
- if (node.test) {
60
- return sourceCode.getTokenAfter(node.test, astUtils.isColonToken);
61
- }
62
- return sourceCode.getFirstToken(node, 1);
63
- }
64
-
65
53
  /**
66
54
  * Check whether the spacing between the given 2 tokens is valid or not.
67
55
  * @param {Token} left The left token to check.
@@ -114,7 +102,7 @@ module.exports = {
114
102
 
115
103
  return {
116
104
  SwitchCase(node) {
117
- const colonToken = getColonToken(node);
105
+ const colonToken = astUtils.getSwitchCaseColonToken(node, sourceCode);
118
106
  const beforeToken = sourceCode.getTokenBefore(colonToken);
119
107
  const afterToken = sourceCode.getTokenAfter(colonToken);
120
108
 
@@ -756,6 +756,19 @@ function isLogicalAssignmentOperator(operator) {
756
756
  return LOGICAL_ASSIGNMENT_OPERATORS.has(operator);
757
757
  }
758
758
 
759
+ /**
760
+ * Get the colon token of the given SwitchCase node.
761
+ * @param {ASTNode} node The SwitchCase node to get.
762
+ * @param {SourceCode} sourceCode The source code object to get tokens.
763
+ * @returns {Token} The colon token of the node.
764
+ */
765
+ function getSwitchCaseColonToken(node, sourceCode) {
766
+ if (node.test) {
767
+ return sourceCode.getTokenAfter(node.test, isColonToken);
768
+ }
769
+ return sourceCode.getFirstToken(node, 1);
770
+ }
771
+
759
772
  //------------------------------------------------------------------------------
760
773
  // Public Interface
761
774
  //------------------------------------------------------------------------------
@@ -1872,5 +1885,6 @@ module.exports = {
1872
1885
  isSpecificMemberAccess,
1873
1886
  equalLiteralValue,
1874
1887
  isSameReference,
1875
- isLogicalAssignmentOperator
1888
+ isLogicalAssignmentOperator,
1889
+ getSwitchCaseColonToken
1876
1890
  };
@@ -21,7 +21,7 @@ module.exports = {};
21
21
  /**
22
22
  * @typedef {Object} ParserOptions
23
23
  * @property {EcmaFeatures} [ecmaFeatures] The optional features.
24
- * @property {3|5|6|7|8|9|10|11|12|2015|2016|2017|2018|2019|2020|2021} [ecmaVersion] The ECMAScript version (or revision number).
24
+ * @property {3|5|6|7|8|9|10|11|12|13|2015|2016|2017|2018|2019|2020|2021|2022} [ecmaVersion] The ECMAScript version (or revision number).
25
25
  * @property {"script"|"module"} [sourceType] The source code type.
26
26
  */
27
27
 
@@ -83,12 +83,12 @@ module.exports = {};
83
83
 
84
84
  /**
85
85
  * @typedef {Object} LintMessage
86
- * @property {number} column The 1-based column number.
86
+ * @property {number|undefined} column The 1-based column number.
87
87
  * @property {number} [endColumn] The 1-based column number of the end location.
88
88
  * @property {number} [endLine] The 1-based line number of the end location.
89
89
  * @property {boolean} fatal If `true` then this is a fatal error.
90
90
  * @property {{range:[number,number], text:string}} [fix] Information for autofix.
91
- * @property {number} line The 1-based line number.
91
+ * @property {number|undefined} line The 1-based line number.
92
92
  * @property {string} message The error message.
93
93
  * @property {string|null} ruleId The ID of the rule which makes this message.
94
94
  * @property {0|1|2} severity The severity of this message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.0.0-beta.1",
3
+ "version": "8.0.1",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "homepage": "https://eslint.org",
48
48
  "bugs": "https://github.com/eslint/eslint/issues/",
49
49
  "dependencies": {
50
- "@eslint/eslintrc": "^1.0.0",
50
+ "@eslint/eslintrc": "^1.0.3",
51
51
  "@humanwhocodes/config-array": "^0.6.0",
52
52
  "ajv": "^6.10.0",
53
53
  "chalk": "^4.0.0",
@@ -59,7 +59,7 @@
59
59
  "eslint-scope": "^6.0.0",
60
60
  "eslint-utils": "^3.0.0",
61
61
  "eslint-visitor-keys": "^3.0.0",
62
- "espree": "^8.0.0",
62
+ "espree": "^9.0.0",
63
63
  "esquery": "^1.4.0",
64
64
  "esutils": "^2.0.2",
65
65
  "fast-deep-equal": "^3.1.3",