diff --git a/index.js b/index.js index d4217a4d..91daf8bf 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ 'ng_definedundefined': require('./rules/ng_definedundefined'), 'ng_di': require('./rules/ng_di'), 'ng_di_order': require('./rules/ng_di_order'), + 'ng_di_unused': require('./rules/ng_di_unused'), 'ng_directive_name': require('./rules/ng_directive_name'), 'ng_document_service': require('./rules/ng_document_service'), 'ng_empty_controller': require('./rules/ng_empty_controller'), @@ -55,6 +56,7 @@ 'ng_definedundefined': 2, 'ng_di': [2, 'function'], 'ng_di_order': 0, + 'ng_di_unused': 0, 'ng_directive_name': 0, 'ng_document_service': 2, 'ng_empty_controller': 0, diff --git a/package.json b/package.json index 38e09e75..e253109c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-angular", - "version": "0.7.0", + "version": "0.8.0", "description": "ESLint rules for AngularJS projects", "main": "index.js", "repository": { diff --git a/rules/ng_di_unused.js b/rules/ng_di_unused.js new file mode 100644 index 00000000..d46612e2 --- /dev/null +++ b/rules/ng_di_unused.js @@ -0,0 +1,93 @@ +module.exports = function(context) { + + 'use strict'; + + var utils = require('./utils/utils'); + + var angularNamedObjectList = [ + 'controller', + 'directive', + 'factory', + 'filter', + 'provider', + 'service' + ]; + var setupCalls = [ + 'config', + 'run' + ]; + + var injectFunctions = []; + + // Keeps track of visited scopes in the collectAngularScopes function to prevent infinite recursion on circular references. + var visitedScopes = []; + + // This collects the variable scopes for the injectible functions which have been collected. + function collectAngularScopes(scope) { + if(visitedScopes.indexOf(scope) === -1) { + visitedScopes.push(scope); + injectFunctions.forEach(function(value) { + if(scope.block === value.node) { + value.scope = scope; + } + }); + scope.childScopes.forEach(function(child) { + collectAngularScopes(child); + }); + } + } + + return { + + 'AssignmentExpression': function(node) { + // Colllect the $get function of a providers. + if(node.left.type === 'MemberExpression' && node.left.property.name === '$get') { + injectFunctions.push({ + node: node.right + }); + } + }, + + 'CallExpression': function(node) { + // An Angular component definition. + if(utils.isAngularComponent(node) && node.callee.type === 'MemberExpression' && node.arguments[1].type === 'FunctionExpression' && angularNamedObjectList.indexOf(node.callee.property.name) >= 0){ + return injectFunctions.push({ + node: node.arguments[1] + }); + } + // Config and run functions. + if(node.callee.type === 'MemberExpression' && node.arguments.length > 0 && setupCalls.indexOf(node.callee.property.name) !== -1 && node.arguments[0].type === 'FunctionExpression') { + return injectFunctions.push({ + node: node.arguments[0] + }); + } + // Injected values in unittests. + if(node.callee.type === 'Identifier' && node.callee.name === 'inject') { + return injectFunctions.push({ + node: node.arguments[0] + }); + } + }, + + // Actually find and report unused injected variables. + 'Program:exit': function(node) { + var globalScope = context.getScope(); + collectAngularScopes(globalScope); + injectFunctions.forEach(function(value) { + if(value.scope) { + value.scope.variables.forEach(function(variable) { + if(variable.name === 'arguments') { + return; + } + if(value.node.params.indexOf(variable.identifiers[0]) === -1) { + return; + } + if(variable.references.length === 0) { + context.report(value.node, 'Unused injected value {{name}}', variable); + } + }); + } + }) + } + }; +}; diff --git a/test/ng_di_unused.js b/test/ng_di_unused.js new file mode 100644 index 00000000..313ed77d --- /dev/null +++ b/test/ng_di_unused.js @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var rule = require('../rules/ng_di_unused'), + RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +var eslintTester = new RuleTester(); +eslintTester.run('ng_di_unused', rule, { + valid: [ + 'app.controller("", function($q){return $q;});', + 'app.directive("", function($q){return $q;});', + 'app.factory("", function($q){return $q;});', + 'app.factory("", function($q){return function(){return $q;};});', + 'app.factory("", function(){var myVar;});', + 'app.filter("", function($q){return $q;});', + 'app.provider("", function($httpProvider){return $httpProvider;});', + 'app.service("", function($q){return $q;});', + 'app.config(function($httpProvider) {$httpProvider.defaults.headers.post.answer="42"})', + 'app.run(function($q) {$q()})', + 'inject(function($q){_$q_ = $q;});', + 'this.$get = function($q){return $q;};', + ], + invalid: [{ + code: 'app.controller("", function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'app.directive("", function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'app.factory("", function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'app.factory("", function($http, $q){});', + errors: [ + {message: 'Unused injected value $http'}, + {message: 'Unused injected value $q'} + ] + }, { + code: 'app.factory("", function($http, $q){return $q.resolve()});', + errors: [ + {message: 'Unused injected value $http'} + ] + }, { + code: 'app.filter("", function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'app.provider("", function($httpProvider){});', + errors: [{message: 'Unused injected value $httpProvider'}] + }, { + code: 'app.service("", function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'app.config(function($httpProvider) {})', + errors: [{message: 'Unused injected value $httpProvider'}] + }, { + code: 'app.run(function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'inject(function($q){});', + errors: [{message: 'Unused injected value $q'}] + }, { + code: 'this.$get = function($q){};', + errors: [{message: 'Unused injected value $q'}] + }] +});