Repository: nzakas/eslint-plugin-typescript
Pull request #9 (2016-10-13 22:25:17)

New: Mark interfaces and generators as used (refs #2)
Custom type annotations and return types being marked as used will be enabled via the new ESLint escope fork


Patch:
diff --git a/docs/rules/no-unused-vars.md b/docs/rules/no-unused-vars.md
new file mode 100644
index 0000000..7e4a654
--- /dev/null
+++ b/docs/rules/no-unused-vars.md
@@ -0,0 +1,25 @@
+# Prevent TypeScript-specific constructs from being erroneously flagged as unused (no-unused-vars)
+
+This rule only has an effect when the `no-unused-vars` core rule is enabled.
+
+It ensures that TypeScript-specific constructs, such as implemented interfaces, are not erroneously flagged as unused.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```ts
+interface Foo {}
+```
+
+The following patterns are not warnings:
+
+```js
+interface Foo {}
+
+class Bar implements Foo {}
+```
+
+## When Not To Use It
+
+If you are not using `no-unused-vars` then you will not need this rule.
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
new file mode 100644
index 0000000..94e0f9d
--- /dev/null
+++ b/lib/rules/no-unused-vars.js
@@ -0,0 +1,142 @@
+/**
+ * @fileoverview Prevent TypeScript-specific variables being falsely marked as unused
+ * @author James Henry
+ */
+"use strict";
+
+/**
+ * Record that a particular variable has been used in code
+ *
+ * @param {Object} context The current rule context.
+ * @param {String} name The name of the variable to mark as used.
+ * @returns {Boolean} True if the variable was found and marked as used, false if not.
+ */
+function markVariableAsUsed(context, name) {
+    var scope = context.getScope();
+    var variables;
+    var i;
+    var len;
+    var found = false;
+
+    // Special Node.js scope means we need to start one level deeper
+    if (scope.type === "global") {
+        while (scope.childScopes.length) {
+            scope = scope.childScopes[0];
+        }
+    }
+
+    do {
+        variables = scope.variables;
+        for (i = 0, len = variables.length; i < len; i++) {
+            if (variables[i].name === name) {
+                variables[i].eslintUsed = true;
+                found = true;
+            }
+        }
+        scope = scope.upper;
+    } while (scope);
+
+    return found;
+}
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "Prevent TypeScript-specific variables being falsely marked as unused.",
+            category: "TypeScript",
+            recommended: true
+        },
+        schema: []
+    },
+
+    create: function(context) {
+
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        /**
+         * Checks if the given node has any decorators and marks them as used.
+         * @param {ASTNode} node The relevant AST node.
+         * @returns {void}
+         * @private
+         */
+        function markDecoratorsAsUsed(node) {
+            if (!node.decorators || !node.decorators.length) {
+                return;
+            }
+            node.decorators.forEach(function(decorator) {
+                /**
+                 * Decorator
+                 */
+                if (decorator.name) {
+                    markVariableAsUsed(context, decorator.name);
+                    return;
+                }
+                /**
+                 * Decorator Factory
+                 */
+                if (decorator.callee && decorator.callee.name) {
+                    markVariableAsUsed(context, decorator.callee.name);
+                    return;
+                }
+            });
+        }
+
+        /**
+         * Checks if the given node has any implemented interfaces and marks them as used.
+         * @param {ASTNode} node The relevant AST node.
+         * @returns {void}
+         * @private
+         */
+        function markImplementedInterfacesAsUsed(node) {
+            if (!node.implements || !node.implements.length) {
+                return;
+            }
+            node.implements.forEach(function(implementedInterface) {
+                if (!implementedInterface || !implementedInterface.id || !implementedInterface.id.name) {
+                    return;
+                }
+                markVariableAsUsed(context, implementedInterface.id.name);
+            });
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+        return {
+            ClassProperty: markDecoratorsAsUsed,
+            ClassDeclaration: function(node) {
+                markDecoratorsAsUsed(node);
+                markImplementedInterfacesAsUsed(node);
+            },
+            MethodDefinition: function(node) {
+                /**
+                 * Decorators are only supported on class methods, so exit early
+                 * if the parent is not a ClassBody
+                 */
+                const anc = context.getAncestors();
+                const tAnc = anc.length;
+                if (!tAnc || !anc[tAnc - 1] || anc[tAnc - 1].type !== "ClassBody") {
+                    return;
+                }
+                /**
+                 * Mark any of the method's own decorators as used
+                 */
+                markDecoratorsAsUsed(node);
+                /**
+                 * Mark any parameter decorators as used
+                 */
+                if (!node.value || !node.value.params || !node.value.params.length) {
+                    return;
+                }
+                node.value.params.forEach(markDecoratorsAsUsed);
+            },
+        };
+
+    }
+};
diff --git a/package.json b/package.json
index 171c5c3..20241ff 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "requireindex": "~1.1.0"
   },
   "devDependencies": {
-    "eslint": "~3.0.0",
+    "eslint": "~3.7.0",
     "eslint-config-eslint": "^3.0.0",
     "mocha": "^2.4.5",
     "typescript-eslint-parser": "^0.4.0"


Test patch:
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
new file mode 100644
index 0000000..c1cba95
--- /dev/null
+++ b/tests/lib/rules/no-unused-vars.js
@@ -0,0 +1,226 @@
+/**
+ * @fileoverview Prevent variables used in TypeScript being marked as unused
+ * @author James Henry
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/no-unused-vars");
+const ruleNoUnusedVars = require("eslint/lib/rules/no-unused-vars");
+const RuleTester = require("eslint").RuleTester;
+
+const eslint = require("eslint").linter;
+eslint.defineRule("typescript/no-unused-vars", rule);
+
+const parser = "typescript-eslint-parser";
+const parserOptions = {
+    ecmaVersion: 6,
+    sourceType: "module",
+    ecmaFeatures: {}
+};
+const rules = {
+    "typescript/no-unused-vars": "error"
+};
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+
+ruleTester.run("no-unused-vars", ruleNoUnusedVars, {
+
+    valid: [
+        {
+            code: [
+                "import { ClassDecoratorFactory } from 'decorators'",
+                "@ClassDecoratorFactory()",
+                "export class Foo {}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { ClassDecorator } from 'decorators'",
+                "@ClassDecorator",
+                "export class Foo {}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { AccessorDecoratorFactory } from 'decorators'",
+                "export class Foo {",
+                "   @AccessorDecoratorFactory(true)",
+                "   get bar() {}",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { AccessorDecorator } from 'decorators'",
+                "export class Foo {",
+                "   @AccessorDecorator",
+                "   set bar() {}",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { MethodDecoratorFactory } from 'decorators'",
+                "export class Foo {",
+                "   @MethodDecoratorFactory(false)",
+                "   bar() {}",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { MethodDecorator } from 'decorators'",
+                "export class Foo {",
+                "   @MethodDecorator",
+                "   static bar() {}",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { ConstructorParameterDecoratorFactory } from 'decorators'",
+                "export class Service {",
+                "   constructor(@ConstructorParameterDecoratorFactory(APP_CONFIG) config: AppConfig) {",
+                "       this.title = config.title;",
+                "   }",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { ConstructorParameterDecorator } from 'decorators'",
+                "export class Foo {",
+                "   constructor(@ConstructorParameterDecorator bar) {",
+                "       this.bar = bar;",
+                "   }",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { ParameterDecoratorFactory } from 'decorators'",
+                "export class Qux {",
+                "   bar(@ParameterDecoratorFactory(true) baz: number) {",
+                "       console.log(baz);",
+                "   }",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { ParameterDecorator } from 'decorators'",
+                "export class Foo {",
+                "   static greet(@ParameterDecorator name: string) {",
+                "       return name;",
+                "   }",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { Input, Output, EventEmitter } from 'decorators'",
+                "export class SomeComponent {",
+                "   @Input() data;",
+                "   @Output()",
+                "   click = new EventEmitter();",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { configurable } from 'decorators'",
+                "export class A {",
+                "   @configurable(true) static prop1;",
+                "                                    ",
+                "   @configurable(false)",
+                "   static prop2;",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "import { foo, bar } from 'decorators'",
+                "export class B {",
+                "   @foo x;",
+                "                ",
+                "   @bar",
+                "   y;",
+                "}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+        {
+            code: [
+                "interface Base {}",
+                "class Thing implements Base {}",
+                "new Thing()",
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+        },
+    ],
+
+    invalid: [
+        {
+            code: [
+                "import { ClassDecoratorFactory } from 'decorators'",
+                "export class Foo {}"
+            ].join("\n"),
+            parser,
+            parserOptions,
+            rules,
+            errors: [{
+                message: "'ClassDecoratorFactory' is defined but never used.",
+                line: 1,
+                column: 10
+            }]
+        },
+    ]
+});