Repository: nzakas/eslint-plugin-typescript
Pull request #44 (2017-08-13 12:54:58)

New: class-name-casing rule (class-name from TSLint)
Implements https://github.com/nzakas/eslint-plugin-typescript/issues/5#issuecomment-321985325.

weirdpattern: Awesome, code LGTM
Please prefix the title with 'New:' so the PR can be easily identified as a rule addition.
corbinu: LGTM
JamesHenry: Thanks! The older README.md cleanup PR that went in means this just needs to be rebased before going in
j-f1: @JamesHenry Done!

Patch:
diff --git a/README.md b/README.md
index 666d41e..d2cba8b 100644
--- a/README.md
+++ b/README.md
@@ -62,5 +62,6 @@ Then configure the rules you want to use under the rules section.
 * [`typescript/member-ordering`](./docs/rules/member-ordering.md) — enforces a standard member declaration order. (`member-ordering` from TSLint)
 * [`typescript/no-unused-vars`](./docs/rules/no-unused-vars.md) — prevents TypeScript-specific constructs from being erroneously flagged as unused
 * [`typescript/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) — enforces member overloads to be consecutive.
+* [`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)
diff --git a/docs/rules/class-name-casing.md b/docs/rules/class-name-casing.md
new file mode 100644
index 0000000..5089c8d
--- /dev/null
+++ b/docs/rules/class-name-casing.md
@@ -0,0 +1,52 @@
+# Enforces PascalCased class and interface names. (class-name-casing)
+
+This rule enforces PascalCased names for classes and interfaces.
+
+## Rule Details
+
+This rule aims to make it easy to differentiate classes from regular variables at a glance.
+
+Examples of **incorrect** code for this rule:
+
+```ts
+
+class invalidClassName {
+
+}
+
+class Another_Invalid_Class_Name {
+
+}
+
+var bar = class invalidName {}
+
+interface someInterface {}
+
+```
+
+Examples of **correct** code for this rule:
+
+```ts
+
+class ValidClassName {
+
+}
+
+export default class {
+
+}
+
+var foo = class {};
+
+interface SomeInterface {}
+
+```
+
+## When Not To Use It
+
+You should turn off this rule if you do not care about class name casing, or if
+you use a different type of casing.
+
+## Further Reading
+
+* [`class-name`](https://palantir.github.io/tslint/rules/class-name/) in [TSLint](https://palantir.github.io/tslint/)
diff --git a/lib/rules/class-name-casing.js b/lib/rules/class-name-casing.js
new file mode 100644
index 0000000..ae815aa
--- /dev/null
+++ b/lib/rules/class-name-casing.js
@@ -0,0 +1,93 @@
+/**
+ * @fileoverview Enforces PascalCased class and interface names.
+ * @author Jed Fox
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "Enforces PascalCased class and interface names.",
+            category: "Best Practices",
+            recommended: true
+        }
+    },
+
+    create(context) {
+
+        // variables should be defined here
+
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        /**
+         * Determine if the identifier name is PascalCased
+         * @param   {string}  name The identifier name
+         * @returns {boolean}      Is the name PascalCased?
+         */
+        function isPascalCase(name) {
+            return name[0].toUpperCase() === name[0] && /^[A-Za-z]+$/.test(name);
+        }
+
+        /**
+         * Report a class declaration as invalid
+         * @param   {Node} decl              The declaration
+         * @param   {Node} [id=classDecl.id] The name of the declaration
+         * @returns {undefined}
+         */
+        function report(decl, id) {
+            id = id || decl.id;
+
+            let friendlyName;
+
+            switch (decl.type) {
+                case "ClassDeclaration":
+                case "ClassExpression":
+                    friendlyName = "Class";
+                    break;
+                case "TSInterfaceDeclaration":
+                    friendlyName = "Interface";
+                    break;
+                default:
+                    friendlyName = decl.type;
+            }
+
+            context.report({
+                node: id,
+                message: `${friendlyName} '${id.name}' must be PascalCased`
+            });
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+
+        return {
+
+            "ClassDeclaration, TSInterfaceDeclaration"(node) {
+
+                // class expressions (i.e. export default class {}) are OK
+                if (node.id && !isPascalCase(node.id.name)) {
+                    report(node);
+                }
+            },
+            VariableDeclarator(node) {
+                if (node.init && node.init.type === "ClassExpression") {
+                    const id = node.id;
+
+                    if (node.init.id && !isPascalCase(node.init.id.name)) {
+                        report(node.init);
+                    } else if (id && !isPascalCase(id.name)) {
+                        report(node.init, id);
+                    }
+                }
+            }
+
+        };
+    }
+};


Test patch:
diff --git a/tests/lib/rules/class-name-casing.js b/tests/lib/rules/class-name-casing.js
new file mode 100644
index 0000000..796a48f
--- /dev/null
+++ b/tests/lib/rules/class-name-casing.js
@@ -0,0 +1,79 @@
+/**
+ * @fileoverview Enforces PascalCased class and interface names.
+ * @author Jed Fox
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/class-name-casing"),
+    RuleTester = require("eslint").RuleTester;
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+    parser: "typescript-eslint-parser"
+});
+
+ruleTester.run("class-name-casing", rule, {
+
+    valid: [
+        "class ValidClassName {}",
+        {
+            code: "export default class {}",
+            parserOptions: {
+                sourceType: "module"
+            }
+        },
+        "var Foo = class {};",
+        "interface SomeInterface {}"
+    ],
+
+    invalid: [
+        {
+            code: "class invalidClassName {}",
+            errors: [{
+                message: "Class 'invalidClassName' must be PascalCased",
+                line: 1,
+                column: 7
+            }]
+        },
+        {
+            code: "class Another_Invalid_Class_Name {}",
+            errors: [{
+                message: "Class 'Another_Invalid_Class_Name' must be PascalCased",
+                line: 1,
+                column: 7
+            }]
+        },
+        {
+            code: "var foo = class {};",
+            errors: [{
+                message: "Class 'foo' must be PascalCased",
+                line: 1,
+                column: 5
+            }]
+        },
+        {
+            code: "var bar = class invalidName {}",
+            errors: [{
+                message: "Class 'invalidName' must be PascalCased",
+                line: 1,
+                column: 17
+            }]
+        },
+        {
+            code: "interface someInterface {}",
+            errors: [{
+                message: "Interface 'someInterface' must be PascalCased",
+                line: 1,
+                column: 11
+            }]
+        }
+    ]
+});