diff --git a/.eslintrc b/.eslintrc index 6773f25a..8beec8ac 100644 --- a/.eslintrc +++ b/.eslintrc @@ -118,6 +118,7 @@ rules: # http://eslint.org/docs/rules/#nodejs callback-return: 2 + global-require: 2 handle-callback-err: - 2 - ^(e|err|error)$ diff --git a/.travis.yml b/.travis.yml index 71812dc1..d807dd0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,5 @@ language: node_js node_js: - "5.0" - "4.2" - - "0.10" + - "0.12" after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/README.md b/README.md index 17d8f408..00b848b7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -[![Build Status](https://travis-ci.org/Gillespie59/eslint-plugin-angular.svg?branch=master)](https://travis-ci.org/Gillespie59/eslint-plugin-angular) -[![Npm dependencies](https://david-dm.org/Gillespie59/eslint-plugin-angular.svg)](https://david-dm.org/Gillespie59/eslint-plugin-angular) -[![devDependency Status](https://david-dm.org/Gillespie59/eslint-plugin-angular/dev-status.png)](https://david-dm.org/Gillespie59/eslint-plugin-angular#info=devDependencies) -[![Join the chat at https://gitter.im/Gillespie59/eslint-plugin-angular](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Gillespie59/eslint-plugin-angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Coverage Status](https://coveralls.io/repos/Gillespie59/eslint-plugin-angular/badge.svg?branch=master)](https://coveralls.io/r/Gillespie59/eslint-plugin-angular?branch=master) +# eslint plugin angular [![Npm version](https://img.shields.io/npm/v/eslint-plugin-angular.svg)](https://www.npmjs.com/package/eslint-plugin-angular) [![Npm downloads per month](https://img.shields.io/npm/dm/eslint-plugin-angular.svg)](https://www.npmjs.com/package/eslint-plugin-angular) +> ESLint rules for your angular project with checks for best-practices, conventions or potential errors. +[![Build Status](https://img.shields.io/travis/Gillespie59/eslint-plugin-angular/master.svg)](https://travis-ci.org/Gillespie59/eslint-plugin-angular) +[![Npm dependencies](https://img.shields.io/david/Gillespie59/eslint-plugin-angular.svg)](https://david-dm.org/Gillespie59/eslint-plugin-angular) +[![devDependency Status](https://img.shields.io/david/dev/Gillespie59/eslint-plugin-angular.svg)](https://david-dm.org/Gillespie59/eslint-plugin-angular#info=devDependencies) +[![Join the chat at https://gitter.im/Gillespie59/eslint-plugin-angular](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/Gillespie59/eslint-plugin-angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Coverage Status](https://img.shields.io/coveralls/Gillespie59/eslint-plugin-angular/master.svg)](https://coveralls.io/r/Gillespie59/eslint-plugin-angular?branch=master) ## Summary @@ -130,6 +132,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, @@ -154,57 +157,97 @@ Users may use the shareable [eslint-config-angular](https://github.com/dustinspe ## Rules +Rules in eslint-plugin-angular are divided into several categories to help you better understand their value. - * [angularelement](docs/angularelement.md) - use `angular.element` instead of `$` or `jQuery` - * [component-limit](docs/component-limit.md) - limit the number of angular components per file - * [controller-as](docs/controller-as.md) - disallow assignments to `$scope` in controllers - * [controller-as-route](docs/controller-as-route.md) - require the use of controllerAs in routes or states - * [controller-as-vm](docs/controller-as-vm.md) - require and specify a capture variable for `this` in controllers - * [controller-name](docs/controller-name.md) - require and specify a prefix for all controller names + +## Possible Errors + +The following rules detect patterns that can lead to errors. + + * [module-getter](docs/module-getter.md) - disallow to reference modules with variables and require to use the getter syntax instead `angular.module('myModule')` ([y022](https://github.com/johnpapa/angular-styleguide#style-y022)) + * [module-setter](docs/module-setter.md) - disallow to assign modules to variables (linked to [module-getter](docs/module-getter.md) ([y021](https://github.com/johnpapa/angular-styleguide#style-y021)) + * [no-private-call](docs/no-private-call.md) - disallow use of internal angular properties prefixed with $$ + +## Best Practices + +These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns.. + + * [component-limit](docs/component-limit.md) - limit the number of angular components per file ([y001](https://github.com/johnpapa/angular-styleguide#style-y001)) + * [controller-as](docs/controller-as.md) - disallow assignments to `$scope` in controllers ([y031](https://github.com/johnpapa/angular-styleguide#style-y031)) + * [controller-as-route](docs/controller-as-route.md) - require the use of controllerAs in routes or states ([y031](https://github.com/johnpapa/angular-styleguide#style-y031)) + * [controller-as-vm](docs/controller-as-vm.md) - require and specify a capture variable for `this` in controllers ([y032](https://github.com/johnpapa/angular-styleguide#style-y032)) * [deferred](docs/deferred.md) - use `$q(function(resolve, reject){})` instead of `$q.deferred` - * [definedundefined](docs/definedundefined.md) - use `angular.isDefined` and `angular.isUndefined` instead of other undefined checks - * [di](docs/di.md) - require a consistent DI syntax - * [di-order](docs/di-order.md) - require DI parameters to be sorted alphabetically * [di-unused](docs/di-unused.md) - disallow unused DI parameters - * [directive-name](docs/directive-name.md) - require and specify a prefix for all directive names - * [directive-restrict](docs/directive-restrict.md) - disallow any other directive restrict than 'A' or 'E' - * [document-service](docs/document-service.md) - use `$document` instead of `document` + * [directive-restrict](docs/directive-restrict.md) - disallow any other directive restrict than 'A' or 'E' ([y074](https://github.com/johnpapa/angular-styleguide#style-y074)) * [empty-controller](docs/empty-controller.md) - disallow empty controllers - * [file-name](docs/file-name.md) - require and specify a consistent component name pattern - * [filter-name](docs/filter-name.md) - require and specify a prefix for all filter names - * [foreach](docs/foreach.md) - use `angular.forEach` instead of native `Array.prototype.forEach` - * [function-type](docs/function-type.md) - require and specify a consistent function style for components ('named' or 'anonymous') - * [interval-service](docs/interval-service.md) - use `$interval` instead of `setInterval` - * [json-functions](docs/json-functions.md) - use `angular.fromJson` and 'angular.toJson' instead of `JSON.parse` and `JSON.stringify` - * [log](docs/log.md) - use the `$log` service instead of the `console` methods - * [module-dependency-order](docs/module-dependency-order.md) - require a consistent order of module dependencies - * [module-getter](docs/module-getter.md) - disallow to reference modules with variables and require to use the getter syntax instead `angular.module('myModule')` - * [module-name](docs/module-name.md) - require and specify a prefix for all module names - * [module-setter](docs/module-setter.md) - disallow to assign modules to variables (linked to [module-getter](docs/module-getter.md) - * [no-angular-mock](docs/no-angular-mock.md) - require to use `angular.mock` methods directly * [no-controller](docs/no-controller.md) - disallow use of controllers (according to the component first pattern) - * [no-cookiestore](docs/no-cookiestore.md) - use `$cookies` instead of `$cookieStore` - * [no-digest](docs/no-digest.md) - DEPRECATED! use `$apply()` instead of `$digest()` (replaced by [watchers-execution](docs/watchers-execution.md)) - * [no-http-callback](docs/no-http-callback.md) - disallow the `$http` methods `success()` and `error()` * [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()` * [on-watch](docs/on-watch.md) - require `$on` and `$watch` deregistration callbacks to be saved in a variable + +## Deprecated Angular Features + +These rules prevent you from using deprecated angular features. + + * [no-cookiestore](docs/no-cookiestore.md) - use `$cookies` instead of `$cookieStore` + * [no-directive-replace](docs/no-directive-replace.md) - disallow the deprecated directive replace property + * [no-http-callback](docs/no-http-callback.md) - disallow the `$http` methods `success()` and `error()` + +## Naming + +These rules help you to specify several naming conventions. + + * [controller-name](docs/controller-name.md) - require and specify a prefix for all controller names ([y123](https://github.com/johnpapa/angular-styleguide#style-y123), [y124](https://github.com/johnpapa/angular-styleguide#style-y124)) + * [directive-name](docs/directive-name.md) - require and specify a prefix for all directive names ([y073](https://github.com/johnpapa/angular-styleguide#style-y073), [y126](https://github.com/johnpapa/angular-styleguide#style-y126)) + * [file-name](docs/file-name.md) - require and specify a consistent component name pattern ([y120](https://github.com/johnpapa/angular-styleguide#style-y120), [y121](https://github.com/johnpapa/angular-styleguide#style-y121)) + * [filter-name](docs/filter-name.md) - require and specify a prefix for all filter names + * [module-name](docs/module-name.md) - require and specify a prefix for all module names ([y127](https://github.com/johnpapa/angular-styleguide#style-y127)) + * [service-name](docs/service-name.md) - require and specify a prefix for all service names ([y125](https://github.com/johnpapa/angular-styleguide#style-y125)) + +## Conventions + +Angular often provide multi ways to to something. These rules help you to define convention for your project. + + * [di](docs/di.md) - require a consistent DI syntax + * [di-order](docs/di-order.md) - require DI parameters to be sorted alphabetically + * [dumb-inject](docs/dumb-inject.md) - unittest `inject` functions should only consist of assignments from injected values to describe block variables + * [function-type](docs/function-type.md) - require and specify a consistent function style for components ('named' or 'anonymous') ([y024](https://github.com/johnpapa/angular-styleguide#style-y024)) + * [module-dependency-order](docs/module-dependency-order.md) - require a consistent order of module dependencies + * [no-service-method](docs/no-service-method.md) - use `factory()` instead of `service()` ([y040](https://github.com/johnpapa/angular-styleguide#style-y040)) * [one-dependency-per-line](docs/one-dependency-per-line.md) - require all DI parameters to be located in their own line * [rest-service](docs/rest-service.md) - disallow different rest service and specify one of '$http', '$resource', 'Restangular' - * [service-name](docs/service-name.md) - require and specify a prefix for all service names - * [timeout-service](docs/timeout-service.md) - use `$timeout` instead of `setTimeout` + * [watchers-execution](docs/watchers-execution.md) - require and specify consistent use `$scope.digest()` or `$scope.apply()` + +## Angular Wrappers + +These rules help you to enforce the usage of angular wrappers. + + * [angularelement](docs/angularelement.md) - use `angular.element` instead of `$` or `jQuery` + * [definedundefined](docs/definedundefined.md) - use `angular.isDefined` and `angular.isUndefined` instead of other undefined checks + * [document-service](docs/document-service.md) - use `$document` instead of `document` ([y180](https://github.com/johnpapa/angular-styleguide#style-y180)) + * [foreach](docs/foreach.md) - use `angular.forEach` instead of native `Array.prototype.forEach` + * [interval-service](docs/interval-service.md) - use `$interval` instead of `setInterval` ([y181](https://github.com/johnpapa/angular-styleguide#style-y181)) + * [json-functions](docs/json-functions.md) - use `angular.fromJson` and 'angular.toJson' instead of `JSON.parse` and `JSON.stringify` + * [log](docs/log.md) - use the `$log` service instead of the `console` methods + * [no-angular-mock](docs/no-angular-mock.md) - require to use `angular.mock` methods directly + * [no-jquery-angularelement](docs/no-jquery-angularelement.md) - disallow to wrap `angular.element` objects with `jQuery` or `$` + * [timeout-service](docs/timeout-service.md) - use `$timeout` instead of `setTimeout` ([y181](https://github.com/johnpapa/angular-styleguide#style-y181)) * [typecheck-array](docs/typecheck-array.md) - use `angular.isArray` instead of `typeof` comparisons * [typecheck-date](docs/typecheck-date.md) - use `angular.isDate` instead of `typeof` comparisons * [typecheck-function](docs/typecheck-function.md) - use `angular.isFunction` instead of `typeof` comparisons * [typecheck-number](docs/typecheck-number.md) - use `angular.isNumber` instead of `typeof` comparisons * [typecheck-object](docs/typecheck-object.md) - use `angular.isObject` instead of `typeof` comparisons - * [typecheck-regexp](docs/typecheck-regexp.md) - DEPRECATED! use `angular.isRegexp` instead of other comparisons (no native angular method) * [typecheck-string](docs/typecheck-string.md) - use `angular.isString` instead of `typeof` comparisons - * [watchers-execution](docs/watchers-execution.md) - require and specify consistent use `$scope.digest()` or `$scope.apply()` - * [window-service](docs/window-service.md) - use `$window` instead of `window` + * [window-service](docs/window-service.md) - use `$window` instead of `window` ([y180](https://github.com/johnpapa/angular-styleguide#style-y180)) + +## Deprecated rules + +These rules will be removed in version 1.0.0 + + * [no-digest](docs/no-digest.md) - use `$apply()` instead of `$digest()` (replaced by [watchers-execution](docs/watchers-execution.md)) + * [typecheck-regexp](docs/typecheck-regexp.md) - use `angular.isRegexp` instead of other comparisons (no native angular method) + ---- @@ -234,6 +277,14 @@ We appreciate contributions and the following notes will help you before you ope Have a look at the existing issues. There may exist similar issues with useful information. +### Read the documentation + +There are some useful references for creating new rules. Specificly useful are: + +* [The Context Object](http://eslint.org/docs/developer-guide/working-with-rules#the-context-object) - This is the most basic understanding needed for adding or modifying a rule. +* [Options Schemas](http://eslint.org/docs/developer-guide/working-with-rules#options-schemas) - This is the preferred way for validating configuration options. +* [Scope](http://estools.github.io/escope/Scope.html) - This is the scope object returned by `context.getScope()`. + ### Files you have to create * `rules/.js` @@ -247,18 +298,18 @@ Have a look at the existing issues. There may exist similar issues with useful i * `examples/.js` * Add some examples for the documentation * Run the `gulp docs` task to test the examples and update the markdown documentation -* `docs/.js` +* `docs/.md` * Generated by the `gulp docs` task ### Files you have to touch * `index.js` - * Add your rule `rulesConfiguration.addRule('', [0, {someConfig: 'someValue'])` + * Add your rule `rulesConfiguration.addRule('', [0, {someConfig: 'someValue'}])` ### Before you open your PR * Check that the `gulp` task is working -* Commit generated changes in `README.md` and `docs/.js` +* Commit generated changes in `README.md` and `docs/.md` * Open your PR to the `development` branch NOT `master` ### Rules specific for Angular 1 or 2 @@ -322,6 +373,6 @@ Here is the basic configuration for the rules defined in the ESLint plugin, in o ## Team -[![Emmanuel DEMEY](https://avatars.githubusercontent.com/u/555768?s=117)](http://gillespie59.github.io/) | -:---:| -[Emmanuel DEMEY](http://gillespie59.github.io/) +[![Emmanuel Demey](https://avatars.githubusercontent.com/u/555768?s=117)](http://gillespie59.github.io/) | [![Tilman Potthof](https://avatars.githubusercontent.com/u/157532?s=117)](https://github.com/tilmanpotthof) | [![Remco Haszing](https://avatars.githubusercontent.com/u/779047?s=117)](https://github.com/remcohaszing) | +:---:|:---:|:---:| +[Emmanuel Demey](http://gillespie59.github.io/) | [Tilman Potthof](https://github.com/tilmanpotthof) | [Remco Haszing](https://github.com/remcohaszing) | diff --git a/docs/dumb-inject.md b/docs/dumb-inject.md new file mode 100644 index 00000000..e4d50319 --- /dev/null +++ b/docs/dumb-inject.md @@ -0,0 +1,64 @@ + + +# dumb-inject - unittest `inject` functions should only consist of assignments from injected values to describe block variables + +`inject` functions in unittests should only contain a sorted mapping of injected values to values in the `describe` block with matching names. +This way the dependency injection setup is separated from the other setup logic, improving readability of the test. + +## Examples + +The following patterns are considered problems; + + /*eslint angular/dumb-inject: 2*/ + + // invalid + describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $httpBackend = _$httpBackend_; + $rootScope = _$rootScope_; + + $httpBackend.whenGET('/data').respond([]); + })); + }); // error: inject functions may only consist of assignments in the form myService = _myService_ + + // invalid + describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $rootScope = _$rootScope_; + $httpBackend = _$httpBackend_; + })); + }); // error: '$httpBackend' must be sorted before '$rootScope' + +The following patterns are **not** considered problems; + + /*eslint angular/dumb-inject: 2*/ + + // valid + describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $httpBackend = _$httpBackend_; + $rootScope = _$rootScope_; + })); + + beforeEach(function() { + $httpBackend.whenGET('/data').respond([]); + }); + }); + +## Version + +This rule was introduced in eslint-plugin-angular 0.15.0 + +## Links + +* [Rule source](../rules/dumb-inject.js) +* [Example source](../examples/dumb-inject.js) diff --git a/docs/no-cookiestore.md b/docs/no-cookiestore.md index a735e30a..440d4797 100644 --- a/docs/no-cookiestore.md +++ b/docs/no-cookiestore.md @@ -12,10 +12,10 @@ The following patterns are considered problems; /*eslint angular/no-cookiestore: 2*/ // invalid - $cookieStore.put('favoriteMeal', 'pizza'); // error: Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service. + $cookieStore.put('favoriteMeal', 'pizza'); // error: Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service. // invalid - $cookieStore.get('favoriteMeal'); // error: Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service. + $cookieStore.get('favoriteMeal'); // error: Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service. The following patterns are **not** considered problems; diff --git a/docs/no-digest.md b/docs/no-digest.md index d3f0ba15..07bacbef 100644 --- a/docs/no-digest.md +++ b/docs/no-digest.md @@ -2,9 +2,13 @@ # no-digest - use `$apply()` instead of `$digest()` -DEPRECATED! The scope's $digest() method shouldn't be used. +**This rule is deprecated and will be removed in future versions. Explanation: There is no reason to forbid the use of `$digest()` in general.** + +The scope's $digest() method shouldn't be used. You should prefer the $apply method. +The `watchers-execution` rule can be configured to enforce the use of `$apply()` or `$digest()`. + ## Version This rule was introduced in eslint-plugin-angular 0.1.0 diff --git a/docs/no-directive-replace.md b/docs/no-directive-replace.md new file mode 100644 index 00000000..e8d9fad3 --- /dev/null +++ b/docs/no-directive-replace.md @@ -0,0 +1,74 @@ + + +# no-directive-replace - disallow the deprecated directive replace property + +This rule disallows the replace attribute in a directive definition object. +The replace property of a directive definition object is deprecated since angular 1.3 ([latest angular docs](https://docs.angularjs.org/api/ng/service/$compile). + +The option `ignoreReplaceFalse` let you ignore directive definitions with replace set to false. + +## Examples + +The following patterns are considered problems with default config; + + /*eslint angular/no-directive-replace: 2*/ + + // invalid + angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: true + }; + }); // error: Directive definition property replace is deprecated. + + // invalid + angular.module('myModule').directive('helloWorld', function() { + var directiveDefinition = {}; + directiveDefinition.templateUrl = 'helloWorld.html'; + directiveDefinition.replace = true; + return directiveDefinition; + }); // error: Directive definition property replace is deprecated. + +The following patterns are **not** considered problems with default config; + + /*eslint angular/no-directive-replace: 2*/ + + // valid + angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

' + }; + }); + +The following patterns are **not** considered problems when configured `{"ignoreReplaceFalse":true}`: + + /*eslint angular/no-directive-replace: [2,{"ignoreReplaceFalse":true}]*/ + + // valid + angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: false + }; + }); + +The following patterns are considered problems when configured `{"ignoreReplaceFalse":false}`: + + /*eslint angular/no-directive-replace: [2,{"ignoreReplaceFalse":false}]*/ + + // invalid + angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: true + }; + }); // error: Directive definition property replace is deprecated. + +## Version + +This rule was introduced in eslint-plugin-angular 0.15.0 + +## Links + +* [Rule source](../rules/no-directive-replace.js) +* [Example source](../examples/no-directive-replace.js) diff --git a/docs/no-run-logic.md b/docs/no-run-logic.md new file mode 100644 index 00000000..430531be --- /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(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/docs/typecheck-array.md b/docs/typecheck-array.md index 0dc1a1d7..b5032410 100644 --- a/docs/typecheck-array.md +++ b/docs/typecheck-array.md @@ -13,6 +13,9 @@ The following patterns are considered problems; // invalid Object.prototype.toString.call(someArray) === '[object Array]'; // error: You should use the angular.isArray method + // invalid + Array.isArray(someArray) // error: You should use the angular.isArray method + The following patterns are **not** considered problems; /*eslint angular/typecheck-array: 2*/ diff --git a/docs/typecheck-regexp.md b/docs/typecheck-regexp.md index be9eb046..ed44ee70 100644 --- a/docs/typecheck-regexp.md +++ b/docs/typecheck-regexp.md @@ -2,7 +2,9 @@ # typecheck-regexp - use `angular.isRegexp` instead of other comparisons -DEPRECATED! You should use the angular.isRegexp method instead of the default JavaScript implementation (toString.call(/^A/) === "[object RegExp]"). +**This rule is deprecated and will be removed in future versions. Explanation: `angular.isRegexp` is no built-in angular method.** + +You should use the angular.isRegexp method instead of the default JavaScript implementation (toString.call(/^A/) === "[object RegExp]"). ## Version diff --git a/examples/dumb-inject.js b/examples/dumb-inject.js new file mode 100644 index 00000000..4e5152ec --- /dev/null +++ b/examples/dumb-inject.js @@ -0,0 +1,38 @@ +// example - valid: true +describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $httpBackend = _$httpBackend_; + $rootScope = _$rootScope_; + })); + + beforeEach(function() { + $httpBackend.whenGET('/data').respond([]); + }); +}); + +// example - valid: false, errorMessage: "inject functions may only consist of assignments in the form myService = _myService_" +describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $httpBackend = _$httpBackend_; + $rootScope = _$rootScope_; + + $httpBackend.whenGET('/data').respond([]); + })); +}); + +// example - valid: false, errorMessage: "'$httpBackend' must be sorted before '$rootScope'" +describe(function() { + var $httpBackend; + var $rootScope; + + beforeEach(inject(function(_$httpBackend_, _$rootScope_) { + $rootScope = _$rootScope_; + $httpBackend = _$httpBackend_; + })); +}); diff --git a/examples/no-cookiestore.js b/examples/no-cookiestore.js index 17990802..081060e9 100644 --- a/examples/no-cookiestore.js +++ b/examples/no-cookiestore.js @@ -4,8 +4,8 @@ $cookies.put('favoriteMeal', 'pizza'); // example - valid: true $cookies.get('favoriteMeal'); -// example - valid: false, errorMessage: "Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service." +// example - valid: false, errorMessage: "Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service." $cookieStore.put('favoriteMeal', 'pizza'); -// example - valid: false, errorMessage: "Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service." +// example - valid: false, errorMessage: "Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service." $cookieStore.get('favoriteMeal'); diff --git a/examples/no-directive-replace.js b/examples/no-directive-replace.js new file mode 100644 index 00000000..f813242e --- /dev/null +++ b/examples/no-directive-replace.js @@ -0,0 +1,40 @@ +// example - valid: true +angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

' + }; +}); + +// example - valid: true, options: [{"ignoreReplaceFalse": true}] +angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: false + }; +}); + +// example - valid: false, errorMessage: "Directive definition property replace is deprecated." +angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: true + }; +}); + +// example - valid: false, errorMessage: "Directive definition property replace is deprecated." +angular.module('myModule').directive('helloWorld', function() { + var directiveDefinition = {}; + directiveDefinition.templateUrl = 'helloWorld.html'; + directiveDefinition.replace = true; + return directiveDefinition; +}); + +// example - valid: false, options: [{"ignoreReplaceFalse": false}], errorMessage: "Directive definition property replace is deprecated." +angular.module('myModule').directive('helloWorld', function() { + return { + template: '

Hello World!

', + replace: true + }; +}); + + 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/examples/typecheck-array.js b/examples/typecheck-array.js index 164034af..b364c843 100644 --- a/examples/typecheck-array.js +++ b/examples/typecheck-array.js @@ -3,3 +3,6 @@ angular.isArray(someArray); // example - valid: false, errorMessage: "You should use the angular.isArray method" Object.prototype.toString.call(someArray) === '[object Array]'; + +// example - valid: false, errorMessage: "You should use the angular.isArray method" +Array.isArray(someArray) diff --git a/index.js b/index.js index b62ea10a..a8e54701 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ rulesConfiguration.addRule('di-unused', 0); rulesConfiguration.addRule('directive-name', 0); rulesConfiguration.addRule('directive-restrict', 0); rulesConfiguration.addRule('document-service', 2); +rulesConfiguration.addRule('dumb-inject', 0); rulesConfiguration.addRule('empty-controller', 0); rulesConfiguration.addRule('file-name', 0); rulesConfiguration.addRule('filter-name', 0); @@ -32,10 +33,12 @@ rulesConfiguration.addRule('no-angular-mock', 0); rulesConfiguration.addRule('no-controller', 0); rulesConfiguration.addRule('no-cookiestore', 2); rulesConfiguration.addRule('no-digest', 0); +rulesConfiguration.addRule('no-directive-replace', 0); 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/package.json b/package.json index 7d5b7253..0382735d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "type": "git", "url": "https://github.com/Gillespie59/eslint-plugin-angularjs.git" }, - "author": "Emmanuel DEMEY", + "contributors": [ + "Emmanuel Demey (http://gillespie59.github.io/)", + "Tilman Potthof (https://github.com/tilmanpotthof)", + "Remco Haszing (https://github.com/remcohaszing)" + ], "license": "MIT", "bugs": { "url": "https://github.com/Gillespie59/eslint-plugin-angularjs/issues" diff --git a/rules/angularelement.js b/rules/angularelement.js index a9ccfa3d..f8a28d31 100644 --- a/rules/angularelement.js +++ b/rules/angularelement.js @@ -5,6 +5,7 @@ * If the jQuery library is imported, angular.element will be a wrapper around the jQuery object. * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/component-limit.js b/rules/component-limit.js index d7d5409d..edf61069 100644 --- a/rules/component-limit.js +++ b/rules/component-limit.js @@ -6,6 +6,7 @@ * * @styleguideReference {johnpapa} `y001` Define 1 component per file * @version 0.11.0 + * @category bestPractice */ 'use strict'; diff --git a/rules/controller-as-route.js b/rules/controller-as-route.js index 2c14b794..dfbe2d0c 100644 --- a/rules/controller-as-route.js +++ b/rules/controller-as-route.js @@ -5,12 +5,13 @@ * * @styleguideReference {johnpapa} `y031` controllerAs Controller Syntax * @version 0.1.0 + * @category bestPractice */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { var routeObject = null; @@ -45,7 +46,7 @@ module.exports = function(context) { var isObjectState = node.arguments.length === 1; stateObject = isObjectState ? node.arguments[0] : node.arguments[1]; - if (stateObject.properties) { + if (stateObject && stateObject.properties) { stateObject.properties.forEach(function(prop) { if (prop.key.name === 'controller') { controllerProp = prop; diff --git a/rules/controller-as-vm.js b/rules/controller-as-vm.js index 1081ead6..eec549a0 100644 --- a/rules/controller-as-vm.js +++ b/rules/controller-as-vm.js @@ -7,11 +7,13 @@ * * @styleguideReference {johnpapa} `y032` controllerAs with vm * @version 0.1.0 + * @category bestPractice */ 'use strict'; +var utils = require('./utils/utils'); + module.exports = function(context) { - var utils = require('./utils/utils'); var badStatements = []; var badCaptureStatements = []; var controllerFunctions = []; diff --git a/rules/controller-as.js b/rules/controller-as.js index 531ae4c4..a67147d9 100644 --- a/rules/controller-as.js +++ b/rules/controller-as.js @@ -7,11 +7,13 @@ * * @styleguideReference {johnpapa} `y031` controllerAs Controller Syntax * @version 0.1.0 + * @category bestPractice */ 'use strict'; +var utils = require('./utils/utils'); + module.exports = function(context) { - var utils = require('./utils/utils'); var badStatements = []; var controllerFunctions = []; diff --git a/rules/controller-name.js b/rules/controller-name.js index 4c021415..dd8bdcce 100644 --- a/rules/controller-name.js +++ b/rules/controller-name.js @@ -8,12 +8,13 @@ * @styleguideReference {johnpapa} `y123` Controller Names * @styleguideReference {johnpapa} `y124` Controller Name Suffix * @version 0.1.0 + * @category naming */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/deferred.js b/rules/deferred.js index 4ed7a577..68f4de83 100644 --- a/rules/deferred.js +++ b/rules/deferred.js @@ -4,6 +4,7 @@ * When you want to create a new promise, you should not use the $q.deferred anymore. * Prefer the new syntax : $q(function(resolve, reject){}) * @version 0.1.0 + * @category bestPractice */ 'use strict'; diff --git a/rules/definedundefined.js b/rules/definedundefined.js index 3b663bcb..8005cf27 100644 --- a/rules/definedundefined.js +++ b/rules/definedundefined.js @@ -5,10 +5,21 @@ * We also check the use of !angular.isUndefined and !angular.isDefined (should prefer the reverse function) * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; +var utils = require('./utils/utils'); + + module.exports = function(context) { + function isCompareOperator(operator) { + return operator === '===' || operator === '!==' || operator === '==' || operator === '!='; + } + function reportError(node) { + context.report(node, 'You should not use directly the "undefined" keyword. Prefer ' + + 'angular.isUndefined or angular.isDefined', {}); + } /** * Rule that check if we use angular.is(Un)defined() instead of the undefined keyword */ @@ -26,15 +37,15 @@ module.exports = function(context) { } }, BinaryExpression: function(node) { - if (node.operator === '===' || node.operator === '!==') { - if (node.left.type === 'Identifier' && node.left.name === 'undefined') { - context.report(node, 'You should not use directly the "undefined" keyword. Prefer ' + - 'angular.isUndefined or angular.isDefined', {}); - } - - if (node.right.type === 'Identifier' && node.right.name === 'undefined') { - context.report(node, 'You should not use directly the "undefined" keyword. Prefer ' + - 'angular.isUndefined or angular.isDefined', {}); + if (isCompareOperator(node.operator)) { + if (utils.isTypeOfStatement(node.left) && node.right.value === 'undefined') { + reportError(node); + } else if (utils.isTypeOfStatement(node.right) && node.left.value === 'undefined') { + reportError(node); + } else if (node.left.type === 'Identifier' && node.left.name === 'undefined') { + reportError(node); + } else if (node.right.type === 'Identifier' && node.right.name === 'undefined') { + reportError(node); } } } diff --git a/rules/di-order.js b/rules/di-order.js index 47a3c778..501758a7 100644 --- a/rules/di-order.js +++ b/rules/di-order.js @@ -6,6 +6,7 @@ * This means for example that `_$httpBackend_` goes before `_$http_`. * * @version 0.6.0 + * @category conventions */ 'use strict'; diff --git a/rules/di-unused.js b/rules/di-unused.js index 61577875..264a0c2b 100644 --- a/rules/di-unused.js +++ b/rules/di-unused.js @@ -4,6 +4,7 @@ * Unused dependencies should not be injected. * * @version 0.8.0 + * @category bestPractice */ 'use strict'; diff --git a/rules/di.js b/rules/di.js index 284ecdaf..2c0973f5 100644 --- a/rules/di.js +++ b/rules/di.js @@ -4,13 +4,14 @@ * All your DI should use the same syntax : the Array, function, or $inject syntaxes ("di": [2, "array, function, or $inject"]) * * @version 0.1.0 + * @category conventions */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); - var angularNamedObjectList = ['value', 'factory', 'service', 'provider', 'controller', 'filter', 'directive']; +module.exports = function(context) { + var angularNamedObjectList = ['factory', 'service', 'provider', 'controller', 'filter', 'directive']; function report(node, syntax) { context.report(node, 'You should use the {{syntax}} syntax for DI', { diff --git a/rules/directive-name.js b/rules/directive-name.js index 08f21999..f10039b0 100644 --- a/rules/directive-name.js +++ b/rules/directive-name.js @@ -8,11 +8,13 @@ * @styleguideReference {johnpapa} `y073` Provide a Unique Directive Prefix * @styleguideReference {johnpapa} `y126` Directive Component Names * @version 0.1.0 + * @category naming */ 'use strict'; +var utils = require('./utils/utils'); + module.exports = function(context) { - var utils = require('./utils/utils'); if (context.settings.angular === 2) { return {}; } diff --git a/rules/directive-restrict.js b/rules/directive-restrict.js index e081992d..221f4242 100644 --- a/rules/directive-restrict.js +++ b/rules/directive-restrict.js @@ -8,12 +8,13 @@ * * @styleguideReference {johnpapa} `y074` Restrict to Elements and Attributes * @version 0.12.0 + * @category bestPractice */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { var options = context.options[0] || {}; var restrictOpt = options.restrict || 'AE'; var explicitRestrict = options.explicit === 'always'; diff --git a/rules/document-service.js b/rules/document-service.js index b08c26a0..70ed2b60 100644 --- a/rules/document-service.js +++ b/rules/document-service.js @@ -5,6 +5,7 @@ * * @styleguideReference {johnpapa} `y180` Angular $ Wrapper Services - $document and $window * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/dumb-inject.js b/rules/dumb-inject.js new file mode 100644 index 00000000..91f647ff --- /dev/null +++ b/rules/dumb-inject.js @@ -0,0 +1,71 @@ +/** + * unittest `inject` functions should only consist of assignments from injected values to describe block variables + * + * `inject` functions in unittests should only contain a sorted mapping of injected values to values in the `describe` block with matching names. + * This way the dependency injection setup is separated from the other setup logic, improving readability of the test. + * + * @version 0.15.0 + * @category conventions + */ +'use strict'; + +var angularRule = require('./utils/angular-rule'); + + +module.exports = angularRule(function(context) { + function report(node, name) { + context.report(node, 'inject functions may only consist of assignments in the form {{name}} = _{{name}}_', { + name: name || 'myService' + }); + } + + return { + 'angular:inject': function(callExpression, fn) { + if (!fn) { + return; + } + var valid = []; + // Report bad statement types + fn.body.body.forEach(function(statement) { + if (statement.type !== 'ExpressionStatement') { + return report(statement); + } + if (statement.expression.type !== 'AssignmentExpression') { + return report(statement); + } + if (statement.expression.right.type !== 'Identifier') { + return report(statement); + } + // From this point there is more context on what to report. + var name = statement.expression.right.name.replace(/^_(.+)_$/, '$1'); + if (statement.expression.left.type !== 'Identifier') { + return report(statement, name); + } + if (statement.expression.right.name !== '_' + name + '_') { + return report(statement, name); + } + if (statement.expression.left.name !== name) { + return report(statement, name); + } + // Register valid statements for sort order validation + valid.push(statement); + }); + // Validate the sorting order + var lastValid; + valid.forEach(function(statement) { + if (!lastValid) { + lastValid = statement.expression.left.name; + return; + } + if (statement.expression.left.name.localeCompare(lastValid) !== -1) { + lastValid = statement.expression.left.name; + return; + } + context.report(statement, "'{{current}}' must be sorted before '{{previous}}'", { + current: statement.expression.left.name, + previous: lastValid + }); + }); + } + }; +}); diff --git a/rules/empty-controller.js b/rules/empty-controller.js index 6d80ef3c..26d1187f 100644 --- a/rules/empty-controller.js +++ b/rules/empty-controller.js @@ -5,12 +5,13 @@ * You can remove this declaration because this controller is useless * * @version 0.1.0 + * @category bestPractice */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function report(node, name) { context.report(node, 'The {{ctrl}} controller is useless because empty. You can remove it from your Router configuration or in one of your view', { ctrl: name diff --git a/rules/file-name.js b/rules/file-name.js index 32ffaff7..6ae8a410 100644 --- a/rules/file-name.js +++ b/rules/file-name.js @@ -9,12 +9,15 @@ * @styleguideReference {johnpapa} `y120` Naming - Naming Guidelines * @styleguideReference {johnpapa} `y121` Naming - Feature File Names * @version 0.7.0 + * @category naming */ 'use strict'; +var path = require('path'); + +var utils = require('./utils/utils'); + module.exports = (function() { - var utils = require('./utils/utils'); - var path = require('path'); var fileEnding = '.js'; var separators = { diff --git a/rules/filter-name.js b/rules/filter-name.js index cd4374bf..0e1d9fcd 100644 --- a/rules/filter-name.js +++ b/rules/filter-name.js @@ -6,12 +6,13 @@ * ("filter-name": [2, "ng"]) * * @version 0.1.0 + * @category naming */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/foreach.js b/rules/foreach.js index ffc2eca8..5e5bedf8 100644 --- a/rules/foreach.js +++ b/rules/foreach.js @@ -4,6 +4,7 @@ * You should use the angular.forEach method instead of the default JavaScript implementation [].forEach. * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/function-type.js b/rules/function-type.js index d83fa632..8a84381c 100644 --- a/rules/function-type.js +++ b/rules/function-type.js @@ -8,11 +8,13 @@ * @linkDescription require and specify a consistent function style for components ('named' or 'anonymous') * @styleguideReference {johnpapa} `y024` Named vs Anonymous Functions * @version 0.1.0 + * @category conventions */ 'use strict'; +var utils = require('./utils/utils'); + module.exports = function(context) { - var utils = require('./utils/utils'); var angularObjectList = ['animation', 'config', 'constant', 'controller', 'directive', 'factory', 'filter', 'provider', 'service', 'value', 'decorator']; var configType = context.options[0] || 'anonymous'; var messageByConfigType = { diff --git a/rules/interval-service.js b/rules/interval-service.js index 313976d5..b89370c1 100644 --- a/rules/interval-service.js +++ b/rules/interval-service.js @@ -5,6 +5,7 @@ * * @styleguideReference {johnpapa} `y181` Angular $ Wrapper Services - $timeout and $interval * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/json-functions.js b/rules/json-functions.js index b73acae2..8f310d78 100644 --- a/rules/json-functions.js +++ b/rules/json-functions.js @@ -5,6 +5,7 @@ * * @linkDescription use `angular.fromJson` and 'angular.toJson' instead of `JSON.parse` and `JSON.stringify` * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/log.js b/rules/log.js index a36e3da8..df8c3faf 100644 --- a/rules/log.js +++ b/rules/log.js @@ -3,6 +3,7 @@ * * You should use $log service instead of console for the methods 'log', 'debug', 'error', 'info', 'warn' * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/module-dependency-order.js b/rules/module-dependency-order.js index 70b6cdac..c63ce015 100644 --- a/rules/module-dependency-order.js +++ b/rules/module-dependency-order.js @@ -10,12 +10,13 @@ * ('module-dependency-order', [2, {grouped: true, prefix: "app"}]) * * @version 0.12.0 + * @category conventions */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { var options = context.options[0] || {}; var groupedMode = options.grouped !== false; var moduleRegex; diff --git a/rules/module-getter.js b/rules/module-getter.js index 6c014018..02e93670 100644 --- a/rules/module-getter.js +++ b/rules/module-getter.js @@ -6,12 +6,13 @@ * @linkDescription disallow to reference modules with variables and require to use the getter syntax instead `angular.module('myModule')` * @styleguideReference {johnpapa} `y022` Module - Getters * @version 0.1.0 + * @category possibleError */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { ExpressionStatement: function(node) { diff --git a/rules/module-name.js b/rules/module-name.js index 4c08ab09..8c95ed2e 100644 --- a/rules/module-name.js +++ b/rules/module-name.js @@ -7,12 +7,13 @@ * * @styleguideReference {johnpapa} `y127` Naming - Modules * @version 0.1.0 + * @category naming */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/module-setter.js b/rules/module-setter.js index 163d55f6..42518c1e 100644 --- a/rules/module-setter.js +++ b/rules/module-setter.js @@ -6,13 +6,13 @@ * @linkDescription disallow to assign modules to variables (linked to [module-getter](docs/module-getter.md) * @styleguideReference {johnpapa} `y021` Module - Definitions (aka Setters) * @version 0.1.0 + * @category possibleError */ 'use strict'; +var utils = require('./utils/utils'); module.exports = function(context) { - var utils = require('./utils/utils'); - return { VariableDeclaration: function(node) { diff --git a/rules/no-angular-mock.js b/rules/no-angular-mock.js index 8eca50cf..a3ddc280 100644 --- a/rules/no-angular-mock.js +++ b/rules/no-angular-mock.js @@ -5,6 +5,7 @@ * So you can remove angular.mock from your code * * @version 0.2.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/no-controller.js b/rules/no-controller.js index a50eb185..4d3214d4 100644 --- a/rules/no-controller.js +++ b/rules/no-controller.js @@ -4,12 +4,13 @@ * According to the Component-First pattern, we should avoid the use of AngularJS controller. * * @version 0.9.0 + * @category bestPractice */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/no-cookiestore.js b/rules/no-cookiestore.js index cc72fab3..dfe974e4 100644 --- a/rules/no-cookiestore.js +++ b/rules/no-cookiestore.js @@ -5,6 +5,7 @@ * Please use the $cookies service instead * * @version 0.3.0 + * @category deprecatedAngularFeature */ 'use strict'; @@ -13,7 +14,7 @@ module.exports = function(context) { MemberExpression: function(node) { if (node.object && node.object.name === '$cookieStore') { - context.report(node, 'Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service.', {}); + context.report(node, 'Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service.', {}); } } }; diff --git a/rules/no-digest.js b/rules/no-digest.js index 3e56e725..cb6097ca 100644 --- a/rules/no-digest.js +++ b/rules/no-digest.js @@ -1,11 +1,14 @@ /** * use `$apply()` instead of `$digest()` * - * DEPRECATED! The scope's $digest() method shouldn't be used. + * The scope's $digest() method shouldn't be used. * You should prefer the $apply method. * - * @linkDescription DEPRECATED! use `$apply()` instead of `$digest()` (replaced by [watchers-execution](docs/watchers-execution.md)) + * The `watchers-execution` rule can be configured to enforce the use of `$apply()` or `$digest()`. + * + * @linkDescription use `$apply()` instead of `$digest()` (replaced by [watchers-execution](docs/watchers-execution.md)) * @version 0.1.0 + * @deprecated There is no reason to forbid the use of `$digest()` in general. */ 'use strict'; diff --git a/rules/no-directive-replace.js b/rules/no-directive-replace.js new file mode 100644 index 00000000..83355d08 --- /dev/null +++ b/rules/no-directive-replace.js @@ -0,0 +1,101 @@ +/** + * disallow the deprecated directive replace property + * + * This rule disallows the replace attribute in a directive definition object. + * The replace property of a directive definition object is deprecated since angular 1.3 ([latest angular docs](https://docs.angularjs.org/api/ng/service/$compile). + * + * The option `ignoreReplaceFalse` let you ignore directive definitions with replace set to false. + * + * @version 0.15.0 + * @category deprecatedAngularFeature + */ +'use strict'; + +var angularRule = require('./utils/angular-rule'); + +module.exports = angularRule(function(context) { + var options = context.options[0] || {}; + var ignoreReplaceFalse = !!options.ignoreReplaceFalse; + + var potentialReplaceNodes = {}; + + function addPotentialReplaceNode(variableName, node) { + var nodeList = potentialReplaceNodes[variableName] || []; + + nodeList.push({ + name: variableName, + node: node, + block: context.getScope().block.body + }); + + potentialReplaceNodes[variableName] = nodeList; + } + + return { + 'angular:directive': function(callExpressionNode, fnNode) { + if (!fnNode || !fnNode.body) { + return; + } + fnNode.body.body.forEach(function(statement) { + if (statement.type === 'ReturnStatement') { + // get potential replace node by argument name of empty string for object expressions + var potentialNodes = potentialReplaceNodes[statement.argument.name || '']; + if (!potentialNodes) { + return; + } + potentialNodes.forEach(function(report) { + // only reports nodes that belong to the same expression + if (report.block === statement.parent) { + context.report(report.node, 'Directive definition property replace is deprecated.'); + } + }); + } + }); + }, + AssignmentExpression: function(node) { + // Only check for literal member property assignments. + if (node.left.type !== 'MemberExpression') { + return; + } + // Only check setting properties named 'replace'. + if (node.left.property.name !== 'replace') { + return; + } + if (ignoreReplaceFalse && node.right.value === false) { + return; + } + addPotentialReplaceNode(node.left.object.name, node); + }, + Property: function(node) { + // This only checks for objects which have defined a literal restrict property. + if (node.key.name !== 'replace') { + return; + } + if (ignoreReplaceFalse === true && node.value.value === false) { + return; + } + + // assumption: Property always belongs to a ObjectExpression + var objectExpressionParent = node.parent.parent; + + // add to potential replace nodes if the object is defined in a variable + if (objectExpressionParent.type === 'VariableDeclarator') { + addPotentialReplaceNode(objectExpressionParent.id.name, node); + } + + // report directly if object is part of a return statement and inside a directive body + if (objectExpressionParent.type === 'ReturnStatement') { + addPotentialReplaceNode('', node); + } + } + }; +}); + +module.exports.schema = [{ + type: 'object', + properties: { + ignoreReplaceFalse: { + type: 'boolean' + } + } +}]; diff --git a/rules/no-http-callback.js b/rules/no-http-callback.js index 39c5ef77..cef62501 100644 --- a/rules/no-http-callback.js +++ b/rules/no-http-callback.js @@ -5,6 +5,7 @@ * Instead the standard promise API should be used. * * @version 0.12.0 + * @category deprecatedAngularFeature */ 'use strict'; diff --git a/rules/no-inline-template.js b/rules/no-inline-template.js index 63cfa1c2..4c990843 100644 --- a/rules/no-inline-template.js +++ b/rules/no-inline-template.js @@ -6,6 +6,7 @@ * ('no-inline-template': [0, {allowSimple: true}]) * * @version 0.12.0 + * @category bestPractice */ 'use strict'; diff --git a/rules/no-jquery-angularelement.js b/rules/no-jquery-angularelement.js index ea3f74c6..66c64cfb 100644 --- a/rules/no-jquery-angularelement.js +++ b/rules/no-jquery-angularelement.js @@ -3,6 +3,7 @@ * * You should not wrap angular.element object into jQuery(), because angular.element already return jQLite element * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/no-private-call.js b/rules/no-private-call.js index bb637202..9d1051b9 100644 --- a/rules/no-private-call.js +++ b/rules/no-private-call.js @@ -6,6 +6,7 @@ * Exception can be allowed with this option: {allow:['$$watchers']} * * @version 0.1.0 + * @category possibleError */ 'use strict'; diff --git a/rules/no-run-logic.js b/rules/no-run-logic.js new file mode 100644 index 00000000..088d6e8d --- /dev/null +++ b/rules/no-run-logic.js @@ -0,0 +1,59 @@ +/** + * 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 + * @category bestPractice + */ +'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/rules/no-service-method.js b/rules/no-service-method.js index a84674e0..f210c009 100644 --- a/rules/no-service-method.js +++ b/rules/no-service-method.js @@ -5,12 +5,13 @@ * * @styleguideReference {johnpapa} `y040` Services - Singletons * @version 0.1.0 + * @category conventions */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/no-services.js b/rules/no-services.js index 63aec6a7..e15a68b7 100644 --- a/rules/no-services.js +++ b/rules/no-services.js @@ -8,12 +8,13 @@ * * @linkDescription disallow DI of specified services for other angular components (`$http` for controllers, filters and directives) * @version 0.1.0 + * @category bestPractice */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { var angularObjectList = ['controller', 'filter', 'directive']; var badServices; var map; diff --git a/rules/on-watch.js b/rules/on-watch.js index 6ad723ae..23a3274b 100644 --- a/rules/on-watch.js +++ b/rules/on-watch.js @@ -3,6 +3,7 @@ * * Watch and On methods on the scope object should be assigned to a variable, in order to be deleted in a $destroy event handler * @version 0.1.0 + * @category bestPractice */ 'use strict'; diff --git a/rules/one-dependency-per-line.js b/rules/one-dependency-per-line.js index 89ed39ec..dc895299 100644 --- a/rules/one-dependency-per-line.js +++ b/rules/one-dependency-per-line.js @@ -4,6 +4,7 @@ * Injected dependencies should be written one per line. * * @version 0.14.0 + * @category conventions */ 'use strict'; diff --git a/rules/rest-service.js b/rules/rest-service.js index e771be1c..90e2627e 100644 --- a/rules/rest-service.js +++ b/rules/rest-service.js @@ -5,12 +5,13 @@ * This rule can have one parameter, with one of the following values: $http, $resource or Restangular ('rest-service': [0, '$http']). * * @version 0.5.0 + * @category conventions */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { var angularObjectList = ['controller', 'filter', 'directive', 'service', 'factory', 'provider']; var services = ['$http', '$resource', 'Restangular']; var message = 'You should use the same service ({{method}}) for REST API calls'; diff --git a/rules/service-name.js b/rules/service-name.js index 7993655c..b214f5af 100644 --- a/rules/service-name.js +++ b/rules/service-name.js @@ -7,12 +7,13 @@ ** * @styleguideReference {johnpapa} `y125` Naming - Factory and Service Names * @version 0.1.0 + * @category naming */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { return { CallExpression: function(node) { diff --git a/rules/timeout-service.js b/rules/timeout-service.js index 932c4d29..492609bf 100644 --- a/rules/timeout-service.js +++ b/rules/timeout-service.js @@ -5,6 +5,7 @@ ** * @styleguideReference {johnpapa} `y181` Angular $ Wrapper Services - $timeout and $interval * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/rules/typecheck-array.js b/rules/typecheck-array.js index af262cc3..fcbbfd42 100644 --- a/rules/typecheck-array.js +++ b/rules/typecheck-array.js @@ -4,12 +4,13 @@ * You should use the angular.isArray method instead of the default JavaScript implementation (typeof [] === "[object Array]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && node.value === '[object Array]') { context.report(origin, 'You should use the angular.isArray method', {}); diff --git a/rules/typecheck-date.js b/rules/typecheck-date.js index 6e53d7a6..7df5c01d 100644 --- a/rules/typecheck-date.js +++ b/rules/typecheck-date.js @@ -4,12 +4,13 @@ * You should use the angular.isDate method instead of the default JavaScript implementation (typeof new Date() === "[object Date]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && node.value === '[object Date]') { context.report(origin, 'You should use the angular.isDate method', {}); diff --git a/rules/typecheck-function.js b/rules/typecheck-function.js index 8735f538..6632cb9f 100644 --- a/rules/typecheck-function.js +++ b/rules/typecheck-function.js @@ -4,12 +4,13 @@ * You should use the angular.isFunction method instead of the default JavaScript implementation (typeof function(){} ==="[object Function]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && (node.value === 'function' || node.value === '[object Function]')) { context.report(origin, 'You should use the angular.isFunction method', {}); diff --git a/rules/typecheck-number.js b/rules/typecheck-number.js index 5b632622..b1fc34fc 100644 --- a/rules/typecheck-number.js +++ b/rules/typecheck-number.js @@ -4,12 +4,13 @@ * You should use the angular.isNumber method instead of the default JavaScript implementation (typeof 3 === "[object Number]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && (node.value === 'number' || node.value === '[object Number]')) { context.report(origin, 'You should use the angular.isNumber method', {}); diff --git a/rules/typecheck-object.js b/rules/typecheck-object.js index 762347c6..8ac0ade0 100644 --- a/rules/typecheck-object.js +++ b/rules/typecheck-object.js @@ -4,12 +4,13 @@ * You should use the angular.isObject method instead of the default JavaScript implementation (typeof {} === "[object Object]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && (node.value === 'object' || node.value === '[object Object]')) { context.report(origin, 'You should use the angular.isObject method', {}); diff --git a/rules/typecheck-regexp.js b/rules/typecheck-regexp.js index 38de545d..0ec33a45 100644 --- a/rules/typecheck-regexp.js +++ b/rules/typecheck-regexp.js @@ -1,16 +1,17 @@ /** * use `angular.isRegexp` instead of other comparisons * - * DEPRECATED! You should use the angular.isRegexp method instead of the default JavaScript implementation (toString.call(/^A/) === "[object RegExp]"). + * You should use the angular.isRegexp method instead of the default JavaScript implementation (toString.call(/^A/) === "[object RegExp]"). * - * @linkDescription DEPRECATED! use `angular.isRegexp` instead of other comparisons (no native angular method) + * @linkDescription use `angular.isRegexp` instead of other comparisons (no native angular method) * @version 0.1.0 + * @deprecated `angular.isRegexp` is no built-in angular method. */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && node.value === '[object RegExp]') { context.report(origin, 'You should use the angular.isRegexp method', {}); diff --git a/rules/typecheck-string.js b/rules/typecheck-string.js index 0a984302..e3a2e147 100644 --- a/rules/typecheck-string.js +++ b/rules/typecheck-string.js @@ -4,12 +4,13 @@ * You should use the angular.isString method instead of the default JavaScript implementation (typeof "" === "[object String]"). * * @version 0.1.0 + * @category angularWrapper */ 'use strict'; -module.exports = function(context) { - var utils = require('./utils/utils'); +var utils = require('./utils/utils'); +module.exports = function(context) { function recordError(node, origin) { if (node.type === 'Literal' && (node.value === 'string' || node.value === '[object String]')) { context.report(origin, 'You should use the angular.isString method', {}); diff --git a/rules/utils/angular-rule.js b/rules/utils/angular-rule.js index 5309e308..6e6ba489 100644 --- a/rules/utils/angular-rule.js +++ b/rules/utils/angular-rule.js @@ -184,9 +184,6 @@ function angularRule(ruleDefinition) { function findFunctionByNode(callExpressionNode, scope) { var node; if (callExpressionNode.callee.type === 'Identifier') { - if (callExpressionNode.callee.name !== 'inject') { - return; - } node = callExpressionNode.arguments[0]; } else if (callExpressionNode.callee.property.name === 'run' || callExpressionNode.callee.property.name === 'config') { node = callExpressionNode.arguments[0]; diff --git a/rules/utils/rulesConfiguration.js b/rules/utils/rulesConfiguration.js index d069ec6b..7adc2370 100644 --- a/rules/utils/rulesConfiguration.js +++ b/rules/utils/rulesConfiguration.js @@ -3,7 +3,7 @@ function Rule(name, config) { this.name = name; this.config = config; - this._requireRule = require('../' + this.name); + this._requireRule = require('../' + this.name); // eslint-disable-line global-require } Rule.prototype = { diff --git a/rules/watchers-execution.js b/rules/watchers-execution.js index 7d7ec718..9d4c0e1e 100644 --- a/rules/watchers-execution.js +++ b/rules/watchers-execution.js @@ -5,6 +5,7 @@ * This will cause an performance improvement comparing to the $apply method, who start from the $rootScope * * @version 0.4.0 + * @category conventions */ 'use strict'; diff --git a/rules/window-service.js b/rules/window-service.js index 814faefe..28ee09ef 100644 --- a/rules/window-service.js +++ b/rules/window-service.js @@ -5,6 +5,7 @@ * * @styleguideReference {johnpapa} `y180` Angular $ Wrapper Services - $document and $window * @version 0.1.0 + * @category angularWrapper */ 'use strict'; diff --git a/scripts/docs.js b/scripts/docs.js index da695b6e..75277881 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -3,7 +3,8 @@ var fs = require('fs'); var parseComments = require('parse-comments'); var _ = require('lodash'); -var eslintAngularIndex = require('../index.js'); +var eslintAngularIndex = require('../index'); +var ruleCategories = require('./ruleCategories.json'); var RuleTester = require('eslint').RuleTester; var templates = require('./templates.js'); @@ -41,7 +42,15 @@ function createDocFiles(cb) { * @param cb callback */ function updateReadme(readmePath, cb) { - var readmeRuleSection = templates.readmeRuleSectionContent(this); + ruleCategories.rulesByCategory = _.groupBy(this.rules, 'category'); + + // filter categories without rules + ruleCategories.categoryOrder = ruleCategories.categoryOrder.filter(function(categoryName) { + var rulesForCategory = ruleCategories.rulesByCategory[categoryName]; + return rulesForCategory && rulesForCategory.length > 0; + }); + + var readmeRuleSection = templates.readmeRuleSectionContent(ruleCategories); var readmeContent = fs.readFileSync(readmePath).toString(); // use split and join to prevent the replace() and dollar sign problem (http://stackoverflow.com/questions/9423722) @@ -175,13 +184,21 @@ function _createRule(ruleName) { rule.linkDescription = mainRuleComment.linkDescription ? mainRuleComment.linkDescription : rule.lead; rule.styleguideReferences = mainRuleComment.styleguideReferences || []; rule.version = mainRuleComment.version; + rule.category = mainRuleComment.category || 'uncategorizedRule'; + + rule.deprecated = !!mainRuleComment.deprecated; + + if (rule.deprecated) { + rule.deprecationReason = mainRuleComment.deprecated; + rule.category = 'deprecatedRule'; + } if (!rule.version) { throw new Error('No @version found for ' + ruleName); } // load rule module for tests - rule.module = require('../rules/' + rule.ruleName); + rule.module = require('../rules/' + rule.ruleName); // eslint-disable-line global-require // load examples, prepare them for the tests and group the for the template rule.allExamples = _loadExamples(rule); diff --git a/scripts/ruleCategories.json b/scripts/ruleCategories.json new file mode 100644 index 00000000..c0b4a90d --- /dev/null +++ b/scripts/ruleCategories.json @@ -0,0 +1,51 @@ +{ + "categoryOrder": [ + "possibleError", + "bestPractice", + "deprecatedAngularFeature", + "naming", + "conventions", + "angularWrapper", + "deprecatedRule", + "removedRule", + "uncategorizedRule" + ], + "categories": { + "possibleError": { + "headline": "Possible Errors", + "description": "The following rules detect patterns that can lead to errors." + }, + "bestPractice": { + "headline": "Best Practices", + "description": "These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns.." + }, + "deprecatedAngularFeature": { + "headline": "Deprecated Angular Features", + "description": "These rules prevent you from using deprecated angular features." + }, + "naming": { + "headline": "Naming", + "description": "These rules help you to specify several naming conventions." + }, + "conventions": { + "headline": "Conventions", + "description": "Angular often provide multi ways to to something. These rules help you to define convention for your project." + }, + "angularWrapper": { + "headline": "Angular Wrappers", + "description": "These rules help you to enforce the usage of angular wrappers." + }, + "deprecatedRule": { + "headline": "Deprecated rules", + "description": "These rules will be removed in version 1.0.0" + }, + "removedRule": { + "headline": "Removed", + "description": "These rules were removed and only exist to inform users about their removal." + }, + "uncategorizedRule": { + "headline": "Uncategorized rule (only for development)", + "description": "Add a @category tag to your rule." + } + } +} diff --git a/scripts/templates.js b/scripts/templates.js index a6e2138a..efbbc17c 100644 --- a/scripts/templates.js +++ b/scripts/templates.js @@ -8,6 +8,7 @@ var templates = { ruleDocumentationPath: _.template('docs/<%= ruleName %>.md'), ruleExamplesPath: _.template('examples/<%= ruleName %>.js'), styleguide: _.template('[<%= name %> by <%= type %> - <%= description %>](<%= link %>)'), + styleguideShort: _.template('[<%= name %>](<%= link %>)'), styleguideLinks: { johnpapa: _.template('https://github.com/johnpapa/angular-styleguide#style-<%= name %>') } @@ -17,15 +18,16 @@ var templatesDir = './scripts/templates/'; var templateSettings = { imports: { formatStyleguideReference: function(styleRef) { - var linkTemplate = templates.styleguideLinks[styleRef.type]; - if (!linkTemplate) { - throw new Error('No styleguide link template for styleguide type: "' + styleRef.type); + return templates.styleguide(styleguideReferenceTemplateContext(styleRef)); + }, + formatStyleguideReferenceListShort: function(rule) { + if (!rule.styleguideReferences || rule.styleguideReferences.length === 0) { + return ''; } - var templateContext = _.extend({ - link: linkTemplate(styleRef) - }, styleRef); - - return templates.styleguide(templateContext); + return ' (' + rule.styleguideReferences + .map(styleguideReferenceTemplateContext) + .map(templates.styleguideShort).join(', ') + + ')'; }, formatConfigAsJson: function(examples) { var config = examples[0].displayOptions; @@ -59,3 +61,13 @@ fs.readdirSync(templatesDir).forEach(function(templateFilename) { }); module.exports = templates; + +function styleguideReferenceTemplateContext(styleRef) { + var linkTemplate = templates.styleguideLinks[styleRef.type]; + if (!linkTemplate) { + throw new Error('No styleguide link template for styleguide type: "' + styleRef.type); + } + return _.extend({ + link: linkTemplate(styleRef) + }, styleRef); +} diff --git a/scripts/templates/readmeRuleSectionContent.template.md b/scripts/templates/readmeRuleSectionContent.template.md index 4565f477..bdeac6fc 100644 --- a/scripts/templates/readmeRuleSectionContent.template.md +++ b/scripts/templates/readmeRuleSectionContent.template.md @@ -1,6 +1,13 @@ ## Rules -<% _.each(rules, function (rule) { %> - * [<%= rule.ruleName %>](<%= rule.documentationPath %>) - <%= rule.linkDescription %><% }) %> +Rules in eslint-plugin-angular are divided into several categories to help you better understand their value. + +<% _.each(categoryOrder, function (categoryName) { %> +## <%= categories[categoryName].headline %> + +<%= categories[categoryName].description %> +<% _.each(rulesByCategory[categoryName], function (rule) { %> + * [<%= rule.ruleName %>](<%= rule.documentationPath %>) - <%= rule.linkDescription %><%= formatStyleguideReferenceListShort(rule) %><% }) %> +<% }) %> ---- diff --git a/scripts/templates/ruleDocumentationContent.template.md b/scripts/templates/ruleDocumentationContent.template.md index f8627a5f..97e9d8bc 100644 --- a/scripts/templates/ruleDocumentationContent.template.md +++ b/scripts/templates/ruleDocumentationContent.template.md @@ -2,6 +2,10 @@ # <%= ruleName %> - <%= lead %> +<% if(deprecated) { %> +**This rule is deprecated and will be removed in future versions. Explanation: <%= deprecationReason %>** +<% } %> + <%= description %> <% if(styleguideReferences.length > 0) { %> diff --git a/test/component-limit.js b/test/component-limit.js index 92683555..a539b225 100644 --- a/test/component-limit.js +++ b/test/component-limit.js @@ -22,7 +22,17 @@ eslintTester.run('component-limit', rule, { 'angular.module("").factory();', 'angular.module("").filter();', 'angular.module("").provider();', + 'angular.module("").run();', 'angular.module("").service();', + 'angular.module("").animation("", "");', + 'angular.module("").config("");', + 'angular.module("").controller("", "");', + 'angular.module("").directive("", "");', + 'angular.module("").factory("", "");', + 'angular.module("").filter("", "");', + 'angular.module("").provider("", "");', + 'angular.module("").run("");', + 'angular.module("").service("", "");', // Identified potential false positives '$scope.$on("", function() {});$scope.$on("", function() {});', 'app.service("", function(myService) { var data = {}; myService.someMethod("", data); });', diff --git a/test/controller-as-route.js b/test/controller-as-route.js index 3c21e3c9..c095d736 100644 --- a/test/controller-as-route.js +++ b/test/controller-as-route.js @@ -26,7 +26,8 @@ eslintTester.run('controller-as-route', rule, { 'var state = "mystate2"', 'something[type][changeType][state](test)', 'var when = "mystate2"', - 'something[type][changeType][when](test)' + 'something[type][changeType][when](test)', + '$stateProvider.state();' ].concat(commonFalsePositives), invalid: [ {code: '$routeProvider.when("/myroute", {controller: "MyController"})', diff --git a/test/definedundefined.js b/test/definedundefined.js index 4a6710fd..f3e1cc4a 100644 --- a/test/definedundefined.js +++ b/test/definedundefined.js @@ -16,13 +16,26 @@ var eslintTester = new RuleTester(); eslintTester.run('definedundefined', rule, { valid: [ 'angular.isUndefined(toto)', - 'angular.isDefined(toto)' + 'angular.isDefined(toto)', + // possible false positives + 'variable === otherValue', + 'variable === null', + 'variable > undefined', + 'angular.isString(null)' ].concat(commonFalsePositives), invalid: [ {code: 'variable === undefined', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, {code: 'undefined === variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, {code: 'undefined !== variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, {code: 'variable !== undefined', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'variable == undefined', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'undefined == variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'undefined != variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'variable != undefined', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'typeof variable === "undefined"', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: 'typeof variable !== "undefined"', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: '"undefined" == typeof variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, + {code: '"undefined" != typeof variable', errors: [{message: 'You should not use directly the "undefined" keyword. Prefer angular.isUndefined or angular.isDefined'}]}, {code: '!angular.isUndefined(variable)', errors: [{message: 'Instead of !angular.isUndefined, you can use the out-of-box angular.isDefined method'}]}, {code: '!angular.isDefined(variable)', errors: [{message: 'Instead of !angular.isDefined, you can use the out-of-box angular.isUndefined method'}]} ] diff --git a/test/di-order.js b/test/di-order.js index 3cca5d61..f76b7a4d 100644 --- a/test/di-order.js +++ b/test/di-order.js @@ -28,6 +28,16 @@ eslintTester.run('di-order', rule, { 'inject(function($http, $q) {});', 'it(inject(function($http, $q) {}));', 'it(inject(function(_$http_, _$httpBackend_) {}));', + // Potential crashes + 'angular.module("").animation("", "");', + 'angular.module("").config("");', + 'angular.module("").controller("", "");', + 'angular.module("").directive("", "");', + 'angular.module("").factory("", "");', + 'angular.module("").filter("", "");', + 'angular.module("").provider("", "");', + 'angular.module("").run("");', + 'angular.module("").service("", "");', { code: 'it(inject(function(_$httpBackend_, _$http_) {}));', options: [false] diff --git a/test/di-unused.js b/test/di-unused.js index cdd8e248..fa662398 100644 --- a/test/di-unused.js +++ b/test/di-unused.js @@ -37,7 +37,17 @@ eslintTester.run('di-unused', rule, { 'angular.module("").run(["$q", function($q) {return $q;}]);', 'inject(function($q) {_$q_ = $q;});', 'angular.module("").provider("", function() {this.$get = function($q) {return $q};});', - 'angular.module("").provider("", function() {this.$get = ["$q", function($q) {return $q}];});' + 'angular.module("").provider("", function() {this.$get = ["$q", function($q) {return $q}];});', + // Potential crashes + 'angular.module("").animation("", "");', + 'angular.module("").config("");', + 'angular.module("").controller("", "");', + 'angular.module("").directive("", "");', + 'angular.module("").factory("", "");', + 'angular.module("").filter("", "");', + 'angular.module("").provider("", "");', + 'angular.module("").run("");', + 'angular.module("").service("", "");' ].concat(commonFalsePositives), invalid: [ // animation @@ -106,6 +116,11 @@ eslintTester.run('di-unused', rule, { errors: [ {message: 'Unused injected value $http'} ] + }, { + code: 'angular.module("").factory("", ["$http", "$q", function($http, $q) {return $q.resolve()}]);', + errors: [ + {message: 'Unused injected value $http'} + ] }, // filter { @@ -192,6 +207,11 @@ eslintTester.run('di-unused', rule, { }, { code: 'angular.module("").provider("", function() {this.$get = ["q", function($q) {}];});', errors: [{message: 'Unused injected value $q'}] + }, + // examples from issue #287 + { + code: 'angular.module("myapp").filter("myfilter", [ "$translate", "$filter", function ($translate, $filter) { return function (value) { return $filter(value, 4) * 100; } } ]);', + errors: [{message: 'Unused injected value $translate'}] } ] }); diff --git a/test/di.js b/test/di.js index 6adcb922..24883425 100644 --- a/test/di.js +++ b/test/di.js @@ -9,7 +9,7 @@ var RuleTester = require('eslint').RuleTester; var commonFalsePositives = require('./utils/commonFalsePositives'); -var angularNamedObjectList = ['value', 'factory', 'service', 'provider', 'controller', 'filter', 'directive']; +var angularNamedObjectList = ['factory', 'service', 'provider', 'controller', 'filter', 'directive']; var angularObjectList = ['run', 'config']; @@ -153,7 +153,19 @@ valid.push({ }, { code: 'mocha.run();', options: ['array'] +}, { + code: 'mocha.run();', + options: ['array'] +}, { + // value false positive with function + code: 'angular.module("") .value("", function () {});', + options: ['array'] +}, { + // value false positive with array (example from issue #99) + code: 'angular.module("") .value("", [{ }, { }]);', + options: ['function'] }); + // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ diff --git a/test/dumb-inject.js b/test/dumb-inject.js new file mode 100644 index 00000000..488a8556 --- /dev/null +++ b/test/dumb-inject.js @@ -0,0 +1,61 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../rules/dumb-inject'); +var RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var eslintTester = new RuleTester(); +eslintTester.run('log', rule, { + valid: [ + // Don't crash if no function is passed. + 'inject()', + // Some valid examples + 'inject(function() {$httpBackend = _$httpBackend_})', + 'inject(function() {$controller = _$controller_; $rootScope = _$rootScope_})', + 'inject(function() {$httpBackend = _$httpBackend_; myService = _myService_})' + ], + invalid: [ + // Not an expression statement + { + code: 'inject(function() {function foo() {}})', + errors: [{message: 'inject functions may only consist of assignments in the form myService = _myService_'}] + }, + // Not an assignment expression + { + code: 'inject(function() {$httpBackend.whenGET()})', + errors: [{message: 'inject functions may only consist of assignments in the form myService = _myService_'}] + }, + // Right is not a simple identifier + { + code: 'inject(function() {navigator = $window.navigator})', + errors: [{message: 'inject functions may only consist of assignments in the form myService = _myService_'}] + }, + // Left is not a simple identifier + { + code: 'inject(function() {foo.bar = _bar_})', + errors: [{message: 'inject functions may only consist of assignments in the form bar = _bar_'}] + }, + // Right is not wrapped using underscores + { + code: 'inject(function() {bar = bar})', + errors: [{message: 'inject functions may only consist of assignments in the form bar = _bar_'}] + }, + // Left does not match right + { + code: 'inject(function() {foo = _bar_})', + errors: [{message: 'inject functions may only consist of assignments in the form bar = _bar_'}] + }, + // Bad sorting of statements + { + code: 'inject(function() {foo = _foo_; bar = _bar_})', + errors: [{message: "'bar' must be sorted before 'foo'"}] + } + ] +}); diff --git a/test/no-cookiestore.js b/test/no-cookiestore.js index c20bbe65..638098c2 100644 --- a/test/no-cookiestore.js +++ b/test/no-cookiestore.js @@ -19,10 +19,10 @@ eslintTester.run('no-cookiestore', rule, { ].concat(commonFalsePositives), invalid: [{ code: '$cookieStore.get("");', - errors: [{message: 'Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service.'}] + errors: [{message: 'Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service.'}] }, { code: '$cookieStore.put("", "");', - errors: [{message: 'Since Angular 1.4, the $cookieStore service is depreacted. Please use now the $cookies service.'}] + errors: [{message: 'Since Angular 1.4, the $cookieStore service is deprecated. Please use now the $cookies service.'}] } ] }); diff --git a/test/no-directive-replace.js b/test/no-directive-replace.js new file mode 100644 index 00000000..58e35728 --- /dev/null +++ b/test/no-directive-replace.js @@ -0,0 +1,69 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../rules/no-directive-replace'); +var RuleTester = require('eslint').RuleTester; +var commonFalsePositives = require('./utils/commonFalsePositives'); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var eslintTester = new RuleTester(); +eslintTester.run('no-directive-replace', rule, { + valid: [ + 'angular.module("").factory("", function() {return {replace: false}})', + 'angular.module("").directive("")', + 'angular.module("").directive()', + 'angular.module("").directive("", function() {})', + 'angular.module("").directive("", function() {return {anotherProperty:"anotherValue"}})', + 'angular.module("").directive("", function() {return {restrict:"A"}})', + 'angular.module("").directive("", function() { var def = {}; return def; })', + 'angular.module("").directive("", function() { function x() { return {replace: true} }; x(); return {}; })', + + 'angular.module("").directive("", function() { var def = {}; def.otherProperty = true; return def; })', + 'angular.module("").directive("", function() { var nonDef = {replace: true}; return {}; })', + 'angular.module("").directive("", function() { var nonDef = {}; function x() { var nonDef = {replace: true} }; x(); return nonDef; })', + { + code: 'angular.module("").directive("", function() {return {replace:false}})', + options: [{ignoreReplaceFalse: true}] + }, + { + code: 'angular.module("").directive("", function() { var def = {}; def.replace = false; return def; })', + options: [{ignoreReplaceFalse: true}] + } + ].concat(commonFalsePositives), + invalid: [ + // Disallowed with default configuration + { + code: 'angular.module("").directive("", function() {return {replace:true}})', + errors: [{message: 'Directive definition property replace is deprecated.'}] + }, + { + code: 'angular.module("").directive("", function() { var def = {replace: true}; return def; })', + errors: [{message: 'Directive definition property replace is deprecated.'}] + }, + { + code: 'angular.module("").directive("", function() { var def = {}; def.replace = true; return def; })', + errors: [{message: 'Directive definition property replace is deprecated.'}] + }, + // Disallow replace false with default configuration + { + code: 'angular.module("").directive("", function() {return {replace:0}})', + options: [{ignoreReplaceFalse: true}], + errors: [{message: 'Directive definition property replace is deprecated.'}] + }, + // named functions + { + code: 'angular.module("").directive("", directive); function directive() { return {replace:true} };', + errors: [{message: 'Directive definition property replace is deprecated.'}] + }, + { + code: 'angular.module("").directive("", directive); function directive() { var def = {}; def.replace = true; return def; };', + errors: [{message: 'Directive definition property replace is deprecated.'}] + } + ] +}); diff --git a/test/no-run-logic.js b/test/no-run-logic.js new file mode 100644 index 00000000..58dae054 --- /dev/null +++ b/test/no-run-logic.js @@ -0,0 +1,129 @@ +'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)});', + // don't crash on component definitions + 'angular.module("").animation();', + 'angular.module("").config();', + 'angular.module("").controller();', + 'angular.module("").directive();', + 'angular.module("").factory();', + 'angular.module("").filter();', + 'angular.module("").provider();', + 'angular.module("").run();', + 'angular.module("").service();' + ], + 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'}] + } + ] +});