Repository: nzakas/eslint-plugin-typescript
Pull request #67 (2017-09-05 18:22:09)

New: Add member-naming
A new rule to enforce naming conventions for class members by visibility.

It can be helpful to enforce naming conventions for `private` (and sometimes `protected`) members of an object. For example, prefixing private properties with a `_` allows them to be easily discerned when being inspected by tools that do not have knowledge of TypeScript (such as most debuggers).

weirdpattern: Hi @nevir, can interfaces really declare accessibility modifiers in TypeScript?
I get a compilation error when I do this: [Interface test](https://www.typescriptlang.org/play/#src=interface%20Interface%20%7B%0D%0A%20%20%20implicitPublicDeclaration%3A%20number%3B%0D%0A%20%20%20public%20publicDeclaration%3A%20number%3B%0D%0A%20%20%20protected%20protectedDeclaration%3A%20number%3B%0D%0A%20%20%20private%20privateDeclaration%3A%20number%3B%0D%0A%0D%0A%20%20%20implicitPublicMethod()%3A%20void%3B%0D%0A%20%20%20public%20publicMethod()%3A%20void%3B%0D%0A%20%20%20protected%20protectedMethod()%3A%20void%3B%0D%0A%20%20%20private%20privateMethod()%3A%20void%3B%0D%0A%20%7D)
nevir: @weirdpattern you're right - I'm completely crazy - removing that!
weirdpattern: It's all good, that's why we have code reviews  :smile: 
nevir: yup! :P

Patch:
diff --git a/README.md b/README.md
index 42c1ad7..278e764 100644
--- a/README.md
+++ b/README.md
@@ -66,3 +66,4 @@ Then configure the rules you want to use under the rules section.
 * [`typescript/class-name-casing`](./docs/rules/adjacent-overload-signatures.md) - enforces PascalCased class and interface names. (`class-name` from TSLint)
 * [`typescript/member-delimiter-style`](./docs/rules/member-delimiter-style.md) - enforces a member delimiter style in interfaces and type literals.
 * [`typescript/no-empty-interface`](./docs/rules/no-empty-interface.md) - disallows the declaration of empty interfaces. (`no-empty-interface` from TSLint)
+* [`typescript/member-naming`](./docs/rules/member-naming.md) - enforces naming conventions for class members by visibility
diff --git a/docs/rules/member-naming.md b/docs/rules/member-naming.md
new file mode 100644
index 0000000..6843548
--- /dev/null
+++ b/docs/rules/member-naming.md
@@ -0,0 +1,39 @@
+# Enforces naming conventions for class members by visibility.
+
+It can be helpful to enforce naming conventions for `private` (and sometimes `protected`) members of an object. For example, prefixing private properties with a `_` allows them to be easily discerned when being inspected by tools that do not have knowledge of TypeScript (such as most debuggers).
+
+## Rule Details
+
+This rule allows you to enforce conventions for class property names by their visibility.  By default, it enforces nothing.
+
+## Options
+
+You can specify a regular expression to test the names of properties for each visibility level: `public`, `protected`, `private`.
+
+Examples of **correct** code with `{ "private": "^_" }` specified:
+
+```ts
+class HappyClass {
+    private _foo: string;
+    private _bar = 123;
+    private _fizz() {}
+}
+```
+
+Examples of **incorrect** code with `{ "private": "^_" }` specified:
+
+```ts
+class SadClass {
+    private foo: string;
+    private bar = 123;
+    private fizz() {}
+}
+```
+
+## When Not To Use It
+
+If you do not want to enforce per-visibility naming rules for member properties.
+
+## Further Reading
+
+* ESLint's [`camelcase` rule](https://eslint.org/docs/rules/camelcase)
diff --git a/lib/rules/member-naming.js b/lib/rules/member-naming.js
new file mode 100644
index 0000000..22e37e0
--- /dev/null
+++ b/lib/rules/member-naming.js
@@ -0,0 +1,74 @@
+/**
+ * @fileoverview Enforces naming conventions for class members by visibility.
+ * @author Ian MacLeod
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description:
+                "Enforces naming conventions for class members by visibility.",
+            category: "TypeScript"
+        },
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    public: { type: "string" },
+                    protected: { type: "string" },
+                    private: { type: "string" }
+                },
+                additionalProperties: false
+            }
+        ]
+    },
+
+    create(context) {
+        const config = context.options[0] || {};
+        const conventions = {};
+
+        for (const accessibility of Object.getOwnPropertyNames(config)) {
+            conventions[accessibility] = new RegExp(config[accessibility]);
+        }
+
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        /**
+         * Check that the property name matches the convention for its
+         * accessibility.
+         * @param {ASTNode} node the named node to evaluate.
+         * @returns {void}
+         * @private
+         */
+        function validateName(node) {
+            const name = node.key.name;
+            const accessibility = node.accessibility || "public";
+            const convention = conventions[accessibility];
+
+            if (!convention || convention.test(name)) return;
+
+            context.report({
+                node: node.key,
+                message:
+                    "{{accessibility}} property {{name}} should match {{convention}}",
+                data: { accessibility, name, convention }
+            });
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+
+        return {
+            MethodDefinition: validateName,
+            ClassProperty: validateName
+        };
+    }
+};
diff --git a/lib/rules/type-annotation-spacing.js b/lib/rules/type-annotation-spacing.js
index 0ce2f5c..7ddce84 100644
--- a/lib/rules/type-annotation-spacing.js
+++ b/lib/rules/type-annotation-spacing.js
@@ -75,7 +75,7 @@ module.exports = {
          * @private
          */
         function checkTypeAnnotationSpacing(typeAnnotation) {
-            const nextToken = typeAnnotation.typeAnnotation || typeAnnotation;
+            const nextToken = typeAnnotation;
             const punctuatorToken = sourceCode.getTokenBefore(nextToken);
             const previousToken = sourceCode.getTokenBefore(punctuatorToken);
 
@@ -140,7 +140,9 @@ module.exports = {
          */
         function checkFunctionReturnTypeSpacing(node) {
             if (node.returnType) {
-                checkTypeAnnotationSpacing(node.returnType);
+                checkTypeAnnotationSpacing(
+                    node.returnType.typeAnnotation || node.returnType
+                );
             }
         }
 
@@ -150,20 +152,17 @@ module.exports = {
         return {
             Identifier(node) {
                 if (node.typeAnnotation) {
-                    checkTypeAnnotationSpacing(node.typeAnnotation);
+                    checkTypeAnnotationSpacing(
+                        node.typeAnnotation.typeAnnotation ||
+                            node.typeAnnotation
+                    );
                 }
             },
-
             TypeAnnotation(node) {
-                if (
-                    node.typeAnnotation &&
-                    node.typeAnnotation.type !== "TSFunctionType" &&
-                    node.parent.type !== "TSAsExpression"
-                ) {
+                if (node.parent.type !== "TSAsExpression") {
                     checkTypeAnnotationSpacing(node.typeAnnotation);
                 }
             },
-
             FunctionDeclaration: checkFunctionReturnTypeSpacing,
             FunctionExpression: checkFunctionReturnTypeSpacing,
             ArrowFunctionExpression: checkFunctionReturnTypeSpacing
diff --git a/package.json b/package.json
index 7bc9e5b..9ca767c 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
   "scripts": {
     "lint": "eslint lib/ tests/",
     "lint:fix": "eslint lib/ tests/ --fix",
-    "mocha": "mocha tests --recursive",
+    "mocha": "mocha tests --recursive --reporter=dot",
     "test": "npm run lint && npm run mocha",
     "precommit": "npm test && lint-staged"
   },
@@ -21,17 +21,17 @@
     "requireindex": "~1.1.0"
   },
   "devDependencies": {
-    "eslint": "^4.5.0",
+    "eslint": "^4.6.1",
     "eslint-config-eslint": "^4.0.0",
-    "eslint-config-prettier": "^2.3.0",
+    "eslint-config-prettier": "^2.4.0",
     "eslint-plugin-node": "^5.1.1",
     "eslint-plugin-prettier": "^2.2.0",
     "husky": "^0.14.3",
-    "lint-staged": "^4.0.3",
-    "mocha": "^3.5.0",
-    "prettier": "^1.5.3",
-    "typescript": "~2.4.2",
-    "typescript-eslint-parser": "^7.0.0"
+    "lint-staged": "^4.1.3",
+    "mocha": "^3.5.3",
+    "prettier": "^1.6.1",
+    "typescript": "~2.5.1",
+    "typescript-eslint-parser": "^8.0.0"
   },
   "lint-staged": {
     "*.js": [


Test patch:
diff --git a/tests/lib/rules/member-naming.js b/tests/lib/rules/member-naming.js
new file mode 100644
index 0000000..089e9a7
--- /dev/null
+++ b/tests/lib/rules/member-naming.js
@@ -0,0 +1,275 @@
+/**
+ * @fileoverview Enforces naming conventions for class members by visibility.
+ * @author Ian MacLeod
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/member-naming"),
+    RuleTester = require("eslint").RuleTester;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+
+ruleTester.run("member-naming", rule, {
+    valid: [
+        {
+            code: `class Class { _fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ public: "^_" }]
+        },
+        {
+            code: `class Class { public _fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ public: "^_" }]
+        },
+        {
+            code: `class Class { protected _fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ protected: "^_" }]
+        },
+        {
+            code: `class Class { private _fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ private: "^_" }]
+        },
+        {
+            code: `class Class { protected fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ private: "^_" }]
+        },
+        {
+            code: `
+class Class {
+    pubOne() {}
+    public pubTwo() {}
+    protected protThree() {}
+    private privFour() {}
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ]
+        },
+        {
+            code: `
+class Class {
+    pubOne: string;
+    public pubTwo: string;
+    protected protThree: string;
+    private privFour: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ]
+        },
+        {
+            code: `
+class Class {
+    pubOne = true;
+    public pubTwo = true;
+    protected protThree = true;
+    private privFour = true;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ]
+        }
+    ],
+    invalid: [
+        {
+            code: `class Class { fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ public: "^_" }],
+            errors: [
+                {
+                    message: "public property fooBar should match /^_/",
+                    line: 1,
+                    column: 15
+                }
+            ]
+        },
+        {
+            code: `class Class { public fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ public: "^_" }],
+            errors: [
+                {
+                    message: "public property fooBar should match /^_/",
+                    line: 1,
+                    column: 22
+                }
+            ]
+        },
+        {
+            code: `class Class { protected fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ protected: "^_" }],
+            errors: [
+                {
+                    message: "protected property fooBar should match /^_/",
+                    line: 1,
+                    column: 25
+                }
+            ]
+        },
+        {
+            code: `class Class { private fooBar() {} }`,
+            parser: "typescript-eslint-parser",
+            options: [{ private: "^_" }],
+            errors: [
+                {
+                    message: "private property fooBar should match /^_/",
+                    line: 1,
+                    column: 23
+                }
+            ]
+        },
+        {
+            code: `
+class Class {
+    one() {}
+    public two() {}
+    protected three() {}
+    private four() {}
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ],
+            errors: [
+                {
+                    message: "public property one should match /^pub[A-Z]/",
+                    line: 3,
+                    column: 5
+                },
+                {
+                    message: "public property two should match /^pub[A-Z]/",
+                    line: 4,
+                    column: 12
+                },
+                {
+                    message:
+                        "protected property three should match /^prot[A-Z]/",
+                    line: 5,
+                    column: 15
+                },
+                {
+                    message: "private property four should match /^priv[A-Z]/",
+                    line: 6,
+                    column: 13
+                }
+            ]
+        },
+        {
+            code: `
+class Class {
+    one: string;
+    public two: string;
+    protected three: string;
+    private four: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ],
+            errors: [
+                {
+                    message: "public property one should match /^pub[A-Z]/",
+                    line: 3,
+                    column: 5
+                },
+                {
+                    message: "public property two should match /^pub[A-Z]/",
+                    line: 4,
+                    column: 12
+                },
+                {
+                    message:
+                        "protected property three should match /^prot[A-Z]/",
+                    line: 5,
+                    column: 15
+                },
+                {
+                    message: "private property four should match /^priv[A-Z]/",
+                    line: 6,
+                    column: 13
+                }
+            ]
+        },
+        {
+            code: `
+class Class {
+    one = true;
+    public two = true;
+    protected three = true;
+    private four = true;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [
+                {
+                    public: "^pub[A-Z]",
+                    protected: "^prot[A-Z]",
+                    private: "^priv[A-Z]"
+                }
+            ],
+            errors: [
+                {
+                    message: "public property one should match /^pub[A-Z]/",
+                    line: 3,
+                    column: 5
+                },
+                {
+                    message: "public property two should match /^pub[A-Z]/",
+                    line: 4,
+                    column: 12
+                },
+                {
+                    message:
+                        "protected property three should match /^prot[A-Z]/",
+                    line: 5,
+                    column: 15
+                },
+                {
+                    message: "private property four should match /^priv[A-Z]/",
+                    line: 6,
+                    column: 13
+                }
+            ]
+        }
+    ]
+});
diff --git a/tests/lib/rules/type-annotation-spacing.js b/tests/lib/rules/type-annotation-spacing.js
index 511196b..2400ea1 100644
--- a/tests/lib/rules/type-annotation-spacing.js
+++ b/tests/lib/rules/type-annotation-spacing.js
@@ -28,7 +28,7 @@ ruleTester.run("type-annotation-spacing", rule, {
             parser: "typescript-eslint-parser"
         },
         {
-            code: "function foo(): string {}",
+            code: "function foo(): void {}",
             parser: "typescript-eslint-parser"
         },
         {
@@ -93,6 +93,14 @@ interface Foo {
         },
         {
             code: `
+interface Foo {
+    thing: { [key in string]: number };
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
 type Foo = {
     name: string;
 }
@@ -478,7 +486,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             options: [{ after: true, before: true }],
@@ -597,7 +605,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name :string) =>string;
+    greet :(name :string) =>string;
 }
             `,
             options: [{ after: false, before: true }],
@@ -716,7 +724,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             options: [{ before: true }],
@@ -919,7 +927,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string)=>string;
+    greet : (name : string)=>string;
 }
             `,
             options: [
@@ -954,7 +962,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             options: [
@@ -997,7 +1005,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string) =>string;
+    greet : (name : string) =>string;
 }
             `,
             options: [
@@ -1016,6 +1024,157 @@ type Foo = {
                 }
             ],
             parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+interface Foo {
+    thing: { [key in string]: number };
+}
+            `,
+            options: [{ after: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+interface Foo {
+    thing: { [key in string]: number };
+}
+            `,
+            options: [{ after: true, before: false }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+interface Foo {
+    thing : { [key in string] : number };
+}
+            `,
+            options: [{ after: true, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+interface Foo {
+    thing :{ [key in string] :number };
+}
+            `,
+            options: [{ after: false, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+interface Foo {
+    thing : { [key in string] : number };
+}
+            `,
+            options: [{ before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing: { [key in string]: number };
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing: { [key in string]: number };
+}
+            `,
+            options: [{ after: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing: { [key in string]: number };
+}
+            `,
+            options: [{ after: true, before: false }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing : { [key in string] : number };
+}
+            `,
+            options: [{ after: true, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing :{ [key in string] :number };
+}
+            `,
+            options: [{ after: false, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+type Foo = {
+    thing : { [key in string] : number };
+}
+            `,
+            options: [{ before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet: (name: string) => void = {}
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet: (name: string) => void = {}
+}
+            `,
+            options: [{ after: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet: (name: string)=> void = {}
+}
+            `,
+            options: [{ after: true, before: false }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet : (name : string) => void = {}
+}
+            `,
+            options: [{ after: true, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet :(name :string) =>void = {}
+}
+            `,
+            options: [{ after: false, before: true }],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: `
+class Foo {
+    greet : (name : string) => void = {}
+}
+            `,
+            options: [{ before: true }],
+            parser: "typescript-eslint-parser"
         }
     ],
     invalid: [
@@ -2413,10 +2572,15 @@ type Foo = {
             options: [{ after: true, before: true }],
             output: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             errors: [
+                {
+                    message: "Expected a space before the ':'",
+                    line: 3,
+                    column: 10
+                },
                 {
                     message: "Expected a space before the ':'",
                     line: 3,
@@ -2432,21 +2596,21 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string)=> string;
+    greet : (name : string)=> string;
 }
             `,
             parser: "typescript-eslint-parser",
             options: [{ after: true, before: true }],
             output: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             errors: [
                 {
                     message: "Expected a space before the '=>'",
                     line: 3,
-                    column: 27
+                    column: 28
                 }
             ]
         },
@@ -2835,10 +2999,15 @@ type Foo = {
             options: [{ before: true }],
             output: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             errors: [
+                {
+                    message: "Expected a space before the ':'",
+                    line: 3,
+                    column: 10
+                },
                 {
                     message: "Expected a space before the ':'",
                     line: 3,
@@ -2854,21 +3023,21 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name : string)=> string;
+    greet : (name : string)=> string;
 }
             `,
             parser: "typescript-eslint-parser",
             options: [{ before: true }],
             output: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             errors: [
                 {
                     message: "Expected a space before the '=>'",
                     line: 3,
-                    column: 27
+                    column: 28
                 }
             ]
         },
@@ -3321,7 +3490,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name:string)=>string;
+    greet : (name:string)=>string;
 }
             `,
             options: [
@@ -3334,19 +3503,19 @@ type Foo = {
             parser: "typescript-eslint-parser",
             output: `
 type Foo = {
-    greet: (name : string)=>string;
+    greet : (name : string)=>string;
 }
             `,
             errors: [
                 {
                     message: "Expected a space after the ':'",
                     line: 3,
-                    column: 17
+                    column: 18
                 },
                 {
                     message: "Expected a space before the ':'",
                     line: 3,
-                    column: 17
+                    column: 18
                 }
             ]
         },
@@ -3396,7 +3565,7 @@ type Foo = {
         {
             code: `
 type Foo = {
-    greet: (name:string)=>string;
+    greet : (name:string)=>string;
 }
             `,
             options: [
@@ -3418,29 +3587,29 @@ type Foo = {
             parser: "typescript-eslint-parser",
             output: `
 type Foo = {
-    greet: (name : string) => string;
+    greet : (name : string) => string;
 }
             `,
             errors: [
                 {
                     message: "Expected a space after the ':'",
                     line: 3,
-                    column: 17
+                    column: 18
                 },
                 {
                     message: "Expected a space before the ':'",
                     line: 3,
-                    column: 17
+                    column: 18
                 },
                 {
                     message: "Expected a space after the '=>'",
                     line: 3,
-                    column: 25
+                    column: 26
                 },
                 {
                     message: "Expected a space before the '=>'",
                     line: 3,
-                    column: 25
+                    column: 26
                 }
             ]
         }