Repository: nzakas/eslint-plugin-typescript
Pull request #10 (2016-10-23 15:52:30)

New: Add rule interface-name-prefix
Ruled based off of TSLint's [interface-name](https://palantir.github.io/tslint/rules/interface-name/) rule


dannyfritz: I'll take a look and add it in. Good excuse to learn how to create ESLint rules with options too.
JamesHenry: Hey @dannyfritz, have you had chance to pick this up again?
dannyfritz: I still intend to do this. However, the timeline i can commit to is before
New Years if that is alright.

On Sun, Nov 27, 2016, 04:45 James Henry <notifications@github.com> wrote:

> Hey @dannyfritz <https://github.com/dannyfritz>, have you had chance to
> pick this up again?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/nzakas/eslint-plugin-typescript/pull/10#issuecomment-263117616>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AATRjLVyzuR3q8nz3BtxtWsA_upOIkTiks5rCW1ZgaJpZM4KeKqs>
> .
>

dannyfritz: This is ready for review. 😄 
dannyfritz: Ready for review! 😄 

Patch:
diff --git a/README.md b/README.md
index 2b64ffc..552ad51 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ Then configure the rules you want to use under the rules section.
 
 * `typescript/type-annotation-spacing` - enforces one space after the colon and zero spaces before the colon of a type annotation.
 * `typescript/explicit-member-accessibility` - enforces accessibility modifiers on class properties and methods.
+* `typescript/interface-name-prefix` - Enforces interface names are prefixed.
 * `typescript/no-triple-slash-reference` - enforces `/// <reference />` is not used.
 * `typescript/no-explicit-any` - enforces the any type is not used.
 * `typescript/no-angle-bracket-type-assertion` - enforces the use of `as Type` assertions instead of `<Type>` assertions.
diff --git a/docs/rules/interface-name-prefix.md b/docs/rules/interface-name-prefix.md
new file mode 100644
index 0000000..58f7861
--- /dev/null
+++ b/docs/rules/interface-name-prefix.md
@@ -0,0 +1,65 @@
+# Enforces interface names are prefixed
+
+It can be hard to differentiate between classes and interfaces.
+Prefixing interfaces with "I" can help telling them apart at a glance.
+
+## Rule Details
+
+This rule enforces consistency of interface naming prefix conventions.
+
+## Options
+
+This rule has a string option.
+
+* `"never"` (default) disallows all interfaces being prefixed with `"I"`
+* `"always"` requires all interfaces be prefixed with `"I"`
+
+### never
+
+TypeScript suggests [never prefixing](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#names) interfaces with "I".
+
+The following patterns are considered warnings:
+
+```ts
+interface IAnimal {
+    name: string;
+}
+```
+
+The following patterns are not warnings:
+
+```ts
+interface Animal {
+    name: string;
+}
+```
+
+### always
+
+The following patterns are considered warnings:
+
+```ts
+interface Animal {
+    name: string;
+}
+```
+
+The following patterns are not warnings:
+
+```ts
+interface IAnimal {
+    name: string;
+}
+```
+
+## When Not To Use It
+
+If you do not want to enforce interface name prefixing.
+
+## Further Reading
+
+TypeScript [Interfaces](https://www.typescriptlang.org/docs/handbook/interfaces.html)
+
+## Compatibility
+
+TSLint: [interface-name](https://palantir.github.io/tslint/rules/interface-name/)
diff --git a/lib/rules/interface-name-prefix.js b/lib/rules/interface-name-prefix.js
new file mode 100644
index 0000000..0dc4484
--- /dev/null
+++ b/lib/rules/interface-name-prefix.js
@@ -0,0 +1,85 @@
+/**
+ * @fileoverview Enforces interface names are prefixed with "I".
+ * @author Danny Fritz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "Enforces interface names are prefixed with \"I\".",
+            category: "TypeScript"
+        },
+        schema: [
+            {
+                enum: ["never", "always"]
+            }
+        ]
+    },
+
+    create: function(context) {
+        var never = context.options[0] !== "always";
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        /**
+         * Checks if a string is prefixed with "I".
+         * @param {string} name The string to check
+         * @returns {boolean} true if it is prefixed with "I"
+         * @private
+         */
+        function isPrefixedWithI(name) {
+            if (typeof name !== "string") {
+                return false;
+            }
+            if (name.length === 0) {
+                return false;
+            }
+            var first = name.charAt(0);
+            var second = name.charAt(1);
+            if (second === "") {
+                return false;
+            }
+            if (first !== "I" || second !== second.toUpperCase()) {
+                return false;
+            }
+            return true;
+        }
+
+        /**
+         * Checks if interface is prefixed with "I".
+         * @param {ASTNode} interfaceNode The node representing an Interface.
+         * @returns {void}
+         * @private
+         */
+        function checkInterfacePrefix(interfaceNode) {
+            if (never) {
+                if (isPrefixedWithI(interfaceNode.name.name)) {
+                    context.report({
+                        node: interfaceNode.name,
+                        message: "Interface name must not be prefixed with \"I\""
+                    });
+                }
+            } else {
+                if (!isPrefixedWithI(interfaceNode.name.name)) {
+                    context.report({
+                        node: interfaceNode.name,
+                        message: "Interface name must be prefixed with \"I\""
+                    });
+                }
+            }
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+        return {
+            TSInterfaceDeclaration: checkInterfacePrefix
+        };
+    }
+};


Test patch:
diff --git a/tests/lib/rules/interface-name-prefix.js b/tests/lib/rules/interface-name-prefix.js
new file mode 100644
index 0000000..b70934f
--- /dev/null
+++ b/tests/lib/rules/interface-name-prefix.js
@@ -0,0 +1,139 @@
+/**
+ * @fileoverview Enforces interface names are prefixed with "I"
+ * @author Danny Fritz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+var rule = require("../../../lib/rules/interface-name-prefix"),
+    RuleTester = require("eslint").RuleTester;
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run("interface-name-prefix", rule, {
+
+    valid: [
+        {
+            code: `
+interface Animal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            options: ["always"],
+            code: `
+interface IAnimal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            options: ["always"],
+            code: `
+interface IIguana {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            options: ["never"],
+            code: `
+interface Iguana {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser"
+        },
+        {
+            options: ["never"],
+            code: `
+interface Animal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser"
+        }
+    ],
+    invalid: [
+        {
+            code: `
+interface IAnimal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [{
+                message: "Interface name must not be prefixed with \"I\"",
+                line: 2,
+                column: 11
+            }]
+        },
+        {
+            options: ["always"],
+            code: `
+interface Animal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [{
+                message: "Interface name must be prefixed with \"I\"",
+                line: 2,
+                column: 11
+            }]
+        },
+        {
+            options: ["always"],
+            code: `
+interface Iguana {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [{
+                message: "Interface name must be prefixed with \"I\"",
+                line: 2,
+                column: 11
+            }]
+        },
+        {
+            options: ["never"],
+            code: `
+interface IIguana {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [{
+                message: "Interface name must not be prefixed with \"I\"",
+                line: 2,
+                column: 11
+            }]
+        },
+        {
+            options: ["never"],
+            code: `
+interface IAnimal {
+    name: string;
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [{
+                message: "Interface name must not be prefixed with \"I\"",
+                line: 2,
+                column: 11
+            }]
+        }
+    ]
+});