Repository: eslint/eslint (js)

Issue:
allowAfterThisConstructor for no-underscore-dangle
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for requesting a rule change. If you are here for another reason, please see below:

    1. To report a bug: https://eslint.org/docs/developer-guide/contributing/reporting-bugs
    2. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    3. To request a change that is not a bug fix, rule change, or new rule: https://eslint.org/docs/developer-guide/contributing/changes
    4. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

Like many, I've been using the `no-underscore-dangle` method to enforce the style of prefixing "private" members with underscores. It's nice in that it flags anytime such properties are accessed, and the `allowAfterThis` and `allowAfterSuper` rules make possible to work with these properties inside a class.

I have found an additional case that would be nice to allow: when an instance method or constructor wants to invoke a "private" static method. To make this possible without having to disable the rule on a case-by-case basis, I'd like to add an option to allow underscore dangles when referencing properties on `this.constructor`.
 
**What rule do you want to change?**

`no-underscore-dangle`

**Does this change cause the rule to produce more or fewer warnings?**

Fewer, optionally.

**How will the change be implemented? (New option, new default behavior, etc.)?**

New option.

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```js
var a = this.constructor.foo_;
this.constructor._bar();
```

**What does the rule currently do for this code?**

Produces the following warnings/errors:
`Unexpected dangling '_' in 'foo_'
`Unexpected dangling '_' in '_bar'

**What will the rule do after it's changed?**
Allows suppression in this case, if new `allowAfterThisConstructor` option is set to true.

**Are you willing to submit a pull request to implement this change?**

Yes, will be creating it shortly.


Pull request description:
New: allowAfterThisConstructor for no-underscore-dangle (fixes #11488)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] 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))
[x] 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:

<!--
    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)**

Implemented a new option for `no_underscore_dangle` rule

**Is there anything you'd like reviewers to focus on?**

See rationale in issue.


Patch:
diff --git a/docs/rules/no-underscore-dangle.md b/docs/rules/no-underscore-dangle.md
index e112c7a1b..ebe7fd7b6 100644
--- a/docs/rules/no-underscore-dangle.md
+++ b/docs/rules/no-underscore-dangle.md
@@ -42,6 +42,7 @@ This rule has an object option:
 * `"allow"` allows specified identifiers to have dangling underscores
 * `"allowAfterThis": false` (default) disallows dangling underscores in members of the `this` object
 * `"allowAfterSuper": false` (default) disallows dangling underscores in members of the `super` object
+* `"allowAfterThisConstructor": false` (default) disallows dangling underscores in members of the `this.constructor` object
 * `"enforceInMethodNames": false` (default) allows dangling underscores in method names
 
 ### allow
@@ -77,6 +78,17 @@ var a = super.foo_;
 super._bar();
 ```
 
+### allowAfterThisConstructor
+
+Examples of **correct** code for this rule with the `{ "allowAfterThisConstructor": true }` option:
+
+```js
+/*eslint no-underscore-dangle: ["error", { "allowAfterThisConstructor": true }]*/
+
+var a = this.constructor.foo_;
+this.constructor._bar();
+```
+
 ### enforceInMethodNames
 
 Examples of **incorrect** code for this rule with the `{ "enforceInMethodNames": true }` option:
diff --git a/lib/rules/no-underscore-dangle.js b/lib/rules/no-underscore-dangle.js
index 3f59815b5..e910e2739 100644
--- a/lib/rules/no-underscore-dangle.js
+++ b/lib/rules/no-underscore-dangle.js
@@ -38,6 +38,10 @@ module.exports = {
                         type: "boolean",
                         default: false
                     },
+                    allowAfterThisConstructor: {
+                        type: "boolean",
+                        default: false
+                    },
                     enforceInMethodNames: {
                         type: "boolean",
                         default: false
@@ -54,6 +58,7 @@ module.exports = {
         const ALLOWED_VARIABLES = options.allow ? options.allow : [];
         const allowAfterThis = typeof options.allowAfterThis !== "undefined" ? options.allowAfterThis : false;
         const allowAfterSuper = typeof options.allowAfterSuper !== "undefined" ? options.allowAfterSuper : false;
+        const allowAfterThisConstructor = typeof options.allowAfterThisConstructor !== "undefined" ? options.allowAfterThisConstructor : false;
         const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false;
 
         //-------------------------------------------------------------------------
@@ -72,7 +77,7 @@ module.exports = {
 
         /**
          * Check if identifier has a underscore at the end
-         * @param {ASTNode} identifier node to evaluate
+         * @param {string} identifier name of the node
          * @returns {boolean} true if its is present
          * @private
          */
@@ -84,7 +89,7 @@ module.exports = {
 
         /**
          * Check if identifier is a special case member expression
-         * @param {ASTNode} identifier node to evaluate
+         * @param {string} identifier name of the node
          * @returns {boolean} true if its is a special case
          * @private
          */
@@ -94,7 +99,7 @@ module.exports = {
 
         /**
          * Check if identifier is a special case variable expression
-         * @param {ASTNode} identifier node to evaluate
+         * @param {string} identifier name of the node
          * @returns {boolean} true if its is a special case
          * @private
          */
@@ -104,6 +109,18 @@ module.exports = {
             return identifier === "_";
         }
 
+        /**
+         * Check if a node is a member reference of this.constructor
+         * @param {ASTNode} node node to evaluate
+         * @returns {boolean} true if it is a reference on this.constructor
+         * @private
+         */
+        function isThisConstructorReference(node) {
+            return node.object.type === "MemberExpression" &&
+                node.object.property.name === "constructor" &&
+                node.object.object.type === "ThisExpression";
+        }
+
         /**
          * Check if function has a underscore at the end
          * @param {ASTNode} node node to evaluate
@@ -156,11 +173,13 @@ module.exports = {
         function checkForTrailingUnderscoreInMemberExpression(node) {
             const identifier = node.property.name,
                 isMemberOfThis = node.object.type === "ThisExpression",
-                isMemberOfSuper = node.object.type === "Super";
+                isMemberOfSuper = node.object.type === "Super",
+                isMemberOfThisConstructor = isThisConstructorReference(node);
 
             if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) &&
                 !(isMemberOfThis && allowAfterThis) &&
                 !(isMemberOfSuper && allowAfterSuper) &&
+                !(isMemberOfThisConstructor && allowAfterThisConstructor) &&
                 !isSpecialCaseIdentifierForMemberExpression(identifier) && !isAllowed(identifier)) {
                 context.report({
                     node,


Test patch:
diff --git a/tests/lib/rules/no-underscore-dangle.js b/tests/lib/rules/no-underscore-dangle.js
index 277c6c85c..b3774dc18 100644
--- a/tests/lib/rules/no-underscore-dangle.js
+++ b/tests/lib/rules/no-underscore-dangle.js
@@ -38,7 +38,8 @@ ruleTester.run("no-underscore-dangle", rule, {
         { code: "const o = { _onClick() { } }", parserOptions: { ecmaVersion: 6 } },
         { code: "const o = { onClick_() { } }", parserOptions: { ecmaVersion: 6 } },
         { code: "const o = { _foo: 'bar' }", parserOptions: { ecmaVersion: 6 } },
-        { code: "const o = { foo_: 'bar' }", parserOptions: { ecmaVersion: 6 } }
+        { code: "const o = { foo_: 'bar' }", parserOptions: { ecmaVersion: 6 } },
+        { code: "this.constructor._bar", options: [{ allowAfterThisConstructor: true }] }
     ],
     invalid: [
         { code: "var _foo = 1", errors: [{ message: "Unexpected dangling '_' in '_foo'.", type: "VariableDeclarator" }] },
@@ -53,6 +54,8 @@ ruleTester.run("no-underscore-dangle", rule, {
         { code: "class foo { _onClick() { } }", options: [{ enforceInMethodNames: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Unexpected dangling '_' in '_onClick'.", type: "MethodDefinition" }] },
         { code: "class foo { onClick_() { } }", options: [{ enforceInMethodNames: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Unexpected dangling '_' in 'onClick_'.", type: "MethodDefinition" }] },
         { code: "const o = { _onClick() { } }", options: [{ enforceInMethodNames: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Unexpected dangling '_' in '_onClick'.", type: "Property" }] },
-        { code: "const o = { onClick_() { } }", options: [{ enforceInMethodNames: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Unexpected dangling '_' in 'onClick_'.", type: "Property" }] }
+        { code: "const o = { onClick_() { } }", options: [{ enforceInMethodNames: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Unexpected dangling '_' in 'onClick_'.", type: "Property" }] },
+        { code: "this.constructor._bar", errors: [{ message: "Unexpected dangling '_' in '_bar'.", type: "MemberExpression" }] },
+        { code: "foo.constructor._bar", options: [{ allowAfterThisConstructor: true }], errors: [{ message: "Unexpected dangling '_' in '_bar'.", type: "MemberExpression" }] }
     ]
 });