diff --git a/README.md b/README.md index 278e2e68..4e724c78 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Users may use the shareable [eslint-config-angular](https://github.com/dustinspe "angular/no-inline-template": [0, {"allowSimple": true}], "angular/no-jquery-angularelement": 2, "angular/no-private-call": 2, + "angular/no-run-logic": [0, {"allowParams": true}], "angular/no-service-method": 2, "angular/no-services": [2, ["$http", "$resource", "Restangular"]], "angular/on-watch": 2, @@ -189,6 +190,7 @@ Users may use the shareable [eslint-config-angular](https://github.com/dustinspe * [no-inline-template](docs/no-inline-template.md) - disallow the use of inline templates * [no-jquery-angularelement](docs/no-jquery-angularelement.md) - disallow to wrap `angular.element` objects with `jQuery` or `$` * [no-private-call](docs/no-private-call.md) - disallow use of internal angular properties prefixed with $$ + * [no-run-logic](docs/no-run-logic.md) - keep run functions clean and simple ([y171](https://github.com/johnpapa/angular-styleguide#style-y171)) * [no-services](docs/no-services.md) - disallow DI of specified services for other angular components (`$http` for controllers, filters and directives) * [no-service-method](docs/no-service-method.md) - use `factory()` instead of `service()` ([y040](https://github.com/johnpapa/angular-styleguide#style-y040)) * [on-watch](docs/on-watch.md) - require `$on` and `$watch` deregistration callbacks to be saved in a variable diff --git a/docs/no-run-logic.md b/docs/no-run-logic.md new file mode 100644 index 00000000..907341aa --- /dev/null +++ b/docs/no-run-logic.md @@ -0,0 +1,60 @@ + + +# no-run-logic - keep run functions clean and simple + +Initialization logic should be moved into a factory or service. This improves testability. + +**Styleguide Reference** + +* [y171 by johnpapa - Run Blocks](https://github.com/johnpapa/angular-styleguide#style-y171) + +## Examples + +The following patterns are considered problems with default config; + + /*eslint angular/no-run-logic: 2*/ + + // invalid + angular.module('app').run(function($window) { + $window.addEventListener('deviceready', deviceready); + + function deviceready() {} + }); // error: The run function may only contain call expressions + +The following patterns are **not** considered problems with default config; + + /*eslint angular/no-run-logic: 2*/ + + // valid + angular.module('app').run(function(KITTENS, kittenService, startup) { + kittenService.prefetchData(KITTENS); + startup('foo', true, 1); + }); + +The following patterns are considered problems when configured `{"allowParams":false}`: + + /*eslint angular/no-run-logic: [2,{"allowParams":false}]*/ + + // invalid + angular.module('app').run(function(kittenService, startup) { + startup('foo', true, 1); + }); // error: Run function call expressions may not take any arguments + +The following patterns are **not** considered problems when configured `{"allowParams":false}`: + + /*eslint angular/no-run-logic: [2,{"allowParams":false}]*/ + + // valid + angular.module('app').run(function(kittenService, startup) { + kittenService.prefetchData(); + startup(); + }); + +## Version + +This rule was introduced in eslint-plugin-angular 0.15.0 + +## Links + +* [Rule source](../rules/no-run-logic.js) +* [Example source](../examples/no-run-logic.js) diff --git a/examples/no-run-logic.js b/examples/no-run-logic.js new file mode 100644 index 00000000..dbf88f0e --- /dev/null +++ b/examples/no-run-logic.js @@ -0,0 +1,23 @@ +// example - valid: true +angular.module('app').run(function(KITTENS, kittenService, startup) { + kittenService.prefetchData(KITTENS); + startup('foo', true, 1); +}); + +// example - valid: true, options: [{allowParams: false}] +angular.module('app').run(function(kittenService, startup) { + kittenService.prefetchData(); + startup(); +}); + +// example - valid: false, errorMessage: "The run function may only contain call expressions" +angular.module('app').run(function($window) { + $window.addEventListener('deviceready', deviceready); + + function deviceready() {} +}); + +// example - valid: false, options: [{allowParams: false}], errorMessage: "Run function call expressions may not take any arguments" +angular.module('app').run(function(startup) { + startup('foo', true, 1); +}); diff --git a/index.js b/index.js index b62ea10a..dcfd851f 100644 --- a/index.js +++ b/index.js @@ -36,6 +36,7 @@ rulesConfiguration.addRule('no-http-callback', 0); rulesConfiguration.addRule('no-inline-template', [0, {'allow-simple': true}]); rulesConfiguration.addRule('no-jquery-angularelement', 2); rulesConfiguration.addRule('no-private-call', 2); +rulesConfiguration.addRule('no-run-logic', 0); rulesConfiguration.addRule('no-services', [2, ['$http', '$resource', 'Restangular', '$q']]); rulesConfiguration.addRule('no-service-method', 2); rulesConfiguration.addRule('on-watch', 2); diff --git a/rules/no-run-logic.js b/rules/no-run-logic.js new file mode 100644 index 00000000..782e0ef8 --- /dev/null +++ b/rules/no-run-logic.js @@ -0,0 +1,58 @@ +/** + * keep run functions clean and simple + * + * Initialization logic should be moved into a factory or service. This improves testability. + * + * @styleguideReference {johnpapa} `y171` Run Blocks + * @version 0.15.0 + */ +'use strict'; + +var angularRule = require('./utils/angular-rule'); + + +module.exports = angularRule(function(context) { + var options = context.options[0] || {}; + var allowParams = options.allowParams !== false; + + function report(node) { + context.report(node, 'The run function may only contain call expressions'); + } + + return { + 'angular:run': function(callExpression, fn) { + if (!fn) { + return; + } + fn.body.body.forEach(function(statement) { + if (statement.type !== 'ExpressionStatement') { + return report(statement); + } + var expression = statement.expression; + if (expression.type !== 'CallExpression') { + return report(statement); + } + if (expression.callee.type === 'MemberExpression' && expression.callee.object.type !== 'Identifier') { + return report(statement); + } + if (!allowParams && expression.arguments.length) { + return context.report(expression, 'Run function call expressions may not take any arguments'); + } + expression.arguments.forEach(function(argument) { + if (argument.type !== 'Literal' && argument.type !== 'Identifier') { + context.report(argument, 'Run function call expressions may only take simple arguments'); + } + }); + }); + } + }; +}); + +module.exports.schema = [{ + type: 'object', + properties: { + allowParams: { + type: 'boolean' + } + } +}]; diff --git a/test/no-run-logic.js b/test/no-run-logic.js new file mode 100644 index 00000000..321a9a98 --- /dev/null +++ b/test/no-run-logic.js @@ -0,0 +1,119 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../rules/no-run-logic'); +var RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var eslintTester = new RuleTester(); +eslintTester.run('no-run-logic', rule, { + valid: [ + 'angular.module("").run();', + 'angular.module("").run(function() {});', + 'angular.module("").run(function() {foo()});', + 'angular.module("").run(function() {foo.bar()});', + // valid arguments if params are allowed + 'angular.module("").run(function() {foo(0)});', + 'angular.module("").run(function() {foo(true)});', + 'angular.module("").run(function() {foo(null)});', + 'angular.module("").run(function() {foo(undefined)});', + 'angular.module("").run(function() {foo("bar")});', + 'angular.module("").run(function() {foo(bar)});' + ], + invalid: [ + // Nested function declarations + { + code: 'angular.module().run(function() {function foo() {}})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run([function() {function foo() {}}])', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'var app = angular.module(); app.run(function() {function foo() {}})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run(fn); function fn() {function foo() {}}', + errors: [{message: 'The run function may only contain call expressions'}] + }, + // Non call expression statements + { + code: 'angular.module().run(function() {foo = bar;})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run([function() {foo = bar;}])', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'var app = angular.module(); app.run(function() {foo = bar;})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run(fn); function fn() {foo = bar;}', + errors: [{message: 'The run function may only contain call expressions'}] + }, + // Nested member expressions + { + code: 'angular.module().run(function() {foo.bar.baz();})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run([function() {foo.bar.baz();}])', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'var app = angular.module(); app.run(function() {foo.bar.baz();})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run(fn); function fn() {foo.bar.baz();}', + errors: [{message: 'The run function may only contain call expressions'}] + }, + // Nested member expressions + { + code: 'angular.module().run(function() {foo().bar.baz();})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run([function() {foo().bar.baz();}])', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'var app = angular.module(); app.run(function() {foo().bar.baz();})', + errors: [{message: 'The run function may only contain call expressions'}] + }, { + code: 'angular.module().run(fn); function fn() {foo().bar.baz();}', + errors: [{message: 'The run function may only contain call expressions'}] + }, + // Disallow any parameters + { + code: 'angular.module().run(function() {foo(1);})', + options: [{allowParams: false}], + errors: [{message: 'Run function call expressions may not take any arguments'}] + }, { + code: 'angular.module().run([function() {foo(1);}])', + options: [{allowParams: false}], + errors: [{message: 'Run function call expressions may not take any arguments'}] + }, { + code: 'var app = angular.module(); app.run(function() {foo(1);})', + options: [{allowParams: false}], + errors: [{message: 'Run function call expressions may not take any arguments'}] + }, { + code: 'angular.module().run(fn); function fn() {foo(1);}', + options: [{allowParams: false}], + errors: [{message: 'Run function call expressions may not take any arguments'}] + }, + // Allow simple parameters + { + code: 'angular.module().run(function() {foo(bar());})', + errors: [{message: 'Run function call expressions may only take simple arguments'}] + }, { + code: 'angular.module().run([function() {foo(bar());}])', + errors: [{message: 'Run function call expressions may only take simple arguments'}] + }, { + code: 'var app = angular.module(); app.run(function() {foo(bar());})', + errors: [{message: 'Run function call expressions may only take simple arguments'}] + }, { + code: 'angular.module().run(fn); function fn() {foo(bar());}', + errors: [{message: 'Run function call expressions may only take simple arguments'}] + } + ] +});