Repository: eslint/eslint
Pull request #14898 (2021-08-06 21:41:08)

Fix: dot-notation false positive with private identifier (refs #14857)
<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/master/CONTRIBUTING.md).

#### What is the purpose of this pull request? (put an "X" next to an item)

<!--
    The following template is intentionally not a markdown checkbox list for the reasons
    explained in https://github.com/eslint/eslint/pull/12848#issuecomment-580302888
-->

[ ] Documentation update
[x] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

refs #14857, fixes https://github.com/eslint/eslint/pull/14591#issuecomment-866334741

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What changes did you make? (Give an overview)

Fixed `allowKeywords: false` option of the `dot-notation` rule to check only `Identifier` properties.


#### Is there anything you'd like reviewers to focus on?



Patch:
diff --git a/.markdownlintignore b/.markdownlintignore
new file mode 100644
index 000000000..1b763b1ba
--- /dev/null
+++ b/.markdownlintignore
@@ -0,0 +1,1 @@
+CHANGELOG.md
diff --git a/Makefile.js b/Makefile.js
index e84d21abf..b93697857 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -66,7 +66,8 @@ const NODE = "node ", // intentional extra space
     // Files
     RULE_FILES = glob.sync("lib/rules/*.js").filter(filePath => path.basename(filePath) !== "index.js"),
     JSON_FILES = find("conf/").filter(fileType("json")),
-    MARKDOWN_FILES_ARRAY = find("docs/").concat(ls(".")).filter(fileType("md")),
+    MARKDOWNLINT_IGNORED_FILES = fs.readFileSync(path.join(__dirname, ".markdownlintignore"), "utf-8").split("\n"),
+    MARKDOWN_FILES_ARRAY = find("docs/").concat(ls(".")).filter(fileType("md")).filter(file => !MARKDOWNLINT_IGNORED_FILES.includes(file)),
     TEST_FILES = "\"tests/{bin,lib,tools}/**/*.js\"",
     PERF_ESLINTRC = path.join(PERF_TMP_DIR, "eslintrc.yml"),
     PERF_MULTIFILES_TARGET_DIR = path.join(PERF_TMP_DIR, "eslint"),
diff --git a/README.md b/README.md
index a429bdf84..3082aeffb 100644
--- a/README.md
+++ b/README.md
@@ -298,7 +298,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <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>
 <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>
 <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>
-<p><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>
+<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>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/docs/rules/dot-notation.md b/docs/rules/dot-notation.md
index a13049a3f..26c77756c 100644
--- a/docs/rules/dot-notation.md
+++ b/docs/rules/dot-notation.md
@@ -46,6 +46,19 @@ var foo = { "class": "CS 101" }
 var x = foo["class"]; // Property name is a reserved word, square-bracket notation required
 ```
 
+Examples of additional **correct** code for the `{ "allowKeywords": false }` option:
+
+```js
+/*eslint dot-notation: ["error", { "allowKeywords": false }]*/
+
+class C {
+    #in;
+    foo() {
+        this.#in; // Dot notation is required for private identifiers
+    }
+}
+```
+
 ### allowPattern
 
 For example, when preparing data to be sent to an external API, it is often required to use property names that include underscores.  If the `camelcase` rule is in effect, these [snake case](https://en.wikipedia.org/wiki/Snake_case) properties would not be allowed.  By providing an `allowPattern` to the `dot-notation` rule, these snake case properties can be accessed with bracket notation.
diff --git a/docs/rules/func-names.md b/docs/rules/func-names.md
index 27b9cc20f..ea1ba617e 100644
--- a/docs/rules/func-names.md
+++ b/docs/rules/func-names.md
@@ -17,14 +17,14 @@ This rule can enforce or disallow the use of named function expressions.
 This rule has a string option:
 
 * `"always"` (default) requires function expressions to have a name
-* `"as-needed"` requires function expressions to have a name, if the name cannot be assigned automatically in an ES6 environment
+* `"as-needed"` requires function expressions to have a name, if the name isn't assigned automatically per the ECMAScript specification.
 * `"never"` disallows named function expressions, except in recursive functions, where a name is needed
 
 This rule has an object option:
 
 * `"generators": "always" | "as-needed" | "never"`
     * `"always"` require named generators
-    * `"as-needed"` require named generators if the name cannot be assigned automatically in an ES6 environment.
+    * `"as-needed"` require named generators if the name isn't assigned automatically per the ECMAScript specification.
     * `"never"` disallow named generators where possible.
 
 When a value for `generators` is not provided the behavior for generator functions falls back to the base option.
@@ -98,6 +98,13 @@ const cat = {
   meow: function() {}
 }
 
+class C {
+    #bar = function() {};
+    baz = function() {};
+}
+
+quux ??= function() {};
+
 (function bar() {
     // ...
 }())
diff --git a/docs/rules/no-console.md b/docs/rules/no-console.md
index f2fb79ec4..b1e4e601c 100644
--- a/docs/rules/no-console.md
+++ b/docs/rules/no-console.md
@@ -9,7 +9,7 @@ console.error("That shouldn't have happened.");
 
 ## Rule Details
 
-This rule disallows calls to methods of the `console` object.
+This rule disallows calls or assignments to methods of the `console` object.
 
 Examples of **incorrect** code for this rule:
 
@@ -19,6 +19,7 @@ Examples of **incorrect** code for this rule:
 console.log("Log a debug level message.");
 console.warn("Log a warn level message.");
 console.error("Log an error level message.");
+console.log = foo();
 ```
 
 Examples of **correct** code for this rule:
diff --git a/docs/rules/no-extra-parens.md b/docs/rules/no-extra-parens.md
index 68b986eff..b521dcbe2 100644
--- a/docs/rules/no-extra-parens.md
+++ b/docs/rules/no-extra-parens.md
@@ -48,6 +48,14 @@ for (a of (b));
 typeof (a);
 
 (function(){} ? a() : b());
+
+class A {
+    [(x)] = 1;
+}
+
+class B {
+    x = (y + z);
+}
 ```
 
 Examples of **correct** code for this rule with the default `"all"` option:
@@ -72,6 +80,14 @@ for (a of b);
 for (a in b, c);
 
 for (a in b);
+
+class A {
+    [x] = 1;
+}
+
+class B {
+    x = y + z;
+}
 ```
 
 ### conditionalAssign
diff --git a/docs/rules/no-multi-assign.md b/docs/rules/no-multi-assign.md
index 7ef705675..14721dab0 100644
--- a/docs/rules/no-multi-assign.md
+++ b/docs/rules/no-multi-assign.md
@@ -26,12 +26,19 @@ const foo = bar = "baz";
 let a =
     b =
     c;
+
+class Foo {
+    a = b = 10;
+}
+
+a = b = "quux";
 ```
 
 Examples of **correct** code for this rule:
 
 ```js
 /*eslint no-multi-assign: "error"*/
+
 var a = 5;
 var b = 5;
 var c = 5;
@@ -41,13 +48,21 @@ const bar = "baz";
 
 let a = c;
 let b = c;
+
+class Foo {
+    a = 10;
+    b = 10;
+}
+
+a = "quux";
+b = "quux";
 ```
 
 ## Options
 
 This rule has an object option:
 
-* `"ignoreNonDeclaration"`: When set to `true`, the rule allows chains that don't include initializing a variable in a declaration. Default is `false`.
+* `"ignoreNonDeclaration"`: When set to `true`, the rule allows chains that don't include initializing a variable in a declaration or initializing a class field. Default is `false`.
 
 ### ignoreNonDeclaration
 
@@ -73,6 +88,10 @@ Examples of **incorrect** code for the `{ "ignoreNonDeclaration": true }` option
 let a = b = "baz";
 
 const foo = bar = 1;
+
+class Foo {
+    a = b = 10;
+}
 ```
 
 ## Related Rules
diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js
index 3aa9f3110..20434f359 100644
--- a/lib/rules/dot-notation.js
+++ b/lib/rules/dot-notation.js
@@ -141,6 +141,7 @@ module.exports = {
                 if (
                     !allowKeywords &&
                     !node.computed &&
+                    node.property.type === "Identifier" &&
                     keywords.indexOf(String(node.property.name)) !== -1
                 ) {
                     context.report({
diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js
index 307e340c9..d06335046 100644
--- a/lib/rules/no-extra-parens.js
+++ b/lib/rules/no-extra-parens.js
@@ -808,13 +808,6 @@ module.exports = {
 
             CallExpression: checkCallNew,
 
-            ClassBody(node) {
-                node.body
-                    .filter(member => member.type === "MethodDefinition" && member.computed && member.key)
-                    .filter(member => hasExcessParensWithPrecedence(member.key, PRECEDENCE_OF_ASSIGNMENT_EXPR))
-                    .forEach(member => report(member.key));
-            },
-
             ConditionalExpression(node) {
                 if (isReturnAssignException(node)) {
                     return;
@@ -1063,6 +1056,12 @@ module.exports = {
                 }
             },
 
+            "MethodDefinition[computed=true]"(node) {
+                if (hasExcessParensWithPrecedence(node.key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
+                    report(node.key);
+                }
+            },
+
             NewExpression: checkCallNew,
 
             ObjectExpression(node) {
@@ -1090,6 +1089,16 @@ module.exports = {
                 }
             },
 
+            PropertyDefinition(node) {
+                if (node.computed && hasExcessParensWithPrecedence(node.key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
+                    report(node.key);
+                }
+
+                if (node.value && hasExcessParensWithPrecedence(node.value, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
+                    report(node.value);
+                }
+            },
+
             RestElement(node) {
                 const argument = node.argument;
 
diff --git a/package.json b/package.json
index d4f699bba..f0cd1bf67 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
   "bugs": "https://github.com/eslint/eslint/issues/",
   "dependencies": {
     "@eslint/eslintrc": "^0.4.3",
-    "@humanwhocodes/config-array": "^0.5.0",
+    "@humanwhocodes/config-array": "^0.6.0",
     "ajv": "^6.10.0",
     "chalk": "^4.0.0",
     "cross-spawn": "^7.0.2",
@@ -59,7 +59,7 @@
     "escape-string-regexp": "^4.0.0",
     "eslint-scope": "^6.0.0",
     "eslint-utils": "^3.0.0",
-    "eslint-visitor-keys": "^2.1.0",
+    "eslint-visitor-keys": "^3.0.0",
     "espree": "^8.0.0",
     "esquery": "^1.4.0",
     "esutils": "^2.0.2",
@@ -116,8 +116,8 @@
     "karma-webpack": "^5.0.0",
     "lint-staged": "^11.0.0",
     "load-perf": "^0.2.0",
-    "markdownlint": "^0.19.0",
-    "markdownlint-cli": "^0.22.0",
+    "markdownlint": "^0.23.1",
+    "markdownlint-cli": "^0.28.1",
     "memfs": "^3.0.1",
     "mocha": "^8.3.2",
     "mocha-junit-reporter": "^2.0.0",


Test patch:
diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js
index fed94c128..249faf80e 100644
--- a/tests/lib/rules/dot-notation.js
+++ b/tests/lib/rules/dot-notation.js
@@ -59,7 +59,12 @@ ruleTester.run("dot-notation", rule, {
         "a[void 0];",
         "a[b()];",
         { code: "a[/(?<zero>0)/];", parserOptions: { ecmaVersion: 2018 } },
-        { code: "class C { foo() { this['#a'] } }", parserOptions: { ecmaVersion: 2022 } }
+        { code: "class C { foo() { this['#a'] } }", parserOptions: { ecmaVersion: 2022 } },
+        {
+            code: "class C { #in; foo() { this.#in; } }",
+            options: [{ allowKeywords: false }],
+            parserOptions: { ecmaVersion: 2022 }
+        }
     ],
     invalid: [
         {
diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js
index a14ececc0..5a4dba648 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -44,7 +44,7 @@ function invalid(code, output, type, line, config) {
 
 const ruleTester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2021,
+        ecmaVersion: 2022,
         ecmaFeatures: {
             jsx: true
         }
@@ -182,6 +182,25 @@ ruleTester.run("no-extra-parens", rule, {
         "class foo { a(){} [b](){} c(){} [(d,e)](){} }",
         "class foo { [(a,b)](){} c(){} [d](){} e(){} }",
         "const foo = class { constructor(){} a(){} get b(){} set b(bar){} get c(){} set d(baz){} static e(){} }",
+        "class foo { x; }",
+        "class foo { static x; }",
+        "class foo { x = 1; }",
+        "class foo { static x = 1; }",
+        "class foo { #x; }",
+        "class foo { static #x; }",
+        "class foo { static #x = 1; }",
+        "class foo { #x(){} get #y() {} set #y(value) {} static #z(){} static get #q() {} static set #q(value) {} }",
+        "const foo  = class { #x(){} get #y() {} set #y(value) {} static #z(){} static get #q() {} static set #q(value) {} }",
+        "class foo { [(x, y)]; }",
+        "class foo { static [(x, y)]; }",
+        "class foo { [(x, y)] = 1; }",
+        "class foo { static [(x, y)] = 1; }",
+        "class foo { x = (y, z); }",
+        "class foo { static x = (y, z); }",
+        "class foo { #x = (y, z); }",
+        "class foo { static #x = (y, z); }",
+        "class foo { [(1, 2)] = (3, 4) }",
+        "const foo = class { [(1, 2)] = (3, 4) }",
 
         // ExpressionStatement restricted productions
         "({});",
@@ -771,6 +790,26 @@ ruleTester.run("no-extra-parens", rule, {
         invalid("class foo { [(a,b)](){} [(c+d)](){} }", "class foo { [(a,b)](){} [c+d](){} }", "BinaryExpression"),
         invalid("class foo { [a+(b*c)](){} }", "class foo { [a+b*c](){} }", "BinaryExpression"),
         invalid("const foo = class { [(a)](){} }", "const foo = class { [a](){} }", "Identifier"),
+        invalid("class foo { [(x)]; }", "class foo { [x]; }", "Identifier"),
+        invalid("class foo { static [(x)]; }", "class foo { static [x]; }", "Identifier"),
+        invalid("class foo { [(x)] = 1; }", "class foo { [x] = 1; }", "Identifier"),
+        invalid("class foo { static [(x)] = 1; }", "class foo { static [x] = 1; }", "Identifier"),
+        invalid("const foo = class { [(x)]; }", "const foo = class { [x]; }", "Identifier"),
+        invalid("class foo { [(x = y)]; }", "class foo { [x = y]; }", "AssignmentExpression"),
+        invalid("class foo { [(x + y)]; }", "class foo { [x + y]; }", "BinaryExpression"),
+        invalid("class foo { [(x ? y : z)]; }", "class foo { [x ? y : z]; }", "ConditionalExpression"),
+        invalid("class foo { [((x, y))]; }", "class foo { [(x, y)]; }", "SequenceExpression"),
+        invalid("class foo { x = (y); }", "class foo { x = y; }", "Identifier"),
+        invalid("class foo { static x = (y); }", "class foo { static x = y; }", "Identifier"),
+        invalid("class foo { #x = (y); }", "class foo { #x = y; }", "Identifier"),
+        invalid("class foo { static #x = (y); }", "class foo { static #x = y; }", "Identifier"),
+        invalid("const foo = class { x = (y); }", "const foo = class { x = y; }", "Identifier"),
+        invalid("class foo { x = (() => {}); }", "class foo { x = () => {}; }", "ArrowFunctionExpression"),
+        invalid("class foo { x = (y + z); }", "class foo { x = y + z; }", "BinaryExpression"),
+        invalid("class foo { x = (y ? z : q); }", "class foo { x = y ? z : q; }", "ConditionalExpression"),
+        invalid("class foo { x = ((y, z)); }", "class foo { x = (y, z); }", "SequenceExpression"),
+
+        //
         invalid(
             "var foo = (function*() { if ((yield foo())) { return; } }())",
             "var foo = (function*() { if (yield foo()) { return; } }())",