Browse Source

tools: update ESLint to version 2.1.0

Update ESLint to 2.1.0. ESLint has a number of potentially-useful new
features but this change attempts to be minimal in its changes.
However, some things could not be avoided reasonably.

ESLint 2.1.0 found a few lint issues that ESLing 1.x missed with
template strings that did not take advantage of any features of
template strings, and `let` declarations where `const` sufficed.

Additionally, ESLint 2.1.0 removes some granularity around enabling ES6
features. Some features (e.g., spread operator) that had been turned off
in our configuration for ESLint 1.x are now permitted.

PR-URL: https://github.com/nodejs/node/pull/5214
Reviewed-By: Michaël Zasso <mic.besace@gmail.com>
Reviewed-By: jbergstroem - Johan Bergström <bugs@bergstroem.nu>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
process-exit-stdio-flushing
Rich Trott 9 years ago
parent
commit
d7aa8fa088
  1. 14
      tools/eslint/README.md
  2. 22
      tools/eslint/bin/eslint.js
  3. 33
      tools/eslint/conf/cli-options.js
  4. 48
      tools/eslint/conf/environments.js
  5. 107
      tools/eslint/conf/eslint.json
  6. 5
      tools/eslint/conf/replacements.json
  7. 305
      tools/eslint/lib/ast-utils.js
  8. 163
      tools/eslint/lib/cli-engine.js
  9. 16
      tools/eslint/lib/cli.js
  10. 640
      tools/eslint/lib/code-path-analysis/code-path-analyzer.js
  11. 208
      tools/eslint/lib/code-path-analysis/code-path-segment.js
  12. 1323
      tools/eslint/lib/code-path-analysis/code-path-state.js
  13. 118
      tools/eslint/lib/code-path-analysis/code-path.js
  14. 197
      tools/eslint/lib/code-path-analysis/debug-helpers.js
  15. 258
      tools/eslint/lib/code-path-analysis/fork-context.js
  16. 43
      tools/eslint/lib/code-path-analysis/id-generator.js
  17. 80
      tools/eslint/lib/config.js
  18. 336
      tools/eslint/lib/config/autoconfig.js
  19. 211
      tools/eslint/lib/config/config-file.js
  20. 384
      tools/eslint/lib/config/config-initializer.js
  21. 24
      tools/eslint/lib/config/config-ops.js
  22. 308
      tools/eslint/lib/config/config-rule.js
  23. 91
      tools/eslint/lib/config/config-validator.js
  24. 87
      tools/eslint/lib/config/environments.js
  25. 144
      tools/eslint/lib/config/plugins.js
  26. 197
      tools/eslint/lib/eslint.js
  27. 73
      tools/eslint/lib/file-finder.js
  28. 3
      tools/eslint/lib/formatters/checkstyle.js
  29. 8
      tools/eslint/lib/formatters/html-template-message.html
  30. 113
      tools/eslint/lib/formatters/html-template-page.html
  31. 6
      tools/eslint/lib/formatters/html-template-result.html
  32. 130
      tools/eslint/lib/formatters/html-template.html
  33. 88
      tools/eslint/lib/formatters/html.js
  34. 6
      tools/eslint/lib/formatters/jslint-xml.js
  35. 6
      tools/eslint/lib/formatters/junit.js
  36. 4
      tools/eslint/lib/formatters/stylish.js
  37. 159
      tools/eslint/lib/formatters/table.js
  38. 5
      tools/eslint/lib/formatters/tap.js
  39. 64
      tools/eslint/lib/formatters/visualstudio.js
  40. 239
      tools/eslint/lib/ignored-paths.js
  41. 7
      tools/eslint/lib/load-rules.js
  42. 18
      tools/eslint/lib/options.js
  43. 111
      tools/eslint/lib/rule-context.js
  44. 24
      tools/eslint/lib/rules.js
  45. 135
      tools/eslint/lib/rules/accessor-pairs.js
  46. 360
      tools/eslint/lib/rules/array-bracket-spacing.js
  47. 236
      tools/eslint/lib/rules/array-callback-return.js
  48. 19
      tools/eslint/lib/rules/arrow-body-style.js
  49. 2
      tools/eslint/lib/rules/arrow-spacing.js
  50. 46
      tools/eslint/lib/rules/block-scoped-var.js
  51. 4
      tools/eslint/lib/rules/block-spacing.js
  52. 46
      tools/eslint/lib/rules/brace-style.js
  53. 3
      tools/eslint/lib/rules/callback-return.js
  54. 15
      tools/eslint/lib/rules/camelcase.js
  55. 30
      tools/eslint/lib/rules/comma-dangle.js
  56. 2
      tools/eslint/lib/rules/comma-spacing.js
  57. 5
      tools/eslint/lib/rules/complexity.js
  58. 135
      tools/eslint/lib/rules/consistent-return.js
  59. 68
      tools/eslint/lib/rules/consistent-this.js
  60. 217
      tools/eslint/lib/rules/constructor-super.js
  61. 14
      tools/eslint/lib/rules/curly.js
  62. 3
      tools/eslint/lib/rules/eol-last.js
  63. 22
      tools/eslint/lib/rules/eqeqeq.js
  64. 2
      tools/eslint/lib/rules/func-style.js
  65. 38
      tools/eslint/lib/rules/global-require.js
  66. 110
      tools/eslint/lib/rules/id-blacklist.js
  67. 53
      tools/eslint/lib/rules/indent.js
  68. 14
      tools/eslint/lib/rules/init-declarations.js
  69. 18
      tools/eslint/lib/rules/jsx-quotes.js
  70. 216
      tools/eslint/lib/rules/key-spacing.js
  71. 520
      tools/eslint/lib/rules/keyword-spacing.js
  72. 4
      tools/eslint/lib/rules/lines-around-comment.js
  73. 3
      tools/eslint/lib/rules/max-depth.js
  74. 172
      tools/eslint/lib/rules/max-len.js
  75. 3
      tools/eslint/lib/rules/max-nested-callbacks.js
  76. 3
      tools/eslint/lib/rules/max-params.js
  77. 55
      tools/eslint/lib/rules/max-statements.js
  78. 4
      tools/eslint/lib/rules/new-cap.js
  79. 31
      tools/eslint/lib/rules/newline-after-var.js
  80. 113
      tools/eslint/lib/rules/newline-per-chained-call.js
  81. 29
      tools/eslint/lib/rules/no-alert.js
  82. 88
      tools/eslint/lib/rules/no-arrow-condition.js
  83. 60
      tools/eslint/lib/rules/no-bitwise.js
  84. 2
      tools/eslint/lib/rules/no-class-assign.js
  85. 65
      tools/eslint/lib/rules/no-confusing-arrow.js
  86. 35
      tools/eslint/lib/rules/no-console.js
  87. 2
      tools/eslint/lib/rules/no-const-assign.js
  88. 2
      tools/eslint/lib/rules/no-constant-condition.js
  89. 13
      tools/eslint/lib/rules/no-control-regex.js
  90. 20
      tools/eslint/lib/rules/no-dupe-class-members.js
  91. 149
      tools/eslint/lib/rules/no-empty-function.js
  92. 27
      tools/eslint/lib/rules/no-empty-label.js
  93. 9
      tools/eslint/lib/rules/no-empty.js
  94. 272
      tools/eslint/lib/rules/no-eval.js
  95. 161
      tools/eslint/lib/rules/no-extra-bind.js
  96. 79
      tools/eslint/lib/rules/no-extra-boolean-cast.js
  97. 132
      tools/eslint/lib/rules/no-extra-label.js
  98. 126
      tools/eslint/lib/rules/no-extra-parens.js
  99. 129
      tools/eslint/lib/rules/no-fallthrough.js
  100. 33
      tools/eslint/lib/rules/no-func-assign.js

14
tools/eslint/README.md

@ -1,5 +1,6 @@
[![NPM version][npm-image]][npm-url] [![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url] [![build status][travis-image]][travis-url]
[![Build status][appveyor-image]][appveyor-url]
[![Test coverage][coveralls-image]][coveralls-url] [![Test coverage][coveralls-image]][coveralls-url]
[![Downloads][downloads-image]][downloads-url] [![Downloads][downloads-image]][downloads-url]
[![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282608)](https://www.bountysource.com/trackers/282608-eslint?utm_source=282608&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282608)](https://www.bountysource.com/trackers/282608-eslint?utm_source=282608&utm_medium=shield&utm_campaign=TRACKER_BADGE)
@ -7,7 +8,7 @@
# ESLint # ESLint
[Website](http://eslint.org) | [Configuring](http://eslint.org/docs/user-guide/configuring) | [Rules](http://eslint.org/docs/rules/) | [Contributing](http://eslint.org/docs/developer-guide/contributing) | [Twitter](https://twitter.com/geteslint) | [Mailing List](https://groups.google.com/group/eslint) [Website](http://eslint.org) | [Configuring](http://eslint.org/docs/user-guide/configuring) | [Rules](http://eslint.org/docs/rules/) | [Contributing](http://eslint.org/docs/developer-guide/contributing) | [Reporting Bugs](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) | [Twitter](https://twitter.com/geteslint) | [Mailing List](https://groups.google.com/group/eslint)
ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. In many ways, it is similar to JSLint and JSHint with a few exceptions: ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. In many ways, it is similar to JSLint and JSHint with a few exceptions:
@ -55,6 +56,7 @@ The three error levels allow you fine-grained control over how ESLint applies ru
## Sponsors ## Sponsors
* Development is sponsored by [Box](https://box.com) * Development is sponsored by [Box](https://box.com)
* Site search ([eslint.org](http://eslint.org)) is sponsored by [Algolia](https://www.algolia.com)
## Team ## Team
@ -75,6 +77,14 @@ These folks keep the project moving and are resources for help:
We have scheduled releases every two weeks on Friday or Saturday. We have scheduled releases every two weeks on Friday or Saturday.
## Filing Issues
Before filing an issue, please be sure to read the guidelines for what you're reporting:
* [Bug Report](http://eslint.org/docs/developer-guide/contributing/reporting-bugs)
* [Propose a New Rule](http://eslint.org/docs/developer-guide/contributing/new-rules)
* [Request a Change](http://eslint.org/docs/developer-guide/contributing/changes)
## Frequently Asked Questions ## Frequently Asked Questions
### Why don't you like JSHint??? ### Why don't you like JSHint???
@ -120,6 +130,8 @@ Join our [Mailing List](https://groups.google.com/group/eslint) or [Chatroom](ht
[npm-url]: https://www.npmjs.com/package/eslint [npm-url]: https://www.npmjs.com/package/eslint
[travis-image]: https://img.shields.io/travis/eslint/eslint/master.svg?style=flat-square [travis-image]: https://img.shields.io/travis/eslint/eslint/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/eslint/eslint [travis-url]: https://travis-ci.org/eslint/eslint
[appveyor-image]: https://ci.appveyor.com/api/projects/status/iwxmiobcvbw3b0av/branch/master?svg=true
[appveyor-url]: https://ci.appveyor.com/project/nzakas/eslint/branch/master
[coveralls-image]: https://img.shields.io/coveralls/eslint/eslint/master.svg?style=flat-square [coveralls-image]: https://img.shields.io/coveralls/eslint/eslint/master.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/eslint/eslint?branch=master [coveralls-url]: https://coveralls.io/r/eslint/eslint?branch=master
[downloads-image]: https://img.shields.io/npm/dm/eslint.svg?style=flat-square [downloads-image]: https://img.shields.io/npm/dm/eslint.svg?style=flat-square

22
tools/eslint/bin/eslint.js

@ -20,7 +20,7 @@ var exitCode = 0,
// must do this initialization *before* other requires in order to work // must do this initialization *before* other requires in order to work
if (debug) { if (debug) {
require("debug").enable("eslint:*"); require("debug").enable("eslint:*,-eslint:code-path");
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -60,10 +60,16 @@ if (useStdIn) {
exitCode = cli.execute(process.argv); exitCode = cli.execute(process.argv);
} }
/* // https://github.com/eslint/eslint/issues/4691
* Wait for the stdout buffer to drain. // In Node.js >= 0.12, you can use a cleaner way
* See https://github.com/eslint/eslint/issues/317 if ("exitCode" in process) {
*/ process.exitCode = exitCode;
process.on("exit", function() { } else {
process.exit(exitCode); /*
}); * Wait for the stdout buffer to drain.
* See https://github.com/eslint/eslint/issues/317
*/
process.on("exit", function() {
process.exit(exitCode);
});
}

33
tools/eslint/conf/cli-options.js

@ -0,0 +1,33 @@
/**
* @fileoverview Default CLIEngineOptions.
* @author Ian VanSchooten
* @copyright 2016 Ian VanSchooten. All rights reserved.
* See LICENSE in root directory for full license.
*/
"use strict";
var DEFAULT_PARSER = require("../conf/eslint.json").parser;
module.exports = {
configFile: null,
baseConfig: false,
rulePaths: [],
useEslintrc: true,
envs: [],
globals: [],
rules: {},
extensions: [".js"],
ignore: true,
ignorePath: null,
parser: DEFAULT_PARSER,
cache: false,
// in order to honor the cacheFile option if specified
// this option should not have a default value otherwise
// it will always be used
cacheLocation: "",
cacheFile: ".eslintcache",
fix: false,
allowInlineConfig: true,
cwd: process.cwd()
};

48
tools/eslint/conf/environments.js

@ -16,22 +16,29 @@ var globals = require("globals");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = { module.exports = {
builtin: globals.builtin, builtin: globals.es5,
browser: { browser: {
globals: globals.browser globals: globals.browser
}, },
node: { node: {
globals: globals.node, globals: globals.node,
ecmaFeatures: { parserOptions: {
globalReturn: true ecmaFeatures: {
globalReturn: true
}
} }
}, },
commonjs: { commonjs: {
globals: globals.commonjs, globals: globals.commonjs,
ecmaFeatures: { parserOptions: {
globalReturn: true ecmaFeatures: {
globalReturn: true
}
} }
}, },
"shared-node-browser": {
globals: globals["shared-node-browser"]
},
worker: { worker: {
globals: globals.worker globals: globals.worker
}, },
@ -80,6 +87,9 @@ module.exports = {
serviceworker: { serviceworker: {
globals: globals.serviceworker globals: globals.serviceworker
}, },
atomtest: {
globals: globals.atomtest
},
embertest: { embertest: {
globals: globals.embertest globals: globals.embertest
}, },
@ -87,28 +97,12 @@ module.exports = {
globals: globals.webextensions globals: globals.webextensions
}, },
es6: { es6: {
ecmaFeatures: { globals: globals.es6,
arrowFunctions: true, parserOptions: {
blockBindings: true, ecmaVersion: 6
regexUFlag: true,
regexYFlag: true,
templateStrings: true,
binaryLiterals: true,
octalLiterals: true,
unicodeCodePointEscapes: true,
superInFunctions: true,
defaultParams: true,
restParams: true,
forOf: true,
objectLiteralComputedProperties: true,
objectLiteralShorthandMethods: true,
objectLiteralShorthandProperties: true,
objectLiteralDuplicateProperties: true,
generators: true,
destructuring: true,
classes: true,
spread: true,
newTarget: true
} }
},
greasemonkey: {
globals: globals.greasemonkey
} }
}; };

107
tools/eslint/conf/eslint.json

@ -4,45 +4,47 @@
"rules": { "rules": {
"no-alert": 0, "no-alert": 0,
"no-array-constructor": 0, "no-array-constructor": 0,
"no-arrow-condition": 0,
"no-bitwise": 0, "no-bitwise": 0,
"no-caller": 0, "no-caller": 0,
"no-case-declarations": 0, "no-case-declarations": 2,
"no-catch-shadow": 0, "no-catch-shadow": 0,
"no-class-assign": 0, "no-class-assign": 2,
"no-cond-assign": 2, "no-cond-assign": 2,
"no-confusing-arrow": 0,
"no-console": 2, "no-console": 2,
"no-const-assign": 0, "no-const-assign": 2,
"no-constant-condition": 2, "no-constant-condition": 2,
"no-continue": 0, "no-continue": 0,
"no-control-regex": 2, "no-control-regex": 2,
"no-debugger": 2, "no-debugger": 2,
"no-delete-var": 2, "no-delete-var": 2,
"no-div-regex": 0, "no-div-regex": 0,
"no-dupe-class-members": 0, "no-dupe-class-members": 2,
"no-dupe-keys": 2, "no-dupe-keys": 2,
"no-dupe-args": 2, "no-dupe-args": 2,
"no-duplicate-case": 2, "no-duplicate-case": 2,
"no-else-return": 0, "no-else-return": 0,
"no-empty": 2, "no-empty": 2,
"no-empty-character-class": 2, "no-empty-character-class": 2,
"no-empty-label": 0, "no-empty-function": 0,
"no-empty-pattern": 0, "no-empty-pattern": 2,
"no-eq-null": 0, "no-eq-null": 0,
"no-eval": 0, "no-eval": 0,
"no-ex-assign": 2, "no-ex-assign": 2,
"no-extend-native": 0, "no-extend-native": 0,
"no-extra-bind": 0, "no-extra-bind": 0,
"no-extra-boolean-cast": 2, "no-extra-boolean-cast": 2,
"no-extra-label": 0,
"no-extra-parens": 0, "no-extra-parens": 0,
"no-extra-semi": 2, "no-extra-semi": 2,
"no-fallthrough": 2, "no-fallthrough": 2,
"no-floating-decimal": 0, "no-floating-decimal": 0,
"no-func-assign": 2, "no-func-assign": 2,
"no-implicit-coercion": 0, "no-implicit-coercion": 0,
"no-implicit-globals": 0,
"no-implied-eval": 0, "no-implied-eval": 0,
"no-inline-comments": 0, "no-inline-comments": 0,
"no-inner-declarations": [2, "functions"], "no-inner-declarations": 2,
"no-invalid-regexp": 2, "no-invalid-regexp": 2,
"no-invalid-this": 0, "no-invalid-this": 0,
"no-irregular-whitespace": 2, "no-irregular-whitespace": 2,
@ -52,12 +54,12 @@
"no-lone-blocks": 0, "no-lone-blocks": 0,
"no-lonely-if": 0, "no-lonely-if": 0,
"no-loop-func": 0, "no-loop-func": 0,
"no-mixed-requires": [0, false], "no-mixed-requires": 0,
"no-mixed-spaces-and-tabs": [2, false], "no-mixed-spaces-and-tabs": 2,
"linebreak-style": [0, "unix"], "linebreak-style": 0,
"no-multi-spaces": 0, "no-multi-spaces": 0,
"no-multi-str": 0, "no-multi-str": 0,
"no-multiple-empty-lines": [0, {"max": 2}], "no-multiple-empty-lines": 0,
"no-native-reassign": 0, "no-native-reassign": 0,
"no-negated-condition": 0, "no-negated-condition": 0,
"no-negated-in-lhs": 2, "no-negated-in-lhs": 2,
@ -66,6 +68,7 @@
"no-new-func": 0, "no-new-func": 0,
"no-new-object": 0, "no-new-object": 0,
"no-new-require": 0, "no-new-require": 0,
"no-new-symbol": 2,
"no-new-wrappers": 0, "no-new-wrappers": 0,
"no-obj-calls": 2, "no-obj-calls": 2,
"no-octal": 2, "no-octal": 2,
@ -78,65 +81,72 @@
"no-proto": 0, "no-proto": 0,
"no-redeclare": 2, "no-redeclare": 2,
"no-regex-spaces": 2, "no-regex-spaces": 2,
"no-restricted-imports": 0,
"no-restricted-modules": 0, "no-restricted-modules": 0,
"no-restricted-syntax": 0, "no-restricted-syntax": 0,
"no-return-assign": 0, "no-return-assign": 0,
"no-script-url": 0, "no-script-url": 0,
"no-self-assign": 2,
"no-self-compare": 0, "no-self-compare": 0,
"no-sequences": 0, "no-sequences": 0,
"no-shadow": 0, "no-shadow": 0,
"no-shadow-restricted-names": 0, "no-shadow-restricted-names": 0,
"no-whitespace-before-property": 0,
"no-spaced-func": 0, "no-spaced-func": 0,
"no-sparse-arrays": 2, "no-sparse-arrays": 2,
"no-sync": 0, "no-sync": 0,
"no-ternary": 0, "no-ternary": 0,
"no-trailing-spaces": 0, "no-trailing-spaces": 0,
"no-this-before-super": 0, "no-this-before-super": 2,
"no-throw-literal": 0, "no-throw-literal": 0,
"no-undef": 2, "no-undef": 2,
"no-undef-init": 0, "no-undef-init": 0,
"no-undefined": 0, "no-undefined": 0,
"no-unexpected-multiline": 0, "no-unexpected-multiline": 2,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"no-unmodified-loop-condition": 0,
"no-unneeded-ternary": 0, "no-unneeded-ternary": 0,
"no-unreachable": 2, "no-unreachable": 2,
"no-unused-expressions": 0, "no-unused-expressions": 0,
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}], "no-unused-labels": 2,
"no-unused-vars": 2,
"no-use-before-define": 0, "no-use-before-define": 0,
"no-useless-call": 0, "no-useless-call": 0,
"no-useless-concat": 0, "no-useless-concat": 0,
"no-useless-constructor": 0,
"no-void": 0, "no-void": 0,
"no-var": 0, "no-var": 0,
"no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], "no-warning-comments": 0,
"no-with": 0, "no-with": 0,
"no-magic-numbers": 0, "no-magic-numbers": 0,
"array-bracket-spacing": [0, "never"], "array-bracket-spacing": 0,
"arrow-body-style": [0, "as-needed"], "array-callback-return": 0,
"arrow-body-style": 0,
"arrow-parens": 0, "arrow-parens": 0,
"arrow-spacing": 0, "arrow-spacing": 0,
"accessor-pairs": 0, "accessor-pairs": 0,
"block-scoped-var": 0, "block-scoped-var": 0,
"block-spacing": 0, "block-spacing": 0,
"brace-style": [0, "1tbs"], "brace-style": 0,
"callback-return": 0, "callback-return": 0,
"camelcase": 0, "camelcase": 0,
"comma-dangle": [2, "never"], "comma-dangle": 2,
"comma-spacing": 0, "comma-spacing": 0,
"comma-style": 0, "comma-style": 0,
"complexity": [0, 11], "complexity": [0, 11],
"computed-property-spacing": [0, "never"], "computed-property-spacing": 0,
"consistent-return": 0, "consistent-return": 0,
"consistent-this": [0, "that"], "consistent-this": 0,
"constructor-super": 0, "constructor-super": 2,
"curly": [0, "all"], "curly": 0,
"default-case": 0, "default-case": 0,
"dot-location": 0, "dot-location": 0,
"dot-notation": [0, { "allowKeywords": true }], "dot-notation": 0,
"eol-last": 0, "eol-last": 0,
"eqeqeq": 0, "eqeqeq": 0,
"func-names": 0, "func-names": 0,
"func-style": [0, "declaration"], "func-style": 0,
"generator-star-spacing": 0, "generator-star-spacing": 0,
"global-require": 0, "global-require": 0,
"guard-for-in": 0, "guard-for-in": 0,
@ -144,53 +154,58 @@
"id-length": 0, "id-length": 0,
"indent": 0, "indent": 0,
"init-declarations": 0, "init-declarations": 0,
"jsx-quotes": [0, "prefer-double"], "jsx-quotes": 0,
"key-spacing": [0, { "beforeColon": false, "afterColon": true }], "key-spacing": 0,
"keyword-spacing": 0,
"lines-around-comment": 0, "lines-around-comment": 0,
"max-depth": [0, 4], "max-depth": 0,
"max-len": [0, 80, 4], "max-len": 0,
"max-nested-callbacks": [0, 2], "max-nested-callbacks": 0,
"max-params": [0, 3], "max-params": 0,
"max-statements": [0, 10], "max-statements": 0,
"new-cap": 0, "new-cap": 0,
"new-parens": 0, "new-parens": 0,
"newline-after-var": 0, "newline-after-var": 0,
"newline-per-chained-call": 0,
"object-curly-spacing": [0, "never"], "object-curly-spacing": [0, "never"],
"object-shorthand": 0, "object-shorthand": 0,
"one-var": [0, "always"], "one-var": 0,
"operator-assignment": [0, "always"], "one-var-declaration-per-line": 0,
"operator-assignment": 0,
"operator-linebreak": 0, "operator-linebreak": 0,
"padded-blocks": 0, "padded-blocks": 0,
"prefer-arrow-callback": 0, "prefer-arrow-callback": 0,
"prefer-const": 0, "prefer-const": 0,
"prefer-spread": 0,
"prefer-reflect": 0, "prefer-reflect": 0,
"prefer-rest-params": 0,
"prefer-spread": 0,
"prefer-template": 0, "prefer-template": 0,
"quote-props": 0, "quote-props": 0,
"quotes": [0, "double"], "quotes": 0,
"radix": 0, "radix": 0,
"id-match": 0, "id-match": 0,
"id-blacklist": 0,
"require-jsdoc": 0, "require-jsdoc": 0,
"require-yield": 0, "require-yield": 0,
"semi": 0, "semi": 0,
"semi-spacing": [0, {"before": false, "after": true}], "semi-spacing": 0,
"sort-vars": 0, "sort-vars": 0,
"space-after-keywords": [0, "always"], "sort-imports": 0,
"space-before-keywords": [0, "always"], "space-before-blocks": 0,
"space-before-blocks": [0, "always"], "space-before-function-paren": 0,
"space-before-function-paren": [0, "always"], "space-in-parens": 0,
"space-in-parens": [0, "never"],
"space-infix-ops": 0, "space-infix-ops": 0,
"space-return-throw-case": 0, "space-unary-ops": 0,
"space-unary-ops": [0, { "words": true, "nonwords": false }],
"spaced-comment": 0, "spaced-comment": 0,
"strict": 0, "strict": 0,
"template-curly-spacing": 0,
"use-isnan": 2, "use-isnan": 2,
"valid-jsdoc": 0, "valid-jsdoc": 0,
"valid-typeof": 2, "valid-typeof": 2,
"vars-on-top": 0, "vars-on-top": 0,
"wrap-iife": 0, "wrap-iife": 0,
"wrap-regex": 0, "wrap-regex": 0,
"yoda": [0, "never"] "yield-star-spacing": 0,
"yoda": 0
} }
} }

5
tools/eslint/conf/replacements.json

@ -2,15 +2,20 @@
"rules": { "rules": {
"generator-star": ["generator-star-spacing"], "generator-star": ["generator-star-spacing"],
"global-strict": ["strict"], "global-strict": ["strict"],
"no-arrow-condition": ["no-confusing-arrow", "no-constant-condition"],
"no-comma-dangle": ["comma-dangle"], "no-comma-dangle": ["comma-dangle"],
"no-empty-class": ["no-empty-character-class"], "no-empty-class": ["no-empty-character-class"],
"no-empty-label": ["no-labels"],
"no-extra-strict": ["strict"], "no-extra-strict": ["strict"],
"no-reserved-keys": ["quote-props"], "no-reserved-keys": ["quote-props"],
"no-space-before-semi": ["semi-spacing"], "no-space-before-semi": ["semi-spacing"],
"no-wrap-func": ["no-extra-parens"], "no-wrap-func": ["no-extra-parens"],
"space-after-function-name": ["space-before-function-paren"], "space-after-function-name": ["space-before-function-paren"],
"space-after-keywords": ["keyword-spacing"],
"space-before-function-parentheses": ["space-before-function-paren"], "space-before-function-parentheses": ["space-before-function-paren"],
"space-before-keywords": ["keyword-spacing"],
"space-in-brackets": ["object-curly-spacing", "array-bracket-spacing", "computed-property-spacing"], "space-in-brackets": ["object-curly-spacing", "array-bracket-spacing", "computed-property-spacing"],
"space-return-throw-case": ["keyword-spacing"],
"space-unary-word-ops": ["space-unary-ops"], "space-unary-word-ops": ["space-unary-ops"],
"spaced-line-comment": ["spaced-comment"] "spaced-line-comment": ["spaced-comment"]
} }

305
tools/eslint/lib/ast-utils.js

@ -17,6 +17,13 @@ var esutils = require("esutils");
// Helpers // Helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var anyFunctionPattern = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression)$/;
var arrayOrTypedArrayPattern = /Array$/;
var arrayMethodPattern = /^(?:every|filter|find|findIndex|forEach|map|some)$/;
var bindOrCallOrApplyPattern = /^(?:bind|call|apply)$/;
var breakableTypePattern = /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/;
var thisTagPattern = /^[\s\*]*@this/m;
/** /**
* Checks reference if is non initializer and writable. * Checks reference if is non initializer and writable.
* @param {Reference} reference - A reference to check. * @param {Reference} reference - A reference to check.
@ -37,6 +44,130 @@ function isModifyingReference(reference, index, references) {
); );
} }
/**
* Checks whether or not a node is a constructor.
* @param {ASTNode} node - A function node to check.
* @returns {boolean} Wehether or not a node is a constructor.
*/
function isES5Constructor(node) {
return (
node.id &&
node.id.name[0] === node.id.name[0].toLocaleUpperCase()
);
}
/**
* Finds a function node from ancestors of a node.
* @param {ASTNode} node - A start node to find.
* @returns {Node|null} A found function node.
*/
function getUpperFunction(node) {
while (node) {
if (anyFunctionPattern.test(node.type)) {
return node;
}
node = node.parent;
}
return null;
}
/**
* Checks whether or not a node is `null` or `undefined`.
* @param {ASTNode} node - A node to check.
* @returns {boolean} Whether or not the node is a `null` or `undefined`.
* @public
*/
function isNullOrUndefined(node) {
return (
(node.type === "Literal" && node.value === null) ||
(node.type === "Identifier" && node.name === "undefined") ||
(node.type === "UnaryExpression" && node.operator === "void")
);
}
/**
* Checks whether or not a node is callee.
* @param {ASTNode} node - A node to check.
* @returns {boolean} Whether or not the node is callee.
*/
function isCallee(node) {
return node.parent.type === "CallExpression" && node.parent.callee === node;
}
/**
* Checks whether or not a node is `Reclect.apply`.
* @param {ASTNode} node - A node to check.
* @returns {boolean} Whether or not the node is a `Reclect.apply`.
*/
function isReflectApply(node) {
return (
node.type === "MemberExpression" &&
node.object.type === "Identifier" &&
node.object.name === "Reflect" &&
node.property.type === "Identifier" &&
node.property.name === "apply" &&
node.computed === false
);
}
/**
* Checks whether or not a node is `Array.from`.
* @param {ASTNode} node - A node to check.
* @returns {boolean} Whether or not the node is a `Array.from`.
*/
function isArrayFromMethod(node) {
return (
node.type === "MemberExpression" &&
node.object.type === "Identifier" &&
arrayOrTypedArrayPattern.test(node.object.name) &&
node.property.type === "Identifier" &&
node.property.name === "from" &&
node.computed === false
);
}
/**
* Checks whether or not a node is a method which has `thisArg`.
* @param {ASTNode} node - A node to check.
* @returns {boolean} Whether or not the node is a method which has `thisArg`.
*/
function isMethodWhichHasThisArg(node) {
while (node) {
if (node.type === "Identifier") {
return arrayMethodPattern.test(node.name);
}
if (node.type === "MemberExpression" && !node.computed) {
node = node.property;
continue;
}
break;
}
return false;
}
/**
* Checks whether or not a node has a `@this` tag in its comments.
* @param {ASTNode} node - A node to check.
* @param {SourceCode} sourceCode - A SourceCode instance to get comments.
* @returns {boolean} Whether or not the node has a `@this` tag in its comments.
*/
function hasJSDocThisTag(node, sourceCode) {
var jsdocComment = sourceCode.getJSDocComment(node);
if (jsdocComment && thisTagPattern.test(jsdocComment.value)) {
return true;
}
// Checks `@this` in its leading comments for callbacks,
// because callbacks don't have its JSDoc comment.
// e.g.
// sinon.test(/* @this sinon.Sandbox */function() { this.spy(); });
return sourceCode.getComments(node).leading.some(function(comment) {
return thisTagPattern.test(comment.value);
});
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -54,19 +185,10 @@ module.exports = {
return left.loc.end.line === right.loc.start.line; return left.loc.end.line === right.loc.start.line;
}, },
/** isNullOrUndefined: isNullOrUndefined,
* Checks whether or not a node is `null` or `undefined`. isCallee: isCallee,
* @param {ASTNode} node - A node to check. getUpperFunction: getUpperFunction,
* @returns {boolean} Whether or not the node is a `null` or `undefined`. isArrayFromMethod: isArrayFromMethod,
* @public
*/
isNullOrUndefined: function(node) {
return (
(node.type === "Literal" && node.value === null) ||
(node.type === "Identifier" && node.name === "undefined") ||
(node.type === "UnaryExpression" && node.operator === "void")
);
},
/** /**
* Checks whether or not a given node is a string literal. * Checks whether or not a given node is a string literal.
@ -80,6 +202,37 @@ module.exports = {
); );
}, },
/**
* Checks whether a given node is a breakable statement or not.
* The node is breakable if the node is one of the following type:
*
* - DoWhileStatement
* - ForInStatement
* - ForOfStatement
* - ForStatement
* - SwitchStatement
* - WhileStatement
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is breakable.
*/
isBreakableStatement: function(node) {
return breakableTypePattern.test(node.type);
},
/**
* Gets the label if the parent node of a given node is a LabeledStatement.
*
* @param {ASTNode} node - A node to get.
* @returns {string|null} The label or `null`.
*/
getLabel: function(node) {
if (node.parent.type === "LabeledStatement") {
return node.parent.label.name;
}
return null;
},
/** /**
* Gets references which are non initializer and writable. * Gets references which are non initializer and writable.
* @param {Reference[]} references - An array of references. * @param {Reference[]} references - An array of references.
@ -150,5 +303,131 @@ module.exports = {
} }
return null; return null;
},
/**
* Checks whether or not a given function node is the default `this` binding.
*
* First, this checks the node:
*
* - The function name does not start with uppercase (it's a constructor).
* - The function does not have a JSDoc comment that has a @this tag.
*
* Next, this checks the location of the node.
* If the location is below, this judges `this` is valid.
*
* - The location is not on an object literal.
* - The location does not assign to a property.
* - The location is not on an ES2015 class.
* - The location does not call its `bind`/`call`/`apply` method directly.
* - The function is not a callback of array methods (such as `.forEach()`) if `thisArg` is given.
*
* @param {ASTNode} node - A function node to check.
* @param {SourceCode} sourceCode - A SourceCode instance to get comments.
* @returns {boolean} The function node is the default `this` binding.
*/
isDefaultThisBinding: function(node, sourceCode) {
if (isES5Constructor(node) || hasJSDocThisTag(node, sourceCode)) {
return false;
}
while (node) {
var parent = node.parent;
switch (parent.type) {
// Looks up the destination.
// e.g.
// obj.foo = nativeFoo || function foo() { ... };
case "LogicalExpression":
case "ConditionalExpression":
node = parent;
break;
// If the upper function is IIFE, checks the destination of the return value.
// e.g.
// obj.foo = (function() {
// // setup...
// return function foo() { ... };
// })();
case "ReturnStatement":
var func = getUpperFunction(parent);
if (func === null || !isCallee(func)) {
return true;
}
node = func.parent;
break;
// e.g.
// var obj = { foo() { ... } };
// var obj = { foo: function() { ... } };
case "Property":
return false;
// e.g.
// obj.foo = foo() { ... };
case "AssignmentExpression":
return (
parent.right !== node ||
parent.left.type !== "MemberExpression"
);
// e.g.
// class A { constructor() { ... } }
// class A { foo() { ... } }
// class A { get foo() { ... } }
// class A { set foo() { ... } }
// class A { static foo() { ... } }
case "MethodDefinition":
return false;
// e.g.
// var foo = function foo() { ... }.bind(obj);
// (function foo() { ... }).call(obj);
// (function foo() { ... }).apply(obj, []);
case "MemberExpression":
return (
parent.object !== node ||
parent.property.type !== "Identifier" ||
!bindOrCallOrApplyPattern.test(parent.property.name) ||
!isCallee(parent) ||
parent.parent.arguments.length === 0 ||
isNullOrUndefined(parent.parent.arguments[0])
);
// e.g.
// Reflect.apply(function() {}, obj, []);
// Array.from([], function() {}, obj);
// list.forEach(function() {}, obj);
case "CallExpression":
if (isReflectApply(parent.callee)) {
return (
parent.arguments.length !== 3 ||
parent.arguments[0] !== node ||
isNullOrUndefined(parent.arguments[1])
);
}
if (isArrayFromMethod(parent.callee)) {
return (
parent.arguments.length !== 3 ||
parent.arguments[1] !== node ||
isNullOrUndefined(parent.arguments[2])
);
}
if (isMethodWhichHasThisArg(parent.callee)) {
return (
parent.arguments.length !== 2 ||
parent.arguments[0] !== node ||
isNullOrUndefined(parent.arguments[1])
);
}
return true;
// Otherwise `this` is default.
default:
return true;
}
}
/* istanbul ignore next */
return true;
} }
}; };

163
tools/eslint/lib/cli-engine.js

@ -20,15 +20,17 @@
var fs = require("fs"), var fs = require("fs"),
path = require("path"), path = require("path"),
assign = require("object-assign"), lodash = require("lodash"),
debug = require("debug"), debug = require("debug"),
glob = require("glob"),
shell = require("shelljs"), shell = require("shelljs"),
rules = require("./rules"), rules = require("./rules"),
eslint = require("./eslint"), eslint = require("./eslint"),
defaultOptions = require("../conf/cli-options"),
IgnoredPaths = require("./ignored-paths"), IgnoredPaths = require("./ignored-paths"),
Config = require("./config"), Config = require("./config"),
util = require("./util"), Plugins = require("./config/plugins"),
fileEntryCache = require("file-entry-cache"), fileEntryCache = require("file-entry-cache"),
globUtil = require("./util/glob-util"), globUtil = require("./util/glob-util"),
SourceCodeFixer = require("./util/source-code-fixer"), SourceCodeFixer = require("./util/source-code-fixer"),
@ -38,7 +40,6 @@ var fs = require("fs"),
crypto = require( "crypto" ), crypto = require( "crypto" ),
pkg = require("../package.json"); pkg = require("../package.json");
var DEFAULT_PARSER = require("../conf/eslint.json").parser;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Typedefs // Typedefs
@ -76,29 +77,7 @@ var DEFAULT_PARSER = require("../conf/eslint.json").parser;
// Private // Private
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
defaultOptions = lodash.assign({}, defaultOptions, {cwd: process.cwd()});
var defaultOptions = {
configFile: null,
baseConfig: false,
rulePaths: [],
useEslintrc: true,
envs: [],
globals: [],
rules: {},
extensions: [".js"],
ignore: true,
ignorePath: null,
parser: DEFAULT_PARSER,
cache: false,
// in order to honor the cacheFile option if specified
// this option should not have a default value otherwise
// it will always be used
cacheLocation: "",
cacheFile: ".eslintcache",
fix: false,
allowInlineConfig: true
},
loadedPlugins = Object.create(null);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -106,34 +85,6 @@ var defaultOptions = {
debug = debug("eslint:cli-engine"); debug = debug("eslint:cli-engine");
/**
* Load the given plugins if they are not loaded already.
* @param {string[]} pluginNames An array of plugin names which should be loaded.
* @returns {void}
*/
function loadPlugins(pluginNames) {
if (pluginNames) {
pluginNames.forEach(function(pluginName) {
var pluginNamespace = util.getNamespace(pluginName),
pluginNameWithoutNamespace = util.removeNameSpace(pluginName),
pluginNameWithoutPrefix = util.removePluginPrefix(pluginNameWithoutNamespace),
plugin;
if (!loadedPlugins[pluginNameWithoutPrefix]) {
debug("Load plugin " + pluginNameWithoutPrefix);
plugin = require(pluginNamespace + util.PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix);
// if this plugin has rules, import them
if (plugin.rules) {
rules.import(plugin.rules, pluginNameWithoutPrefix);
}
loadedPlugins[pluginNameWithoutPrefix] = plugin;
}
});
}
}
/** /**
* It will calculate the error and warning count for collection of messages per file * It will calculate the error and warning count for collection of messages per file
* @param {Object[]} messages - Collection of messages * @param {Object[]} messages - Collection of messages
@ -192,6 +143,7 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
stats, stats,
fileExtension = path.extname(filename), fileExtension = path.extname(filename),
processor, processor,
loadedPlugins,
fixedResult; fixedResult;
if (filename) { if (filename) {
@ -201,7 +153,12 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
filename = filename || "<text>"; filename = filename || "<text>";
debug("Linting " + filename); debug("Linting " + filename);
config = configHelper.getConfig(filePath); config = configHelper.getConfig(filePath);
loadPlugins(config.plugins);
if (config.plugins) {
Plugins.loadAll(config.plugins);
}
loadedPlugins = Plugins.getAll();
for (var plugin in loadedPlugins) { for (var plugin in loadedPlugins) {
if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) { if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
@ -286,7 +243,7 @@ function createIgnoreResult(filePath) {
{ {
fatal: false, fatal: false,
severity: 1, severity: 1,
message: "File ignored because of your .eslintignore file. Use --no-ignore to override." message: "File ignored because of a matching ignore pattern. Use --no-ignore to override."
} }
], ],
errorCount: 0, errorCount: 0,
@ -325,14 +282,15 @@ function md5Hash(str) {
* if cacheFile points to a file or looks like a file then in will just use that file * if cacheFile points to a file or looks like a file then in will just use that file
* *
* @param {string} cacheFile The name of file to be used to store the cache * @param {string} cacheFile The name of file to be used to store the cache
* @param {string} cwd Current working directory
* @returns {string} the resolved path to the cache file * @returns {string} the resolved path to the cache file
*/ */
function getCacheFile(cacheFile) { function getCacheFile(cacheFile, cwd) {
// make sure the path separators are normalized for the environment/os // make sure the path separators are normalized for the environment/os
// keeping the trailing path separator if present // keeping the trailing path separator if present
cacheFile = path.normalize(cacheFile); cacheFile = path.normalize(cacheFile);
var resolvedCacheFile = path.resolve(cacheFile); var resolvedCacheFile = path.resolve(cwd, cacheFile);
var looksLikeADirectory = cacheFile[cacheFile.length - 1 ] === path.sep; var looksLikeADirectory = cacheFile[cacheFile.length - 1 ] === path.sep;
/** /**
@ -340,7 +298,7 @@ function getCacheFile(cacheFile) {
* @returns {string} the resolved path to the cacheFile * @returns {string} the resolved path to the cacheFile
*/ */
function getCacheFileForDirectory() { function getCacheFileForDirectory() {
return path.join(resolvedCacheFile, ".cache_" + md5Hash(process.cwd())); return path.join(resolvedCacheFile, ".cache_" + md5Hash(cwd));
} }
var fileStats; var fileStats;
@ -389,31 +347,33 @@ function getCacheFile(cacheFile) {
*/ */
function CLIEngine(options) { function CLIEngine(options) {
options = lodash.assign(Object.create(null), defaultOptions, options);
/** /**
* Stored options for this instance * Stored options for this instance
* @type {Object} * @type {Object}
*/ */
this.options = assign(Object.create(defaultOptions), options || {}); this.options = options;
var cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile); var cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
/** /**
* cache used to not operate on files that haven't changed since last successful * cache used to not operate on files that haven't changed since last successful
* execution (e.g. file passed with no errors and no warnings * execution (e.g. file passed with no errors and no warnings
* @type {Object} * @type {Object}
*/ */
this._fileCache = fileEntryCache.create(cacheFile); // eslint-disable-line no-underscore-dangle this._fileCache = fileEntryCache.create(cacheFile);
if (!this.options.cache) { if (!this.options.cache) {
this._fileCache.destroy(); // eslint-disable-line no-underscore-dangle this._fileCache.destroy();
} }
// load in additional rules // load in additional rules
if (this.options.rulePaths) { if (this.options.rulePaths) {
var cwd = this.options.cwd;
this.options.rulePaths.forEach(function(rulesdir) { this.options.rulePaths.forEach(function(rulesdir) {
debug("Loading rules from " + rulesdir); debug("Loading rules from " + rulesdir);
rules.load(rulesdir); rules.load(rulesdir, cwd);
}); });
} }
@ -444,7 +404,7 @@ CLIEngine.getFormatter = function(format) {
// if there's a slash, then it's a file // if there's a slash, then it's a file
if (format.indexOf("/") > -1) { if (format.indexOf("/") > -1) {
formatterPath = path.resolve(process.cwd(), format); formatterPath = path.resolve(this.options.cwd, format);
} else { } else {
formatterPath = "./formatters/" + format; formatterPath = "./formatters/" + format;
} }
@ -506,11 +466,7 @@ CLIEngine.prototype = {
* @returns {void} * @returns {void}
*/ */
addPlugin: function(name, pluginobject) { addPlugin: function(name, pluginobject) {
var pluginNameWithoutPrefix = util.removePluginPrefix(util.removeNameSpace(name)); Plugins.define(name, pluginobject);
if (pluginobject.rules) {
rules.import(pluginobject.rules, pluginNameWithoutPrefix);
}
loadedPlugins[pluginNameWithoutPrefix] = pluginobject;
}, },
/** /**
@ -532,15 +488,27 @@ CLIEngine.prototype = {
var results = [], var results = [],
processed = {}, processed = {},
options = this.options, options = this.options,
fileCache = this._fileCache, // eslint-disable-line no-underscore-dangle fileCache = this._fileCache,
configHelper = new Config(options), configHelper = new Config(options),
ignoredPaths = new IgnoredPaths(options),
globOptions,
stats, stats,
startTime, startTime,
prevConfig; // the previous configuration used prevConfig; // the previous configuration used
startTime = Date.now(); startTime = Date.now();
patterns = this.resolveFileGlobPatterns(patterns); globOptions = {
nodir: true
};
var cwd = options.cwd || process.cwd;
patterns = this.resolveFileGlobPatterns(patterns.map(function(pattern) {
if (pattern.indexOf("/") > 0) {
return path.join(cwd, pattern);
}
return pattern;
}));
/** /**
* Calculates the hash of the config file used to validate a given file * Calculates the hash of the config file used to validate a given file
@ -572,11 +540,28 @@ CLIEngine.prototype = {
* Executes the linter on a file defined by the `filename`. Skips * Executes the linter on a file defined by the `filename`. Skips
* unsupported file extensions and any files that are already linted. * unsupported file extensions and any files that are already linted.
* @param {string} filename The resolved filename of the file to be linted * @param {string} filename The resolved filename of the file to be linted
* @param {boolean} warnIgnored always warn when a file is ignored
* @returns {void} * @returns {void}
*/ */
function executeOnFile(filename) { function executeOnFile(filename, warnIgnored) {
var hashOfConfig; var hashOfConfig;
if (options.ignore !== false) {
if (ignoredPaths.contains(filename, "custom")) {
if (warnIgnored) {
results.push(createIgnoreResult(filename));
}
return;
}
if (ignoredPaths.contains(filename, "default")) {
return;
}
}
filename = path.resolve(filename);
if (processed[filename]) { if (processed[filename]) {
return; return;
} }
@ -636,18 +621,19 @@ CLIEngine.prototype = {
results.push(res); results.push(res);
} }
// Lint each desired file patterns.forEach(function(pattern) {
globUtil.listFilesToProcess(patterns, options).forEach(executeOnFile);
// only warn for files explicitly passed on the command line var file = path.resolve(pattern);
if (options.ignore) {
patterns.forEach(function(file) { if (shell.test("-f", file)) {
var fullPath = path.resolve(file); executeOnFile(fs.realpathSync(pattern), !shell.test("-d", file));
if (shell.test("-f", fullPath) && !processed[fullPath]) { } else {
results.push(createIgnoreResult(file)); glob.sync(pattern, globOptions).forEach(function(globMatch) {
} executeOnFile(globMatch, false);
}); });
} }
});
stats = calculateStatsPerRun(results); stats = calculateStatsPerRun(results);
@ -677,10 +663,9 @@ CLIEngine.prototype = {
stats, stats,
options = this.options, options = this.options,
configHelper = new Config(options), configHelper = new Config(options),
ignoredPaths = IgnoredPaths.load(options), ignoredPaths = new IgnoredPaths(options);
exclude = ignoredPaths.contains.bind(ignoredPaths);
if (filename && options.ignore && exclude(filename)) { if (filename && (options.ignore !== false) && ignoredPaths.contains(filename)) {
results.push(createIgnoreResult(filename)); results.push(createIgnoreResult(filename));
} else { } else {
results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig)); results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig));
@ -716,7 +701,7 @@ CLIEngine.prototype = {
var ignoredPaths; var ignoredPaths;
if (this.options.ignore) { if (this.options.ignore) {
ignoredPaths = IgnoredPaths.load(this.options); ignoredPaths = new IgnoredPaths(this.options);
return ignoredPaths.contains(filePath); return ignoredPaths.contains(filePath);
} }

16
tools/eslint/lib/cli.js

@ -158,6 +158,22 @@ var cli = {
} }
engine = new CLIEngine(translateOptions(currentOptions)); engine = new CLIEngine(translateOptions(currentOptions));
if (currentOptions.printConfig) {
if (files.length !== 1) {
log.error("The --print-config option requires a " +
"single file as positional argument.");
return 1;
}
if (text) {
log.error("The --print-config option is not available for piped-in code.");
return 1;
}
var fileConfig = engine.getConfigForFile(files[0]);
log.info(JSON.stringify(fileConfig, null, " "));
return 0;
}
report = text ? engine.executeOnText(text, currentOptions.stdinFilename) : engine.executeOnFiles(files); report = text ? engine.executeOnText(text, currentOptions.stdinFilename) : engine.executeOnFiles(files);
if (currentOptions.fix) { if (currentOptions.fix) {

640
tools/eslint/lib/code-path-analysis/code-path-analyzer.js

@ -0,0 +1,640 @@
/**
* @fileoverview A class of the code path analyzer.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var assert = require("assert"),
CodePath = require("./code-path"),
CodePathSegment = require("./code-path-segment"),
IdGenerator = require("./id-generator"),
debug = require("./debug-helpers"),
astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is a `case` node (not `default` node).
*
* @param {ASTNode} node - A `SwitchCase` node to check.
* @returns {boolean} `true` if the node is a `case` node (not `default` node).
*/
function isCaseNode(node) {
return Boolean(node.test);
}
/**
* Checks whether or not a given logical expression node goes different path
* between the `true` case and the `false` case.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a test of a choice statement.
*/
function isForkingByTrueOrFalse(node) {
var parent = node.parent;
switch (parent.type) {
case "ConditionalExpression":
case "IfStatement":
case "WhileStatement":
case "DoWhileStatement":
case "ForStatement":
return parent.test === node;
case "LogicalExpression":
return true;
default:
return false;
}
}
/**
* Gets the boolean value of a given literal node.
*
* This is used to detect infinity loops (e.g. `while (true) {}`).
* Statements preceded by an infinity loop are unreachable if the loop didn't
* have any `break` statement.
*
* @param {ASTNode} node - A node to get.
* @returns {boolean|undefined} a boolean value if the node is a Literal node,
* otherwise `undefined`.
*/
function getBooleanValueIfSimpleConstant(node) {
if (node.type === "Literal") {
return Boolean(node.value);
}
return void 0;
}
/**
* Checks that a given identifier node is a reference or not.
*
* This is used to detect the first throwable node in a `try` block.
*
* @param {ASTNode} node - An Identifier node to check.
* @returns {boolean} `true` if the node is a reference.
*/
function isIdentifierReference(node) {
var parent = node.parent;
switch (parent.type) {
case "LabeledStatement":
case "BreakStatement":
case "ContinueStatement":
case "ArrayPattern":
case "RestElement":
case "ImportSpecifier":
case "ImportDefaultSpecifier":
case "ImportNamespaceSpecifier":
case "CatchClause":
return false;
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
case "ClassDeclaration":
case "ClassExpression":
case "VariableDeclarator":
return parent.id !== node;
case "Property":
case "MethodDefinition":
return (
parent.key !== node ||
parent.computed ||
parent.shorthand
);
case "AssignmentPattern":
return parent.key !== node;
default:
return true;
}
}
/**
* Updates the current segment with the head segment.
* This is similar to local branches and tracking branches of git.
*
* To separate the current and the head is in order to not make useless segments.
*
* In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd" events
* are fired.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function forwardCurrentToHead(analyzer, node) {
var codePath = analyzer.codePath;
var state = CodePath.getState(codePath);
var currentSegments = state.currentSegments;
var headSegments = state.headSegments;
var end = Math.max(currentSegments.length, headSegments.length);
var i, currentSegment, headSegment;
// Fires leaving events.
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i];
headSegment = headSegments[i];
if (currentSegment !== headSegment && currentSegment) {
debug.dump("onCodePathSegmentEnd " + currentSegment.id);
if (currentSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentEnd",
currentSegment,
node);
}
}
}
// Update state.
state.currentSegments = headSegments;
// Fires entering events.
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i];
headSegment = headSegments[i];
if (currentSegment !== headSegment && headSegment) {
debug.dump("onCodePathSegmentStart " + headSegment.id);
CodePathSegment.markUsed(headSegment);
if (headSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentStart",
headSegment,
node);
}
}
}
}
/**
* Updates the current segment with empty.
* This is called at the last of functions or the program.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function leaveFromCurrentSegment(analyzer, node) {
var state = CodePath.getState(analyzer.codePath);
var currentSegments = state.currentSegments;
for (var i = 0; i < currentSegments.length; ++i) {
var currentSegment = currentSegments[i];
debug.dump("onCodePathSegmentEnd " + currentSegment.id);
if (currentSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentEnd",
currentSegment,
node);
}
}
state.currentSegments = [];
}
/**
* Updates the code path due to the position of a given node in the parent node
* thereof.
*
* For example, if the node is `parent.consequent`, this creates a fork from the
* current path.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function preprocess(analyzer, node) {
var codePath = analyzer.codePath;
var state = CodePath.getState(codePath);
var parent = node.parent;
switch (parent.type) {
case "LogicalExpression":
if (parent.right === node) {
state.makeLogicalRight();
}
break;
case "ConditionalExpression":
case "IfStatement":
// Fork if this node is at `consequent`/`alternate`.
// `popForkContext()` exists at `IfStatement:exit` and
// `ConditionalExpression:exit`.
if (parent.consequent === node) {
state.makeIfConsequent();
} else if (parent.alternate === node) {
state.makeIfAlternate();
}
break;
case "SwitchCase":
if (parent.consequent[0] === node) {
state.makeSwitchCaseBody(false, !parent.test);
}
break;
case "TryStatement":
if (parent.handler === node) {
state.makeCatchBlock();
} else if (parent.finalizer === node) {
state.makeFinallyBlock();
}
break;
case "WhileStatement":
if (parent.test === node) {
state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
} else {
assert(parent.body === node);
state.makeWhileBody();
}
break;
case "DoWhileStatement":
if (parent.body === node) {
state.makeDoWhileBody();
} else {
assert(parent.test === node);
state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
}
break;
case "ForStatement":
if (parent.test === node) {
state.makeForTest(getBooleanValueIfSimpleConstant(node));
} else if (parent.update === node) {
state.makeForUpdate();
} else if (parent.body === node) {
state.makeForBody();
}
break;
case "ForInStatement":
case "ForOfStatement":
if (parent.left === node) {
state.makeForInOfLeft();
} else if (parent.right === node) {
state.makeForInOfRight();
} else {
assert(parent.body === node);
state.makeForInOfBody();
}
break;
case "AssignmentPattern":
// Fork if this node is at `right`.
// `left` is executed always, so it uses the current path.
// `popForkContext()` exists at `AssignmentPattern:exit`.
if (parent.right === node) {
state.pushForkContext();
state.forkBypassPath();
state.forkPath();
}
break;
default:
break;
}
}
/**
* Updates the code path due to the type of a given node in entering.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function processCodePathToEnter(analyzer, node) {
var codePath = analyzer.codePath;
var state = codePath && CodePath.getState(codePath);
var parent = node.parent;
switch (node.type) {
case "Program":
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
if (codePath) {
// Emits onCodePathSegmentStart events if updated.
forwardCurrentToHead(analyzer, node);
debug.dumpState(node, state, false);
}
// Create the code path of this scope.
codePath = analyzer.codePath = new CodePath(
analyzer.idGenerator.next(),
codePath,
analyzer.onLooped
);
state = CodePath.getState(codePath);
// Emits onCodePathStart events.
debug.dump("onCodePathStart " + codePath.id);
analyzer.emitter.emit("onCodePathStart", codePath, node);
break;
case "LogicalExpression":
state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
break;
case "ConditionalExpression":
case "IfStatement":
state.pushChoiceContext("test", false);
break;
case "SwitchStatement":
state.pushSwitchContext(
node.cases.some(isCaseNode),
astUtils.getLabel(node));
break;
case "TryStatement":
state.pushTryContext(Boolean(node.finalizer));
break;
case "SwitchCase":
// Fork if this node is after the 2st node in `cases`.
// It's similar to `else` blocks.
// The next `test` node is processed in this path.
if (parent.discriminant !== node && parent.cases[0] !== node) {
state.forkPath();
}
break;
case "WhileStatement":
case "DoWhileStatement":
case "ForStatement":
case "ForInStatement":
case "ForOfStatement":
state.pushLoopContext(node.type, astUtils.getLabel(node));
break;
case "LabeledStatement":
if (!astUtils.isBreakableStatement(node.body)) {
state.pushBreakContext(false, node.label.name);
}
break;
default:
break;
}
// Emits onCodePathSegmentStart events if updated.
forwardCurrentToHead(analyzer, node);
debug.dumpState(node, state, false);
}
/**
* Updates the code path due to the type of a given node in leaving.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function processCodePathToExit(analyzer, node) {
var codePath = analyzer.codePath;
var state = CodePath.getState(codePath);
var dontForward = false;
switch (node.type) {
case "IfStatement":
case "ConditionalExpression":
case "LogicalExpression":
state.popChoiceContext();
break;
case "SwitchStatement":
state.popSwitchContext();
break;
case "SwitchCase":
// This is the same as the process at the 1st `consequent` node in
// `preprocess` function.
// Must do if this `consequent` is empty.
if (node.consequent.length === 0) {
state.makeSwitchCaseBody(true, !node.test);
}
if (state.forkContext.reachable) {
dontForward = true;
}
break;
case "TryStatement":
state.popTryContext();
break;
case "BreakStatement":
forwardCurrentToHead(analyzer, node);
state.makeBreak(node.label && node.label.name);
dontForward = true;
break;
case "ContinueStatement":
forwardCurrentToHead(analyzer, node);
state.makeContinue(node.label && node.label.name);
dontForward = true;
break;
case "ReturnStatement":
forwardCurrentToHead(analyzer, node);
state.makeReturn();
dontForward = true;
break;
case "ThrowStatement":
forwardCurrentToHead(analyzer, node);
state.makeThrow();
dontForward = true;
break;
case "Identifier":
if (isIdentifierReference(node)) {
state.makeFirstThrowablePathInTryBlock();
dontForward = true;
}
break;
case "CallExpression":
case "MemberExpression":
case "NewExpression":
state.makeFirstThrowablePathInTryBlock();
break;
case "WhileStatement":
case "DoWhileStatement":
case "ForStatement":
case "ForInStatement":
case "ForOfStatement":
state.popLoopContext();
break;
case "AssignmentPattern":
state.popForkContext();
break;
case "LabeledStatement":
if (!astUtils.isBreakableStatement(node.body)) {
state.popBreakContext();
}
break;
default:
break;
}
// Skip updating the current segment to avoid creating useless segments if
// the node type is the same as the parent node type.
if (!dontForward && (!node.parent || node.type !== node.parent.type)) {
// Emits onCodePathSegmentStart events if updated.
forwardCurrentToHead(analyzer, node);
}
debug.dumpState(node, state, true);
}
/**
* Updates the code path to finalize the current code path.
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function postprocess(analyzer, node) {
switch (node.type) {
case "Program":
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
var codePath = analyzer.codePath;
// Mark the current path as the final node.
CodePath.getState(codePath).makeFinal();
// Emits onCodePathSegmentEnd event of the current segments.
leaveFromCurrentSegment(analyzer, node);
// Emits onCodePathEnd event of this code path.
debug.dump("onCodePathEnd " + codePath.id);
analyzer.emitter.emit("onCodePathEnd", codePath, node);
debug.dumpDot(codePath);
codePath = analyzer.codePath = analyzer.codePath.upper;
if (codePath) {
debug.dumpState(node, CodePath.getState(codePath), true);
}
break;
default:
break;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The class to analyze code paths.
* This class implements the EventGenerator interface.
*
* @constructor
* @param {EventGenerator} eventGenerator - An event generator to wrap.
*/
function CodePathAnalyzer(eventGenerator) {
this.original = eventGenerator;
this.emitter = eventGenerator.emitter;
this.codePath = null;
this.idGenerator = new IdGenerator("s");
this.currentNode = null;
this.onLooped = this.onLooped.bind(this);
}
CodePathAnalyzer.prototype = {
constructor: CodePathAnalyzer,
/**
* Does the process to enter a given AST node.
* This updates state of analysis and calls `enterNode` of the wrapped.
*
* @param {ASTNode} node - A node which is entering.
* @returns {void}
*/
enterNode: function(node) {
this.currentNode = node;
// Updates the code path due to node's position in its parent node.
if (node.parent) {
preprocess(this, node);
}
// Updates the code path.
// And emits onCodePathStart/onCodePathSegmentStart events.
processCodePathToEnter(this, node);
// Emits node events.
this.original.enterNode(node);
this.currentNode = null;
},
/**
* Does the process to leave a given AST node.
* This updates state of analysis and calls `leaveNode` of the wrapped.
*
* @param {ASTNode} node - A node which is leaving.
* @returns {void}
*/
leaveNode: function(node) {
this.currentNode = node;
// Updates the code path.
// And emits onCodePathStart/onCodePathSegmentStart events.
processCodePathToExit(this, node);
// Emits node events.
this.original.leaveNode(node);
// Emits the last onCodePathStart/onCodePathSegmentStart events.
postprocess(this, node);
this.currentNode = null;
},
/**
* This is called on a code path looped.
* Then this raises a looped event.
*
* @param {CodePathSegment} fromSegment - A segment of prev.
* @param {CodePathSegment} toSegment - A segment of next.
* @returns {void}
*/
onLooped: function(fromSegment, toSegment) {
if (fromSegment.reachable && toSegment.reachable) {
debug.dump("onCodePathSegmentLoop " + fromSegment.id + " -> " + toSegment.id);
this.emitter.emit(
"onCodePathSegmentLoop",
fromSegment,
toSegment,
this.currentNode
);
}
}
};
module.exports = CodePathAnalyzer;

208
tools/eslint/lib/code-path-analysis/code-path-segment.js

@ -0,0 +1,208 @@
/**
* @fileoverview A class of the code path segment.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var assert = require("assert"),
debug = require("./debug-helpers");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Replaces unused segments with the previous segments of each unused segment.
*
* @param {CodePathSegment[]} segments - An array of segments to replace.
* @returns {CodePathSegment[]} The replaced array.
*/
function flattenUnusedSegments(segments) {
var done = Object.create(null);
var retv = [];
for (var i = 0; i < segments.length; ++i) {
var segment = segments[i];
// Ignores duplicated.
if (done[segment.id]) {
continue;
}
// Use previous segments if unused.
if (!segment.internal.used) {
for (var j = 0; j < segment.allPrevSegments.length; ++j) {
var prevSegment = segment.allPrevSegments[j];
if (!done[prevSegment.id]) {
done[prevSegment.id] = true;
retv.push(prevSegment);
}
}
} else {
done[segment.id] = true;
retv.push(segment);
}
}
return retv;
}
/**
* Checks whether or not a given segment is reachable.
*
* @param {CodePathSegment} segment - A segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A code path segment.
*
* @constructor
* @param {string} id - An identifier.
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
* This array includes unreachable segments.
* @param {boolean} reachable - A flag which shows this is reachable.
*/
function CodePathSegment(id, allPrevSegments, reachable) {
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* An array of the next segments.
* @type {CodePathSegment[]}
*/
this.nextSegments = [];
/**
* An array of the previous segments.
* @type {CodePathSegment[]}
*/
this.prevSegments = allPrevSegments.filter(isReachable);
/**
* An array of the next segments.
* This array includes unreachable segments.
* @type {CodePathSegment[]}
*/
this.allNextSegments = [];
/**
* An array of the previous segments.
* This array includes unreachable segments.
* @type {CodePathSegment[]}
*/
this.allPrevSegments = allPrevSegments;
/**
* A flag which shows this is reachable.
* @type {boolean}
*/
this.reachable = reachable;
// Internal data.
Object.defineProperty(this, "internal", {value: {
used: false
}});
/* istanbul ignore if */
if (debug.enabled) {
this.internal.nodes = [];
this.internal.exitNodes = [];
}
}
/**
* Creates the root segment.
*
* @param {string} id - An identifier.
* @returns {CodePathSegment} The created segment.
*/
CodePathSegment.newRoot = function(id) {
return new CodePathSegment(id, [], true);
};
/**
* Creates a segment that follows given segments.
*
* @param {string} id - An identifier.
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
CodePathSegment.newNext = function(id, allPrevSegments) {
return new CodePathSegment(
id,
flattenUnusedSegments(allPrevSegments),
allPrevSegments.some(isReachable));
};
/**
* Creates an unreachable segment that follows given segments.
*
* @param {string} id - An identifier.
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
CodePathSegment.newUnreachable = function(id, allPrevSegments) {
return new CodePathSegment(id, flattenUnusedSegments(allPrevSegments), false);
};
/**
* Creates a segment that follows given segments.
* This factory method does not connect with `allPrevSegments`.
* But this inherits `reachable` flag.
*
* @param {string} id - An identifier.
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
CodePathSegment.newDisconnected = function(id, allPrevSegments) {
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
};
/**
* Makes a given segment being used.
*
* And this function registers the segment into the previous segments as a next.
*
* @param {CodePathSegment} segment - A segment to mark.
* @returns {void}
*/
CodePathSegment.markUsed = function(segment) {
assert(!segment.internal.used, segment.id + " is marked twice.");
segment.internal.used = true;
var i;
if (segment.reachable) {
for (i = 0; i < segment.allPrevSegments.length; ++i) {
var prevSegment = segment.allPrevSegments[i];
prevSegment.allNextSegments.push(segment);
prevSegment.nextSegments.push(segment);
}
} else {
for (i = 0; i < segment.allPrevSegments.length; ++i) {
segment.allPrevSegments[i].allNextSegments.push(segment);
}
}
};
module.exports = CodePathSegment;

1323
tools/eslint/lib/code-path-analysis/code-path-state.js

File diff suppressed because it is too large

118
tools/eslint/lib/code-path-analysis/code-path.js

@ -0,0 +1,118 @@
/**
* @fileoverview A class of the code path.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var CodePathState = require("./code-path-state");
var IdGenerator = require("./id-generator");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A code path.
*
* @constructor
* @param {string} id - An identifier.
* @param {CodePath|null} upper - The code path of the upper function scope.
* @param {function} onLooped - A callback function to notify looping.
*/
function CodePath(id, upper, onLooped) {
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* The code path of the upper function scope.
* @type {CodePath|null}
*/
this.upper = upper;
/**
* The code paths of nested function scopes.
* @type {CodePath[]}
*/
this.childCodePaths = [];
// Initializes internal state.
Object.defineProperty(
this,
"internal",
{value: new CodePathState(new IdGenerator(id + "_"), onLooped)});
// Adds this into `childCodePaths` of `upper`.
if (upper) {
upper.childCodePaths.push(this);
}
}
CodePath.prototype = {
constructor: CodePath,
/**
* The initial code path segment.
* @type {CodePathSegment}
*/
get initialSegment() {
return this.internal.initialSegment;
},
/**
* Final code path segments.
* This array is a mix of `returnedSegments` and `thrownSegments`.
* @type {CodePathSegment[]}
*/
get finalSegments() {
return this.internal.finalSegments;
},
/**
* Final code path segments which is with `return` statements.
* This array contains the last path segment if it's reachable.
* Since the reachable last path returns `undefined`.
* @type {CodePathSegment[]}
*/
get returnedSegments() {
return this.internal.returnedForkContext;
},
/**
* Final code path segments which is with `throw` statements.
* @type {CodePathSegment[]}
*/
get thrownSegments() {
return this.internal.thrownForkContext;
},
/**
* Current code path segments.
* @type {CodePathSegment[]}
*/
get currentSegments() {
return this.internal.currentSegments;
}
};
/**
* Gets the state of a given code path.
*
* @param {CodePath} codePath - A code path to get.
* @returns {CodePathState} The state of the code path.
*/
CodePath.getState = function getState(codePath) {
return codePath.internal;
};
module.exports = CodePath;

197
tools/eslint/lib/code-path-analysis/debug-helpers.js

@ -0,0 +1,197 @@
/**
* @fileoverview Helpers to debug for code path analysis.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var debug = require("debug")("eslint:code-path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Gets id of a given segment.
* @param {CodePathSegment} segment - A segment to get.
* @returns {string} Id of the segment.
*/
/* istanbul ignore next */
function getId(segment) { // eslint-disable-line require-jsdoc
return segment.id + (segment.reachable ? "" : "!");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/**
* A flag that debug dumping is enabled or not.
* @type {boolean}
*/
enabled: debug.enabled,
/**
* Dumps given objects.
*
* @param {...any} args - objects to dump.
* @returns {void}
*/
dump: debug,
/**
* Dumps the current analyzing state.
*
* @param {ASTNode} node - A node to dump.
* @param {CodePathState} state - A state to dump.
* @param {boolean} leaving - A flag whether or not it's leaving
* @returns {void}
*/
dumpState: !debug.enabled ? debug : /* istanbul ignore next */ function(node, state, leaving) {
for (var i = 0; i < state.currentSegments.length; ++i) {
var segInternal = state.currentSegments[i].internal;
if (leaving) {
segInternal.exitNodes.push(node);
} else {
segInternal.nodes.push(node);
}
}
debug(
state.currentSegments.map(getId).join(",") + ") " +
node.type + (leaving ? ":exit" : "")
);
},
/**
* Dumps a DOT code of a given code path.
* The DOT code can be visialized with Graphvis.
*
* @param {CodePath} codePath - A code path to dump.
* @returns {void}
* @see http://www.graphviz.org
* @see http://www.webgraphviz.com
*/
dumpDot: !debug.enabled ? debug : /* istanbul ignore next */ function(codePath) {
var text =
"\n" +
"digraph {\n" +
"node[shape=box,style=\"rounded,filled\",fillcolor=white];\n" +
"initial[label=\"\",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
if (codePath.returnedSegments.length > 0) {
text += "final[label=\"\",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
}
if (codePath.thrownSegments.length > 0) {
text += "thrown[label=\"✘\",shape=circle,width=0.3,height=0.3,fixedsize];\n";
}
var traceMap = Object.create(null);
var arrows = this.makeDotArrows(codePath, traceMap);
for (var id in traceMap) { // eslint-disable-line guard-for-in
var segment = traceMap[id];
text += id + "[";
if (segment.reachable) {
text += "label=\"";
} else {
text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<<unreachable>>\\n";
}
if (segment.internal.nodes.length > 0) {
text += segment.internal.nodes.map(function(node) {
switch (node.type) {
case "Identifier": return node.type + " (" + node.name + ")";
case "Literal": return node.type + " (" + node.value + ")";
default: return node.type;
}
}).join("\\n");
} else if (segment.internal.exitNodes.length > 0) {
text += segment.internal.exitNodes.map(function(node) {
switch (node.type) {
case "Identifier": return node.type + ":exit (" + node.name + ")";
case "Literal": return node.type + ":exit (" + node.value + ")";
default: return node.type + ":exit";
}
}).join("\\n");
} else {
text += "????";
}
text += "\"];\n";
}
text += arrows + "\n";
text += "}";
debug("DOT", text);
},
/**
* Makes a DOT code of a given code path.
* The DOT code can be visialized with Graphvis.
*
* @param {CodePath} codePath - A code path to make DOT.
* @param {object} traceMap - Optional. A map to check whether or not segments had been done.
* @returns {string} A DOT code of the code path.
*/
makeDotArrows: function(codePath, traceMap) {
var stack = [[codePath.initialSegment, 0]];
var done = traceMap || Object.create(null);
var lastId = codePath.initialSegment.id;
var text = "initial->" + codePath.initialSegment.id;
while (stack.length > 0) {
var item = stack.pop();
var segment = item[0];
var index = item[1];
if (done[segment.id] && index === 0) {
continue;
}
done[segment.id] = segment;
var nextSegment = segment.allNextSegments[index];
if (!nextSegment) {
continue;
}
if (lastId === segment.id) {
text += "->" + nextSegment.id;
} else {
text += ";\n" + segment.id + "->" + nextSegment.id;
}
lastId = nextSegment.id;
stack.unshift([segment, 1 + index]);
stack.push([nextSegment, 0]);
}
codePath.returnedSegments.forEach(function(finalSegment) {
if (lastId === finalSegment.id) {
text += "->final";
} else {
text += ";\n" + finalSegment.id + "->final";
}
lastId = null;
});
codePath.thrownSegments.forEach(function(finalSegment) {
if (lastId === finalSegment.id) {
text += "->thrown";
} else {
text += ";\n" + finalSegment.id + "->thrown";
}
lastId = null;
});
return text + ";";
}
};

258
tools/eslint/lib/code-path-analysis/fork-context.js

@ -0,0 +1,258 @@
/**
* @fileoverview A class to operate forking.
*
* This is state of forking.
* This has a fork list and manages it.
*
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var assert = require("assert"),
CodePathSegment = require("./code-path-segment");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Gets whether or not a given segment is reachable.
*
* @param {CodePathSegment} segment - A segment to get.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* Creates new segments from the specific range of `context.segmentsList`.
*
* When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
* `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
* This `h` is from `b`, `d`, and `f`.
*
* @param {ForkContext} context - An instance.
* @param {number} begin - The first index of the previous segments.
* @param {number} end - The last index of the previous segments.
* @param {function} create - A factory function of new segments.
* @returns {CodePathSegment[]} New segments.
*/
function makeSegments(context, begin, end, create) {
var list = context.segmentsList;
if (begin < 0) {
begin = list.length + begin;
}
if (end < 0) {
end = list.length + end;
}
var segments = [];
for (var i = 0; i < context.count; ++i) {
var allPrevSegments = [];
for (var j = begin; j <= end; ++j) {
allPrevSegments.push(list[j][i]);
}
segments.push(create(context.idGenerator.next(), allPrevSegments));
}
return segments;
}
/**
* `segments` becomes doubly in a `finally` block. Then if a code path exits by a
* control statement (such as `break`, `continue`) from the `finally` block, the
* destination's segments may be half of the source segments. In that case, this
* merges segments.
*
* @param {ForkContext} context - An instance.
* @param {CodePathSegment[]} segments - Segments to merge.
* @returns {CodePathSegment[]} The merged segments.
*/
function mergeExtraSegments(context, segments) {
while (segments.length > context.count) {
var merged = [];
for (var i = 0, length = segments.length / 2 | 0; i < length; ++i) {
merged.push(CodePathSegment.newNext(
context.idGenerator.next(),
[segments[i], segments[i + length]]
));
}
segments = merged;
}
return segments;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A class to manage forking.
*
* @constructor
* @param {IdGenerator} idGenerator - An identifier generator for segments.
* @param {ForkContext|null} upper - An upper fork context.
* @param {number} count - A number of parallel segments.
*/
function ForkContext(idGenerator, upper, count) {
this.idGenerator = idGenerator;
this.upper = upper;
this.count = count;
this.segmentsList = [];
}
ForkContext.prototype = {
constructor: ForkContext,
/**
* The head segments.
* @type {CodePathSegment[]}
*/
get head() {
var list = this.segmentsList;
return list.length === 0 ? [] : list[list.length - 1];
},
/**
* A flag which shows empty.
* @type {boolean}
*/
get empty() {
return this.segmentsList.length === 0;
},
/**
* A flag which shows reachable.
* @type {boolean}
*/
get reachable() {
var segments = this.head;
return segments.length > 0 && segments.some(isReachable);
},
/**
* Creates new segments from this context.
*
* @param {number} begin - The first index of previous segments.
* @param {number} end - The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
*/
makeNext: function(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newNext);
},
/**
* Creates new segments from this context.
* The new segments is always unreachable.
*
* @param {number} begin - The first index of previous segments.
* @param {number} end - The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
*/
makeUnreachable: function(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
},
/**
* Creates new segments from this context.
* The new segments don't have connections for previous segments.
* But these inherit the reachable flag from this context.
*
* @param {number} begin - The first index of previous segments.
* @param {number} end - The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
*/
makeDisconnected: function(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
},
/**
* Adds segments into this context.
* The added segments become the head.
*
* @param {CodePathSegment[]} segments - Segments to add.
* @returns {void}
*/
add: function(segments) {
assert(segments.length >= this.count, segments.length + " >= " + this.count);
this.segmentsList.push(mergeExtraSegments(this, segments));
},
/**
* Replaces the head segments with given segments.
* The current head segments are removed.
*
* @param {CodePathSegment[]} segments - Segments to add.
* @returns {void}
*/
replaceHead: function(segments) {
assert(segments.length >= this.count, segments.length + " >= " + this.count);
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
},
/**
* Adds all segments of a given fork context into this context.
*
* @param {ForkContext} context - A fork context to add.
* @returns {void}
*/
addAll: function(context) {
assert(context.count === this.count);
var source = context.segmentsList;
for (var i = 0; i < source.length; ++i) {
this.segmentsList.push(source[i]);
}
},
/**
* Clears all secments in this context.
*
* @returns {void}
*/
clear: function() {
this.segmentsList = [];
}
};
/**
* Creates the root fork context.
*
* @param {IdGenerator} idGenerator - An identifier generator for segments.
* @returns {ForkContext} New fork context.
*/
ForkContext.newRoot = function(idGenerator) {
var context = new ForkContext(idGenerator, null, 1);
context.add([CodePathSegment.newRoot(idGenerator.next())]);
return context;
};
/**
* Creates an empty fork context preceded by a given context.
*
* @param {ForkContext} parentContext - The parent fork context.
* @param {boolean} forkLeavingPath - A flag which shows inside of `finally` block.
* @returns {ForkContext} New fork context.
*/
ForkContext.newEmpty = function(parentContext, forkLeavingPath) {
return new ForkContext(
parentContext.idGenerator,
parentContext,
(forkLeavingPath ? 2 : 1) * parentContext.count);
};
module.exports = ForkContext;

43
tools/eslint/lib/code-path-analysis/id-generator.js

@ -0,0 +1,43 @@
/**
* @fileoverview A class of identifiers generator for code path segments.
*
* Each rule uses the identifier of code path segments to store additional
* information of the code path.
*
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A generator for unique ids.
*
* @constructor
* @param {string} prefix - Optional. A prefix of generated ids.
*/
function IdGenerator(prefix) {
this.prefix = String(prefix);
this.n = 0;
}
/**
* Generates id.
*
* @returns {string} A generated id.
*/
IdGenerator.prototype.next = function() {
this.n = 1 + this.n | 0;
/* istanbul ignore if */
if (this.n < 0) {
this.n = 1;
}
return this.prefix + this.n;
};
module.exports = IdGenerator;

80
tools/eslint/lib/config.js

@ -1,7 +1,7 @@
/** /**
* @fileoverview Responsible for loading config files * @fileoverview Responsible for loading config files
* @author Seth McLaughlin * @author Seth McLaughlin
* @copyright 2014 Nicholas C. Zakas. All rights reserved. * @copyright 2014-2016 Nicholas C. Zakas. All rights reserved.
* @copyright 2014 Michael McLaughlin. All rights reserved. * @copyright 2014 Michael McLaughlin. All rights reserved.
* @copyright 2013 Seth McLaughlin. All rights reserved. * @copyright 2013 Seth McLaughlin. All rights reserved.
* See LICENSE in root directory for full license. * See LICENSE in root directory for full license.
@ -15,7 +15,7 @@
var path = require("path"), var path = require("path"),
ConfigOps = require("./config/config-ops"), ConfigOps = require("./config/config-ops"),
ConfigFile = require("./config/config-file"), ConfigFile = require("./config/config-file"),
util = require("./util"), Plugins = require("./config/plugins"),
FileFinder = require("./file-finder"), FileFinder = require("./file-finder"),
debug = require("debug"), debug = require("debug"),
userHome = require("user-home"), userHome = require("user-home"),
@ -26,14 +26,7 @@ var path = require("path"),
// Constants // Constants
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var PACKAGE_CONFIG_FILENAME = "package.json", var PERSONAL_CONFIG_DIR = userHome || null;
PERSONAL_CONFIG_DIR = userHome || null;
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
var loadedPlugins = Object.create(null);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -79,47 +72,6 @@ function loadConfig(configToLoad) {
return config; return config;
} }
/**
* Load configuration for all plugins provided.
* @param {string[]} pluginNames An array of plugin names which should be loaded.
* @returns {Object} all plugin configurations merged together
*/
function getPluginsConfig(pluginNames) {
var pluginConfig = {};
pluginNames.forEach(function(pluginName) {
var pluginNamespace = util.getNamespace(pluginName),
pluginNameWithoutNamespace = util.removeNameSpace(pluginName),
pluginNameWithoutPrefix = util.removePluginPrefix(pluginNameWithoutNamespace),
plugin = {},
rules = {};
if (!loadedPlugins[pluginNameWithoutPrefix]) {
try {
plugin = require(pluginNamespace + util.PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix);
loadedPlugins[pluginNameWithoutPrefix] = plugin;
} catch (err) {
debug("Failed to load plugin configuration for " + pluginNameWithoutPrefix + ". Proceeding without it.");
plugin = { rulesConfig: {}};
}
} else {
plugin = loadedPlugins[pluginNameWithoutPrefix];
}
if (!plugin.rulesConfig) {
plugin.rulesConfig = {};
}
Object.keys(plugin.rulesConfig).forEach(function(item) {
rules[pluginNameWithoutPrefix + "/" + item] = plugin.rulesConfig[item];
});
pluginConfig = ConfigOps.merge(pluginConfig, rules);
});
return {rules: pluginConfig};
}
/** /**
* Get personal config object from ~/.eslintrc. * Get personal config object from ~/.eslintrc.
* @returns {Object} the personal config object (empty object if there is no personal config) * @returns {Object} the personal config object (empty object if there is no personal config)
@ -156,7 +108,7 @@ function getLocalConfig(thisConfig, directory) {
localConfigFiles = thisConfig.findLocalConfigFiles(directory), localConfigFiles = thisConfig.findLocalConfigFiles(directory),
numFiles = localConfigFiles.length, numFiles = localConfigFiles.length,
rootPath, rootPath,
projectConfigPath = ConfigFile.getFilenameForDirectory(process.cwd()); projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd);
for (i = 0; i < numFiles; i++) { for (i = 0; i < numFiles; i++) {
@ -192,7 +144,7 @@ function getLocalConfig(thisConfig, directory) {
} }
// Use the personal config file if there are no other local config files found. // Use the personal config file if there are no other local config files found.
return found ? config : ConfigOps.merge(config, getPersonalConfig()); return found || thisConfig.useSpecificConfig ? config : ConfigOps.merge(config, getPersonalConfig());
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -204,7 +156,6 @@ function getLocalConfig(thisConfig, directory) {
* @constructor * @constructor
* @class Config * @class Config
* @param {Object} options Options to be passed in * @param {Object} options Options to be passed in
* @param {string} [cwd] current working directory. Defaults to process.cwd()
*/ */
function Config(options) { function Config(options) {
var useConfig; var useConfig;
@ -240,7 +191,7 @@ function Config(options) {
if (isResolvable(useConfig) || isResolvable("eslint-config-" + useConfig) || useConfig.charAt(0) === "@") { if (isResolvable(useConfig) || isResolvable("eslint-config-" + useConfig) || useConfig.charAt(0) === "@") {
this.useSpecificConfig = loadConfig(useConfig); this.useSpecificConfig = loadConfig(useConfig);
} else { } else {
this.useSpecificConfig = loadConfig(path.resolve(process.cwd(), useConfig)); this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, useConfig));
} }
} }
} }
@ -254,8 +205,7 @@ function Config(options) {
Config.prototype.getConfig = function(filePath) { Config.prototype.getConfig = function(filePath) {
var config, var config,
userConfig, userConfig,
directory = filePath ? path.dirname(filePath) : process.cwd(), directory = filePath ? path.dirname(filePath) : this.options.cwd;
pluginConfig;
debug("Constructing config for " + (filePath ? filePath : "text")); debug("Constructing config for " + (filePath ? filePath : "text"));
@ -266,7 +216,7 @@ Config.prototype.getConfig = function(filePath) {
return config; return config;
} }
// Step 1: Determine user-specified config from .eslintrc and package.json files // Step 1: Determine user-specified config from .eslintrc.* and package.json files
if (this.useEslintrc) { if (this.useEslintrc) {
debug("Using .eslintrc and package.json files"); debug("Using .eslintrc and package.json files");
userConfig = getLocalConfig(this, directory); userConfig = getLocalConfig(this, directory);
@ -287,10 +237,9 @@ Config.prototype.getConfig = function(filePath) {
config = ConfigOps.merge(config, this.useSpecificConfig); config = ConfigOps.merge(config, this.useSpecificConfig);
} }
// Step 5: Merge in command line environments // Step 5: Merge in command line environments
debug("Merging command line environment settings"); debug("Merging command line environment settings");
config = ConfigOps.merge(config, ConfigOps.createEnvironmentConfig(this.env)); config = ConfigOps.merge(config, { env: this.env });
// Step 6: Merge in command line rules // Step 6: Merge in command line rules
if (this.options.rules) { if (this.options.rules) {
@ -304,14 +253,13 @@ Config.prototype.getConfig = function(filePath) {
// Step 8: Merge in command line plugins // Step 8: Merge in command line plugins
if (this.options.plugins) { if (this.options.plugins) {
debug("Merging command line plugins"); debug("Merging command line plugins");
pluginConfig = getPluginsConfig(this.options.plugins); Plugins.loadAll(this.options.plugins);
config = ConfigOps.merge(config, { plugins: this.options.plugins }); config = ConfigOps.merge(config, { plugins: this.options.plugins });
} }
// Step 9: Merge in plugin specific rules in reverse // Step 9: Apply environments to the config if present
if (config.plugins) { if (config.env) {
pluginConfig = getPluginsConfig(config.plugins); config = ConfigOps.applyEnvironments(config);
config = ConfigOps.merge(pluginConfig, config);
} }
this.cache[directory] = config; this.cache[directory] = config;
@ -327,7 +275,7 @@ Config.prototype.getConfig = function(filePath) {
Config.prototype.findLocalConfigFiles = function(directory) { Config.prototype.findLocalConfigFiles = function(directory) {
if (!this.localConfigFinder) { if (!this.localConfigFinder) {
this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, PACKAGE_CONFIG_FILENAME); this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
} }
return this.localConfigFinder.findAllInDirectoryAndParents(directory); return this.localConfigFinder.findAllInDirectoryAndParents(directory);

336
tools/eslint/lib/config/autoconfig.js

@ -0,0 +1,336 @@
/**
* @fileoverview Used for creating a suggested configuration based on project code.
* @author Ian VanSchooten
* @copyright 2015 Ian VanSchooten. All rights reserved.
* See LICENSE in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var lodash = require("lodash"),
debug = require("debug"),
eslint = require("../eslint"),
configRule = require("./config-rule"),
recConfig = require("../../conf/eslint.json");
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
var MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only
RECOMMENDED_CONFIG_NAME = "eslint:recommended";
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
debug = debug("eslint:autoconfig");
/**
* Information about a rule configuration, in the context of a Registry.
*
* @typedef {Object} registryItem
* @param {ruleConfig} config A valid configuration for the rule
* @param {number} specificity The number of elements in the ruleConfig array
* @param {number} errorCount The number of errors encountered when linting with the config
*/
/**
* This callback is used to measure execution status in a progress bar
* @callback progressCallback
* @param {number} The total number of times the callback will be called.
*/
/**
* Create registryItems for rules
* @param {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items
* @returns {Object} registryItems for each rule in provided rulesConfig
*/
function makeRegistryItems(rulesConfig) {
return Object.keys(rulesConfig).reduce(function(accumulator, ruleId) {
accumulator[ruleId] = rulesConfig[ruleId].map(function(config) {
return {
config: config,
specificity: config.length || 1,
errorCount: void 0
};
});
return accumulator;
}, {});
}
/**
* Creates an object in which to store rule configs and error counts
*
* Unless a rulesConfig is provided at construction, the registry will not contain
* any rules, only methods. This will be useful for building up registries manually.
*
* @constructor
* @class Registry
* @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations
*/
function Registry(rulesConfig) {
this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {};
}
Registry.prototype = {
constructor: Registry,
/**
* Populate the registry with core rule configs.
*
* It will set the registry's `rule` property to an object having rule names
* as keys and an array of registryItems as values.
*
* @returns {void}
*/
populateFromCoreRules: function() {
var rulesConfig = configRule.createCoreRuleConfigs();
this.rules = makeRegistryItems(rulesConfig);
},
/**
* Creates sets of rule configurations which can be used for linting
* and initializes registry errors to zero for those configurations (side effect).
*
* This combines as many rules together as possible, such that the first sets
* in the array will have the highest number of rules configured, and later sets
* will have fewer and fewer, as not all rules have the same number of possible
* configurations.
*
* The length of the returned array will be <= MAX_CONFIG_COMBINATIONS.
*
* @param {Object} registry The autoconfig registry
* @returns {Object[]} "rules" configurations to use for linting
*/
buildRuleSets: function() {
var idx = 0,
ruleIds = Object.keys(this.rules),
ruleSets = [];
/**
* Add a rule configuration from the registry to the ruleSets
*
* This is broken out into its own function so that it doesn't need to be
* created inside of the while loop.
*
* @param {string} rule The ruleId to add.
* @returns {void}
*/
var addRuleToRuleSet = function(rule) {
// This check ensures that there is a rule configuration, and that
// it either has fewer than the max cominbations allowed, or if it has
// too many configs, we will only use the most basic of them.
var hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS);
if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) {
// If the rule has too many possible combinations, only take simple ones, avoiding objects.
if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === "object") {
return;
}
ruleSets[idx] = ruleSets[idx] || {};
ruleSets[idx][rule] = this.rules[rule][idx].config;
// Initialize errorCount to zero, since this is a config which will be linted
this.rules[rule][idx].errorCount = 0;
}
}.bind(this);
while (ruleSets.length === idx) {
ruleIds.forEach(addRuleToRuleSet);
idx += 1;
}
return ruleSets;
},
/**
* Remove all items from the registry with a non-zero number of errors
*
* Note: this also removes rule configurations which were not linted
* (meaning, they have an undefined errorCount).
*
* @returns {void}
*/
stripFailingConfigs: function() {
var ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = lodash.assign({}, this.rules);
ruleIds.forEach(function(ruleId) {
var errorFreeItems = newRegistry.rules[ruleId].filter(function(registryItem) {
return (registryItem.errorCount === 0);
});
if (errorFreeItems.length > 0) {
newRegistry.rules[ruleId] = errorFreeItems;
} else {
delete newRegistry.rules[ruleId];
}
});
return newRegistry;
},
/**
* Removes rule configurations which were not included in a ruleSet
*
* @returns {void}
*/
stripExtraConfigs: function() {
var ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = lodash.assign({}, this.rules);
ruleIds.forEach(function(ruleId) {
newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(function(registryItem) {
return (typeof registryItem.errorCount !== "undefined");
});
});
return newRegistry;
},
/**
* Creates a registry of rules which had no error-free configs.
* The new registry is intended to be analyzed to determine whether its rules
* should be disabled or set to warning.
*
* @returns {Registry} A registry of failing rules.
*/
getFailingRulesRegistry: function() {
var ruleIds = Object.keys(this.rules),
failingRegistry = new Registry();
ruleIds.forEach(function(ruleId) {
var failingConfigs = this.rules[ruleId].filter(function(registryItem) {
return (registryItem.errorCount > 0);
});
if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
failingRegistry.rules[ruleId] = failingConfigs;
}
}.bind(this));
return failingRegistry;
},
/**
* Create an eslint config for any rules which only have one configuration
* in the registry.
*
* @returns {Object} An eslint config with rules section populated
*/
createConfig: function() {
var ruleIds = Object.keys(this.rules),
config = {rules: {}};
ruleIds.forEach(function(ruleId) {
if (this.rules[ruleId].length === 1) {
config.rules[ruleId] = this.rules[ruleId][0].config;
}
}.bind(this));
return config;
},
/**
* Return a cloned registry containing only configs with a desired specificity
*
* @param {number} specificity Only keep configs with this specificity
* @returns {Registry} A registry of rules
*/
filterBySpecificity: function(specificity) {
var ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = lodash.assign({}, this.rules);
ruleIds.forEach(function(ruleId) {
newRegistry.rules[ruleId] = this.rules[ruleId].filter(function(registryItem) {
return (registryItem.specificity === specificity);
});
}.bind(this));
return newRegistry;
},
/**
* Lint SourceCodes against all configurations in the registry, and record results
*
* @param {Object[]} sourceCodes SourceCode objects for each filename
* @param {Object} config ESLint config object
* @param {progressCallback} [cb] Optional callback for reporting execution status
* @returns {Registry} New registry with errorCount populated
*/
lintSourceCode: function(sourceCodes, config, cb) {
var totalFilesLinting,
lintConfig,
ruleSets,
ruleSetIdx,
filenames,
lintedRegistry;
lintedRegistry = new Registry();
lintedRegistry.rules = lodash.assign({}, this.rules);
ruleSets = lintedRegistry.buildRuleSets();
lintedRegistry = lintedRegistry.stripExtraConfigs();
debug("Linting with all possible rule combinations");
filenames = Object.keys(sourceCodes);
totalFilesLinting = filenames.length * ruleSets.length;
filenames.forEach(function(filename) {
debug("Linting file: " + filename);
ruleSetIdx = 0;
ruleSets.forEach(function(ruleSet) {
lintConfig = lodash.assign({}, config, {rules: ruleSet});
var lintResults = eslint.verify(sourceCodes[filename], lintConfig);
lintResults.forEach(function(result) {
lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1;
});
ruleSetIdx += 1;
if (cb) {
cb(totalFilesLinting); // eslint-disable-line callback-return
}
});
// Deallocate for GC
sourceCodes[filename] = null;
});
return lintedRegistry;
}
};
/**
* Extract rule configuration into eslint:recommended where possible.
*
* This will return a new config with `"extends": "eslint:recommended"` and
* only the rules which have configurations different from the recommended config.
*
* @param {Object} config config object
* @returns {Object} config object using `"extends": "eslint:recommended"`
*/
function extendFromRecommended(config) {
var newConfig = lodash.assign({}, config);
var recRules = Object.keys(recConfig.rules).filter(function(ruleId) {
return (recConfig.rules[ruleId] === 2 || recConfig.rules[ruleId][0] === 2);
});
recRules.forEach(function(ruleId) {
if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
delete newConfig.rules[ruleId];
}
});
newConfig.extends = RECOMMENDED_CONFIG_NAME;
return newConfig;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
Registry: Registry,
extendFromRecommended: extendFromRecommended
};

211
tools/eslint/lib/config/config-file.js

@ -16,9 +16,31 @@ var debug = require("debug"),
path = require("path"), path = require("path"),
ConfigOps = require("./config-ops"), ConfigOps = require("./config-ops"),
validator = require("./config-validator"), validator = require("./config-validator"),
Plugins = require("./plugins"),
resolveModule = require("resolve"),
pathIsInside = require("path-is-inside"),
stripComments = require("strip-json-comments"), stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify"),
isAbsolutePath = require("path-is-absolute"); isAbsolutePath = require("path-is-absolute");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines sort order for object keys for json-stable-stringify
*
* see: https://github.com/substack/json-stable-stringify#cmp
*
* @param {Object} a The first comparison object ({key: akey, value: avalue})
* @param {Object} b The second comparison object ({key: bkey, value: bvalue})
* @returns {number} 1 or -1, used in stringify cmp method
*/
function sortByKey(a, b) {
return a.key > b.key ? 1 : -1;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Private // Private
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -28,7 +50,8 @@ var CONFIG_FILES = [
".eslintrc.yaml", ".eslintrc.yaml",
".eslintrc.yml", ".eslintrc.yml",
".eslintrc.json", ".eslintrc.json",
".eslintrc" ".eslintrc",
"package.json"
]; ];
debug = debug("eslint:config-file"); debug = debug("eslint:config-file");
@ -111,7 +134,7 @@ function loadLegacyConfigFile(filePath) {
var yaml = require("js-yaml"); var yaml = require("js-yaml");
try { try {
return yaml.safeLoad(stripComments(readFile(filePath))) || {}; return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
} catch (e) { } catch (e) {
debug("Error reading YAML file: " + filePath); debug("Error reading YAML file: " + filePath);
e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; e.message = "Cannot read config file: " + filePath + "\nError: " + e.message;
@ -147,7 +170,7 @@ function loadJSConfigFile(filePath) {
function loadPackageJSONConfigFile(filePath) { function loadPackageJSONConfigFile(filePath) {
debug("Loading package.json config file: " + filePath); debug("Loading package.json config file: " + filePath);
try { try {
return require(filePath).eslintConfig || null; return loadJSONConfigFile(filePath).eslintConfig || null;
} catch (e) { } catch (e) {
debug("Error reading package.json file: " + filePath); debug("Error reading package.json file: " + filePath);
e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; e.message = "Cannot read config file: " + filePath + "\nError: " + e.message;
@ -155,61 +178,44 @@ function loadPackageJSONConfigFile(filePath) {
} }
} }
/**
* Loads a JavaScript configuration from a package.
* @param {string} filePath The package name to load.
* @returns {Object} The configuration object from the package.
* @throws {Error} If the package cannot be read.
* @private
*/
function loadPackage(filePath) {
debug("Loading config package: " + filePath);
try {
return require(filePath);
} catch (e) {
debug("Error reading package: " + filePath);
e.message = "Cannot read config package: " + filePath + "\nError: " + e.message;
throw e;
}
}
/** /**
* Loads a configuration file regardless of the source. Inspects the file path * Loads a configuration file regardless of the source. Inspects the file path
* to determine the correctly way to load the config file. * to determine the correctly way to load the config file.
* @param {string} filePath The path to the configuration. * @param {Object} file The path to the configuration.
* @returns {Object} The configuration information. * @returns {Object} The configuration information.
* @private * @private
*/ */
function loadConfigFile(filePath) { function loadConfigFile(file) {
var config; var config;
if (isFilePath(filePath)) { var filePath = file.filePath;
switch (path.extname(filePath)) {
case ".js": switch (path.extname(filePath)) {
config = loadJSConfigFile(filePath); case ".js":
break; config = loadJSConfigFile(filePath);
if (file.configName) {
case ".json": config = config.configs[file.configName];
if (path.basename(filePath) === "package.json") { }
config = loadPackageJSONConfigFile(filePath); break;
if (config === null) {
return null; case ".json":
} if (path.basename(filePath) === "package.json") {
} else { config = loadPackageJSONConfigFile(filePath);
config = loadJSONConfigFile(filePath); if (config === null) {
return null;
} }
break; } else {
config = loadJSONConfigFile(filePath);
}
break;
case ".yaml": case ".yaml":
case ".yml": case ".yml":
config = loadYAMLConfigFile(filePath); config = loadYAMLConfigFile(filePath);
break; break;
default: default:
config = loadLegacyConfigFile(filePath); config = loadLegacyConfigFile(filePath);
}
} else {
config = loadPackage(filePath);
} }
return ConfigOps.merge(ConfigOps.createEmptyConfig(), config); return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);
@ -225,7 +231,7 @@ function loadConfigFile(filePath) {
function writeJSONConfigFile(config, filePath) { function writeJSONConfigFile(config, filePath) {
debug("Writing JSON config file: " + filePath); debug("Writing JSON config file: " + filePath);
var content = JSON.stringify(config, null, 4); var content = stringify(config, {cmp: sortByKey, space: 4});
fs.writeFileSync(filePath, content, "utf8"); fs.writeFileSync(filePath, content, "utf8");
} }
@ -242,7 +248,7 @@ function writeYAMLConfigFile(config, filePath) {
// lazy load YAML to improve performance when not used // lazy load YAML to improve performance when not used
var yaml = require("js-yaml"); var yaml = require("js-yaml");
var content = yaml.safeDump(config); var content = yaml.safeDump(config, {sortKeys: true});
fs.writeFileSync(filePath, content, "utf8"); fs.writeFileSync(filePath, content, "utf8");
} }
@ -256,7 +262,7 @@ function writeYAMLConfigFile(config, filePath) {
function writeJSConfigFile(config, filePath) { function writeJSConfigFile(config, filePath) {
debug("Writing JS config file: " + filePath); debug("Writing JS config file: " + filePath);
var content = "module.exports = " + JSON.stringify(config, null, 4) + ";"; var content = "module.exports = " + stringify(config, {cmp: sortByKey, space: 4}) + ";";
fs.writeFileSync(filePath, content, "utf8"); fs.writeFileSync(filePath, content, "utf8");
} }
@ -288,6 +294,27 @@ function write(config, filePath) {
} }
} }
/**
* Determines the lookup path for node packages referenced in a config file.
* If the config
* @param {string} configFilePath The config file referencing the file.
* @returns {string} The lookup path for the file path.
* @private
*/
function getLookupPath(configFilePath) {
// calculates the path of the project including ESLint as dependency
var projectPath = path.resolve(__dirname, "../../../");
if (configFilePath && pathIsInside(configFilePath, projectPath)) {
// be careful of https://github.com/substack/node-resolve/issues/78
return path.resolve(configFilePath);
}
// default to ESLint project path since it's unlikely that plugins will be
// in this directory
return projectPath;
}
/** /**
* Applies values from the "extends" field in a configuration file. * Applies values from the "extends" field in a configuration file.
* @param {Object} config The configuration information. * @param {Object} config The configuration information.
@ -337,37 +364,59 @@ function applyExtends(config, filePath) {
return config; return config;
} }
/**
* Brings package name to correct format based on prefix
* @param {string} name The name of the package.
* @param {string} prefix Can be either "eslint-plugin" or "eslint-config
* @returns {string} Normalized name of the package
* @private
*/
function normalizePackageName(name, prefix) {
if (name.charAt(0) === "@") {
// it's a scoped package
// package name is "eslint-config", or just a username
var scopedPackageShortcutRegex = new RegExp("^(@[^\/]+)(?:\/(?:" + prefix + ")?)?$"),
scopedPackageNameRegex = new RegExp("^" + prefix + "(-|$)");
if (scopedPackageShortcutRegex.test(name)) {
name = name.replace(scopedPackageShortcutRegex, "$1/" + prefix);
} else if (!scopedPackageNameRegex.test(name.split("/")[1])) {
// for scoped packages, insert the eslint-config after the first / unless
// the path is already @scope/eslint or @scope/eslint-config-xxx
name = name.replace(/^@([^\/]+)\/(.*)$/, "@$1/" + prefix + "-$2");
}
} else if (name.indexOf(prefix + "-") !== 0) {
name = prefix + "-" + name;
}
return name;
}
/** /**
* Resolves a configuration file path into the fully-formed path, whether filename * Resolves a configuration file path into the fully-formed path, whether filename
* or package name. * or package name.
* @param {string} filePath The filepath to resolve. * @param {string} filePath The filepath to resolve.
* @returns {string} A path that can be used directly to load the configuration. * @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} A path that can be used directly to load the configuration.
* @private * @private
*/ */
function resolve(filePath) { function resolve(filePath, relativeTo) {
if (isFilePath(filePath)) { if (isFilePath(filePath)) {
return path.resolve(filePath); return { filePath: path.resolve(relativeTo || "", filePath) };
} else { } else {
if (filePath.indexOf("plugin:") === 0) {
// it's a package var packagePath = filePath.substr(7, filePath.lastIndexOf("/") - 7);
var configName = filePath.substr(filePath.lastIndexOf("/") + 1, filePath.length - filePath.lastIndexOf("/") - 1);
if (filePath.charAt(0) === "@") { filePath = resolveModule.sync(normalizePackageName(packagePath, "eslint-plugin"), {
// it's a scoped package basedir: getLookupPath(relativeTo)
});
// package name is "eslint-config", or just a username return { filePath: filePath, configName: configName };
var scopedPackageShortcutRegex = /^(@[^\/]+)(?:\/(?:eslint-config)?)?$/; } else {
if (scopedPackageShortcutRegex.test(filePath)) { filePath = resolveModule.sync(normalizePackageName(filePath, "eslint-config"), {
filePath = filePath.replace(scopedPackageShortcutRegex, "$1/eslint-config"); basedir: getLookupPath(relativeTo)
} else if (filePath.split("/")[1].indexOf("eslint-config-") !== 0) { });
// for scoped packages, insert the eslint-config after the first / return { filePath: filePath };
filePath = filePath.replace(/^@([^\/]+)\/(.*)$/, "@$1/eslint-config-$2");
}
} else if (filePath.indexOf("eslint-config-") !== 0) {
filePath = "eslint-config-" + filePath;
} }
return filePath;
} }
} }
@ -376,16 +425,29 @@ function resolve(filePath) {
* Loads a configuration file from the given file path. * Loads a configuration file from the given file path.
* @param {string} filePath The filename or package name to load the configuration * @param {string} filePath The filename or package name to load the configuration
* information from. * information from.
* @param {boolean} [applyEnvironments=false] Set to true to merge in environment settings.
* @returns {Object} The configuration information. * @returns {Object} The configuration information.
* @private * @private
*/ */
function load(filePath) { function load(filePath, applyEnvironments) {
var resolvedPath = resolve(filePath), var resolvedPath = resolve(filePath),
config = loadConfigFile(resolvedPath); config = loadConfigFile(resolvedPath);
if (config) { if (config) {
// ensure plugins are properly loaded first
if (config.plugins) {
Plugins.loadAll(config.plugins);
}
// include full path of parser if present
if (config.parser) {
config.parser = resolveModule.sync(config.parser, {
basedir: getLookupPath(path.dirname(path.resolve(filePath)))
});
}
// validate the configuration before continuing // validate the configuration before continuing
validator.validate(config, filePath); validator.validate(config, filePath);
@ -395,8 +457,8 @@ function load(filePath) {
config = applyExtends(config, filePath); config = applyExtends(config, filePath);
} }
if (config.env) { if (config.env && applyEnvironments) {
// Merge in environment-specific globals and ecmaFeatures. // Merge in environment-specific globals and parserOptions.
config = ConfigOps.applyEnvironments(config); config = ConfigOps.applyEnvironments(config);
} }
@ -411,6 +473,7 @@ function load(filePath) {
module.exports = { module.exports = {
getLookupPath: getLookupPath,
load: load, load: load,
resolve: resolve, resolve: resolve,
write: write, write: write,

384
tools/eslint/lib/config/config-initializer.js

@ -10,9 +10,19 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var exec = require("child_process").exec, var util = require("util"),
debug = require("debug"),
lodash = require("lodash"),
inquirer = require("inquirer"), inquirer = require("inquirer"),
ConfigFile = require("./config-file"); ProgressBar = require("progress"),
autoconfig = require("./autoconfig.js"),
ConfigFile = require("./config-file"),
getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
npmUtil = require("../util/npm-util"),
recConfig = require("../../conf/eslint.json"),
log = require("../logging");
debug = debug("eslint:config-initializer");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Private // Private
@ -21,12 +31,11 @@ var exec = require("child_process").exec,
/* istanbul ignore next: hard to test fs function */ /* istanbul ignore next: hard to test fs function */
/** /**
* Create .eslintrc file in the current working directory * Create .eslintrc file in the current working directory
* @param {object} config object that contains user's answers * @param {Object} config object that contains user's answers
* @param {string} format The file format to write to. * @param {string} format The file format to write to.
* @param {function} callback function to call once the file is written.
* @returns {void} * @returns {void}
*/ */
function writeFile(config, format, callback) { function writeFile(config, format) {
// default is .js // default is .js
var extname = ".js"; var extname = ".js";
@ -37,69 +46,209 @@ function writeFile(config, format, callback) {
} }
try { ConfigFile.write(config, "./.eslintrc" + extname);
ConfigFile.write(config, "./.eslintrc" + extname); log.info("Successfully created .eslintrc" + extname + " file in " + process.cwd());
console.log("Successfully created .eslintrc" + extname + " file in " + process.cwd()); }
} catch (e) {
callback(e); /**
* Synchronously install necessary plugins, configs, parsers, etc. based on the config
* @param {Object} config config object
* @returns {void}
*/
function installModules(config) {
var modules = [],
installStatus,
modulesToInstall;
// Create a list of modules which should be installed based on config
if (config.plugins) {
modules = modules.concat(config.plugins.map(function(name) {
return "eslint-plugin-" + name;
}));
}
if (config.extends && config.extends.indexOf("eslint:") === -1) {
modules.push("eslint-config-" + config.extends);
}
// Determine which modules are already installed
if (modules.length === 0) {
return; return;
} }
installStatus = npmUtil.checkDevDeps(modules);
// install any external configs as well as any included plugins // Install packages which aren't already installed
if (config.extends && config.extends.indexOf("eslint") === -1) { modulesToInstall = Object.keys(installStatus).filter(function(module) {
console.log("Installing additional dependencies"); return installStatus[module] === false;
exec("npm i eslint-config-" + config.extends + " --save-dev", function(err) { });
if (modulesToInstall.length > 0) {
log.info("Installing " + modulesToInstall.join(", "));
npmUtil.installSyncSaveDev(modulesToInstall);
}
}
if (err) { /**
return callback(err); * Set the `rules` of a config by examining a user's source code
} *
* Note: This clones the config object and returns a new config to avoid mutating
* the original config parameter.
*
* @param {Object} answers answers received from inquirer
* @param {Object} config config object
* @returns {Object} config object with configured rules
*/
function configureRules(answers, config) {
var BAR_TOTAL = 20,
BAR_SOURCE_CODE_TOTAL = 4;
var newConfig = lodash.assign({}, config),
bar,
patterns,
sourceCodes,
fileQty,
registry,
failingRegistry,
disabledConfigs = {},
singleConfigs,
specTwoConfigs,
specThreeConfigs,
defaultConfigs;
// TODO: consider supporting more than 1 plugin though it's required yet. // Set up a progress bar, as this process can take a long time
exec("npm i eslint-plugin-" + config.plugins[0] + " --save-dev", callback); bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
width: 30,
total: BAR_TOTAL
});
bar.tick(0); // Shows the progress bar
// Get the SourceCode of all chosen files
patterns = answers.patterns.split(/[\s]+/);
try {
sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, function(total) {
bar.tick((BAR_SOURCE_CODE_TOTAL / total));
}); });
return; } catch (e) {
log.info("\n");
throw e;
} }
fileQty = Object.keys(sourceCodes).length;
// install the react plugin if it was explictly chosen if (fileQty === 0) {
if (config.plugins && config.plugins.indexOf("react") >= 0) { log.info("\n");
console.log("Installing React plugin"); throw new Error("Automatic Configuration failed. No files were able to be parsed.");
exec("npm i eslint-plugin-react --save-dev", callback);
return;
} }
callback();
// Create a registry of rule configs
registry = new autoconfig.Registry();
registry.populateFromCoreRules();
// Lint all files with each rule config in the registry
registry = registry.lintSourceCode(sourceCodes, newConfig, function(total) {
bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total); // Subtract out ticks used at beginning
});
debug("\nRegistry: " + util.inspect(registry.rules, {depth: null}));
// Create a list of recommended rules, because we don't want to disable them
var recRules = Object.keys(recConfig.rules).filter(function(ruleId) {
return (recConfig.rules[ruleId] === 2 || recConfig.rules[ruleId][0] === 2);
});
// Find and disable rules which had no error-free configuration
failingRegistry = registry.getFailingRulesRegistry();
Object.keys(failingRegistry.rules).forEach(function(ruleId) {
// If the rule is recommended, set it to error, otherwise disable it
disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0;
});
// Now that we know which rules to disable, strip out configs with errors
registry = registry.stripFailingConfigs();
// If there is only one config that results in no errors for a rule, we should use it.
// createConfig will only add rules that have one configuration in the registry.
singleConfigs = registry.createConfig().rules;
// The "sweet spot" for number of options in a config seems to be two (severity plus one option).
// Very often, a third option (usually an object) is available to address
// edge cases, exceptions, or unique situations. We will prefer to use a config with
// specificity of two.
specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
// Maybe a specific combination using all three options works
specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
// If all else fails, try to use the default (severity only)
defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
// Combine configs in reverse priority order (later take precedence)
newConfig.rules = lodash.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs);
// Make sure progress bar has finished (floating point rounding)
bar.update(BAR_TOTAL);
// Log out some stats to let the user know what happened
var totalRules = Object.keys(newConfig.rules).length;
var enabledRules = Object.keys(newConfig.rules).filter(function(ruleId) {
return (newConfig.rules[ruleId] !== 0);
}).length;
var resultMessage = [
"\nEnabled " + enabledRules + " out of " + totalRules,
"rules based on " + fileQty,
"file" + ((fileQty === 1) ? "." : "s.")
].join(" ");
log.info(resultMessage);
return newConfig;
} }
/** /**
* process user's answers and create config object * process user's answers and create config object
* @param {object} answers answers received from inquirer * @param {Object} answers answers received from inquirer
* @returns {object} config object * @returns {Object} config object
*/ */
function processAnswers(answers) { function processAnswers(answers) {
var config = {rules: {}, env: {}, extends: "eslint:recommended"}; var config = {rules: {}, env: {}};
config.rules.indent = [2, answers.indent];
config.rules.quotes = [2, answers.quotes];
config.rules["linebreak-style"] = [2, answers.linebreak];
config.rules.semi = [2, answers.semi ? "always" : "never"];
if (answers.es6) { if (answers.es6) {
config.env.es6 = true; config.env.es6 = true;
if (answers.modules) {
config.parserOptions = config.parserOptions || {};
config.parserOptions.sourceType = "module";
}
}
if (answers.commonjs) {
config.env.commonjs = true;
} }
answers.env.forEach(function(env) { answers.env.forEach(function(env) {
config.env[env] = true; config.env[env] = true;
}); });
if (answers.jsx) { if (answers.jsx) {
config.ecmaFeatures = {jsx: true}; config.parserOptions = config.parserOptions || {};
config.parserOptions.ecmaFeatures = config.parserOptions.ecmaFeatures || {};
config.parserOptions.ecmaFeatures.jsx = true;
if (answers.react) { if (answers.react) {
config.plugins = ["react"]; config.plugins = ["react"];
config.ecmaFeatures.experimentalObjectRestSpread = true; config.parserOptions.ecmaFeatures.experimentalObjectRestSpread = true;
} }
} }
if (answers.source === "prompt") {
config.extends = "eslint:recommended";
config.rules.indent = [2, answers.indent];
config.rules.quotes = [2, answers.quotes];
config.rules["linebreak-style"] = [2, answers.linebreak];
config.rules.semi = [2, answers.semi ? "always" : "never"];
}
installModules(config);
if (answers.source === "auto") {
config = configureRules(answers, config);
config = autoconfig.extendFromRecommended(config);
}
return config; return config;
} }
/** /**
* process user's style guide of choice and return an appropriate config object. * process user's style guide of choice and return an appropriate config object.
* @param {string} guide name of the chosen style guide * @param {string} guide name of the chosen style guide
* @returns {object} config object * @returns {Object} config object
*/ */
function getConfigForStyleGuide(guide) { function getConfigForStyleGuide(guide) {
var guides = { var guides = {
@ -110,6 +259,9 @@ function getConfigForStyleGuide(guide) {
if (!guides[guide]) { if (!guides[guide]) {
throw new Error("You referenced an unsupported guide."); throw new Error("You referenced an unsupported guide.");
} }
installModules(guides[guide]);
return guides[guide]; return guides[guide];
} }
@ -120,13 +272,18 @@ function getConfigForStyleGuide(guide) {
* @returns {void} * @returns {void}
*/ */
function promptUser(callback) { function promptUser(callback) {
var config;
inquirer.prompt([ inquirer.prompt([
{ {
type: "list", type: "list",
name: "source", name: "source",
message: "How would you like to configure ESLint?", message: "How would you like to configure ESLint?",
default: "prompt", default: "prompt",
choices: [{name: "Answer questions about your style", value: "prompt"}, {name: "Use a popular style guide", value: "guide"}] choices: [
{name: "Answer questions about your style", value: "prompt"},
{name: "Use a popular style guide", value: "guide"},
{name: "Inspect your JavaScript file(s)", value: "auto"}
]
}, },
{ {
type: "list", type: "list",
@ -137,6 +294,20 @@ function promptUser(callback) {
return answers.source === "guide"; return answers.source === "guide";
} }
}, },
{
type: "input",
name: "patterns",
message: "Which file(s), path(s), or glob(s) should be examined?",
when: function(answers) {
return (answers.source === "auto");
},
validate: function(input) {
if (input.trim().length === 0 && input.trim() !== ",") {
return "You must tell us what code to examine. Try again.";
}
return true;
}
},
{ {
type: "list", type: "list",
name: "format", name: "format",
@ -144,52 +315,40 @@ function promptUser(callback) {
default: "JavaScript", default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"], choices: ["JavaScript", "YAML", "JSON"],
when: function(answers) { when: function(answers) {
return answers.source === "guide"; return (answers.source === "guide" || answers.source === "auto");
} }
} }
], function(earlyAnswers) { ], function(earlyAnswers) {
// early exit if you are using a style guide // early exit if you are using a style guide
if (earlyAnswers.source === "guide") { if (earlyAnswers.source === "guide") {
writeFile(getConfigForStyleGuide(earlyAnswers.styleguide), earlyAnswers.format, callback); try {
config = getConfigForStyleGuide(earlyAnswers.styleguide);
writeFile(config, earlyAnswers.format);
} catch (err) {
callback(err);
return;
}
return; return;
} }
// continue with the style questions otherwise... // continue with the questions otherwise...
inquirer.prompt([ inquirer.prompt([
{
type: "list",
name: "indent",
message: "What style of indentation do you use?",
default: "tabs",
choices: [{name: "Tabs", value: "tab"}, {name: "Spaces", value: 4}]
},
{
type: "list",
name: "quotes",
message: "What quotes do you use for strings?",
default: "double",
choices: [{name: "Double", value: "double"}, {name: "Single", value: "single"}]
},
{
type: "list",
name: "linebreak",
message: "What line endings do you use?",
default: "unix",
choices: [{name: "Unix", value: "unix"}, {name: "Windows", value: "windows"}]
},
{
type: "confirm",
name: "semi",
message: "Do you require semicolons?",
default: true
},
{ {
type: "confirm", type: "confirm",
name: "es6", name: "es6",
message: "Are you using ECMAScript 6 features?", message: "Are you using ECMAScript 6 features?",
default: false default: false
}, },
{
type: "confirm",
name: "modules",
message: "Are you using ES6 modules?",
default: false,
when: function(answers) {
return answers.es6 === true;
}
},
{ {
type: "checkbox", type: "checkbox",
name: "env", name: "env",
@ -197,6 +356,17 @@ function promptUser(callback) {
default: ["browser"], default: ["browser"],
choices: [{name: "Node", value: "node"}, {name: "Browser", value: "browser"}] choices: [{name: "Node", value: "node"}, {name: "Browser", value: "browser"}]
}, },
{
type: "confirm",
name: "commonjs",
message: "Do you use CommonJS?",
default: false,
when: function(answers) {
return answers.env.some(function(env) {
return env === "browser";
});
}
},
{ {
type: "confirm", type: "confirm",
name: "jsx", name: "jsx",
@ -211,17 +381,75 @@ function promptUser(callback) {
when: function(answers) { when: function(answers) {
return answers.jsx; return answers.jsx;
} }
},
{
type: "list",
name: "format",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"]
} }
], function(answers) { ], function(secondAnswers) {
var config = processAnswers(answers);
writeFile(config, answers.format, callback); // early exit if you are using automatic style generation
if (earlyAnswers.source === "auto") {
try {
if (secondAnswers.jsx) {
log.error("Unfortunately, autoconfig does not yet work for JSX code.\nPlease see https://github.com/eslint/eslint/issues/5007 for current status.");
return;
}
var combinedAnswers = lodash.assign({}, earlyAnswers, secondAnswers);
config = processAnswers(combinedAnswers);
installModules(config);
writeFile(config, earlyAnswers.format);
} catch (err) {
callback(err);
return;
}
return;
}
// continue with the style questions otherwise...
inquirer.prompt([
{
type: "list",
name: "indent",
message: "What style of indentation do you use?",
default: "tabs",
choices: [{name: "Tabs", value: "tab"}, {name: "Spaces", value: 4}]
},
{
type: "list",
name: "quotes",
message: "What quotes do you use for strings?",
default: "double",
choices: [{name: "Double", value: "double"}, {name: "Single", value: "single"}]
},
{
type: "list",
name: "linebreak",
message: "What line endings do you use?",
default: "unix",
choices: [{name: "Unix", value: "unix"}, {name: "Windows", value: "windows"}]
},
{
type: "confirm",
name: "semi",
message: "Do you require semicolons?",
default: true
},
{
type: "list",
name: "format",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"]
}
], function(answers) {
try {
var totalAnswers = lodash.assign({}, earlyAnswers, secondAnswers, answers);
config = processAnswers(totalAnswers);
installModules(config);
writeFile(config, answers.format);
} catch (err) {
callback(err);
return;
}
return;
});
}); });
}); });
} }

24
tools/eslint/lib/config/config-ops.js

@ -11,9 +11,9 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var debug = require("debug"), var lodash = require("lodash"),
environments = require("../../conf/environments"), debug = require("debug"),
assign = require("object-assign"); Environments = require("./environments");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Private // Private
@ -36,7 +36,7 @@ module.exports = {
globals: {}, globals: {},
env: {}, env: {},
rules: {}, rules: {},
ecmaFeatures: {} parserOptions: {}
}; };
}, },
@ -57,16 +57,16 @@ module.exports = {
Object.keys(env).filter(function(name) { Object.keys(env).filter(function(name) {
return env[name]; return env[name];
}).forEach(function(name) { }).forEach(function(name) {
var environment = environments[name]; var environment = Environments.get(name);
if (environment) { if (environment) {
debug("Creating config for environment " + name); debug("Creating config for environment " + name);
if (environment.globals) { if (environment.globals) {
assign(envConfig.globals, environment.globals); lodash.assign(envConfig.globals, environment.globals);
} }
if (environment.ecmaFeatures) { if (environment.parserOptions) {
assign(envConfig.ecmaFeatures, environment.ecmaFeatures); lodash.assign(envConfig.parserOptions, environment.parserOptions);
} }
} }
}); });
@ -167,14 +167,10 @@ module.exports = {
Object.keys(src).forEach(function(key) { Object.keys(src).forEach(function(key) {
if (Array.isArray(src[key]) || Array.isArray(target[key])) { if (Array.isArray(src[key]) || Array.isArray(target[key])) {
dst[key] = deepmerge(target[key], src[key], key === "plugins", isRule); dst[key] = deepmerge(target[key], src[key], key === "plugins", isRule);
} else if (typeof src[key] !== "object" || !src[key]) { } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
dst[key] = src[key]; dst[key] = src[key];
} else { } else {
if (!target[key]) { dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules");
dst[key] = src[key];
} else {
dst[key] = deepmerge(target[key], src[key], combine, key === "rules");
}
} }
}); });
} }

308
tools/eslint/lib/config/config-rule.js

@ -0,0 +1,308 @@
/**
* @fileoverview Create configurations for a rule
* @author Ian VanSchooten
* @copyright 2016 Ian VanSchooten. All rights reserved.
* See LICENSE in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var rules = require("../rules"),
loadRules = require("../load-rules");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Wrap all of the elements of an array into arrays.
* @param {*[]} xs Any array.
* @returns {Array[]} An array of arrays.
*/
function explodeArray(xs) {
return xs.reduce(function(accumulator, x) {
accumulator.push([x]);
return accumulator;
}, []);
}
/**
* Mix two arrays such that each element of the second array is concatenated
* onto each element of the first array.
*
* For example:
* combineArrays([a, [b, c]], [x, y]); // -> [[a, x], [a, y], [b, c, x], [b, c, y]]
*
* @param {array} arr1 The first array to combine.
* @param {array} arr2 The second array to combine.
* @returns {array} A mixture of the elements of the first and second arrays.
*/
function combineArrays(arr1, arr2) {
var res = [];
if (arr1.length === 0) {
return explodeArray(arr2);
}
if (arr2.length === 0) {
return explodeArray(arr1);
}
arr1.forEach(function(x1) {
arr2.forEach(function(x2) {
res.push([].concat(x1, x2));
});
});
return res;
}
/**
* Group together valid rule configurations based on object properties
*
* e.g.:
* groupByProperty([
* {before: true},
* {before: false},
* {after: true},
* {after: false}
* ]);
*
* will return:
* [
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* ]
*
* @param {Object[]} objects Array of objects, each with one property/value pair
* @returns {Array[]} Array of arrays of objects grouped by property
*/
function groupByProperty(objects) {
var groupedObj = objects.reduce(function(accumulator, obj) {
var prop = Object.keys(obj)[0];
accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
return accumulator;
}, {});
return Object.keys(groupedObj).map(function(prop) {
return groupedObj[prop];
});
}
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Configuration settings for a rule.
*
* A configuration can be a single number (severity), or an array where the first
* element in the array is the severity, and is the only required element.
* Configs may also have one or more additional elements to specify rule
* configuration or options.
*
* @typedef {array|number} ruleConfig
* @param {number} 0 The rule's severity (0, 1, 2).
*/
/**
* Object whose keys are rule names and values are arrays of valid ruleConfig items
* which should be linted against the target source code to determine error counts.
* (a ruleConfigSet.ruleConfigs).
*
* e.g. rulesConfig = {
* "comma-dangle": [2, [2, "always"], [2, "always-multiline"], [2, "never"]],
* "no-console": [2]
* }
* @typedef rulesConfig
*/
/**
* Create valid rule configurations by combining two arrays,
* with each array containing multiple objects each with a
* single property/value pair and matching properties.
*
* e.g.:
* combinePropertyObjects(
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* );
*
* will return:
* [
* {before: true, after: true},
* {before: true, after: false},
* {before: false, after: true},
* {before: false, after: false}
* ]
*
* @param {Object[]} objArr1 Single key/value objects, all with the same key
* @param {Object[]} objArr2 Single key/value objects, all with another key
* @returns {Object[]} Combined objects for each combination of input properties and values
*/
function combinePropertyObjects(objArr1, objArr2) {
var res = [];
if (objArr1.length === 0) {
return objArr2;
}
if (objArr2.length === 0) {
return objArr1;
}
objArr1.forEach(function(obj1) {
objArr2.forEach(function(obj2) {
var combinedObj = {};
var obj1Props = Object.keys(obj1);
var obj2Props = Object.keys(obj2);
obj1Props.forEach(function(prop1) {
combinedObj[prop1] = obj1[prop1];
});
obj2Props.forEach(function(prop2) {
combinedObj[prop2] = obj2[prop2];
});
res.push(combinedObj);
});
});
return res;
}
/**
* Creates a new instance of a rule configuration set
*
* A rule configuration set is an array of configurations that are valid for a
* given rule. For example, the configuration set for the "semi" rule could be:
*
* ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
*
* @param {ruleConfig[]} configs Valid rule configurations
* @constructor
*/
function RuleConfigSet(configs) {
/**
* Stored valid rule configurations for this instance
* @type {array}
*/
this.ruleConfigs = configs || [];
}
RuleConfigSet.prototype = {
constructor: RuleConfigSet,
/**
* Add a severity level to the front of all configs in the instance.
* This should only be called after all configs have been added to the instance.
*
* @param {number} [severity=2] The level of severity for the rule (0, 1, 2)
* @returns {void}
*/
addErrorSeverity: function(severity) {
severity = severity || 2;
this.ruleConfigs = this.ruleConfigs.map(function(config) {
config.unshift(severity);
return config;
});
// Add a single config at the beginning consisting of only the severity
this.ruleConfigs.unshift(severity);
},
/**
* Add rule configs from an array of strings (schema enums)
* @param {string[]} enums Array of valid rule options (e.g. ["always", "never"])
* @returns {void}
*/
addEnums: function(enums) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, enums));
},
/**
* Add rule configurations from a schema object
* @param {Object} obj Schema item with type === "object"
* @returns {void}
*/
addObject: function(obj) {
var objectConfigSet = {
objectConfigs: [],
add: function(property, values) {
var optionObj;
for (var idx = 0; idx < values.length; idx++) {
optionObj = {};
optionObj[property] = values[idx];
this.objectConfigs.push(optionObj);
}
},
combine: function() {
this.objectConfigs = groupByProperty(this.objectConfigs).reduce(function(accumulator, objArr) {
return combinePropertyObjects(accumulator, objArr);
}, []);
}
};
// The object schema could have multiple independent properties.
// If any contain enums or booleans, they can be added and then combined
Object.keys(obj.properties).forEach(function(prop) {
if (obj.properties[prop].enum) {
objectConfigSet.add(prop, obj.properties[prop].enum);
}
if (obj.properties[prop].type && obj.properties[prop].type === "boolean") {
objectConfigSet.add(prop, [true, false]);
}
});
objectConfigSet.combine();
if (objectConfigSet.objectConfigs.length > 0) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
}
}
};
/**
* Generate valid rule configurations based on a schema object
* @param {Object} schema A rule's schema object
* @returns {array[]} Valid rule configurations
*/
function generateConfigsFromSchema(schema) {
var configSet = new RuleConfigSet();
if (Array.isArray(schema)) {
schema.forEach(function(opt) {
if (opt.enum) {
configSet.addEnums(opt.enum);
}
if (opt.type && opt.type === "object") {
configSet.addObject(opt);
}
if (opt.oneOf) {
// TODO (IanVS): not yet implemented
}
});
}
configSet.addErrorSeverity();
return configSet.ruleConfigs;
}
/**
* Generate possible rule configurations for all of the core rules
* @returns {rulesConfig} Hash of rule names and arrays of possible configurations
*/
function createCoreRuleConfigs() {
var ruleList = loadRules();
return Object.keys(ruleList).reduce(function(accumulator, id) {
var rule = rules.get(id);
var schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
accumulator[id] = generateConfigsFromSchema(schema);
return accumulator;
}, {});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
generateConfigsFromSchema: generateConfigsFromSchema,
createCoreRuleConfigs: createCoreRuleConfigs
};

91
tools/eslint/lib/config/config-validator.js

@ -2,6 +2,7 @@
* @fileoverview Validates configs. * @fileoverview Validates configs.
* @author Brandon Mills * @author Brandon Mills
* @copyright 2015 Brandon Mills * @copyright 2015 Brandon Mills
* See LICENSE file in root directory for full license.
*/ */
"use strict"; "use strict";
@ -11,7 +12,7 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var rules = require("../rules"), var rules = require("../rules"),
environments = require("../../conf/environments"), Environments = require("./environments"),
schemaValidator = require("is-my-json-valid"); schemaValidator = require("is-my-json-valid");
var validators = { var validators = {
@ -29,36 +30,28 @@ var validators = {
*/ */
function getRuleOptionsSchema(id) { function getRuleOptionsSchema(id) {
var rule = rules.get(id), var rule = rules.get(id),
schema = rule && rule.schema; schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
if (!schema) {
return {
"type": "array",
"items": [
{
"enum": [0, 1, 2]
}
],
"minItems": 1
};
}
// Given a tuple of schemas, insert warning level at the beginning // Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(schema)) { if (Array.isArray(schema)) {
return { if (schema.length) {
"type": "array", return {
"items": [ "type": "array",
{ "items": schema,
"enum": [0, 1, 2] "minItems": 0,
} "maxItems": schema.length
].concat(schema), };
"minItems": 1, } else {
"maxItems": schema.length + 1 return {
}; "type": "array",
"minItems": 0,
"maxItems": 0
};
}
} }
// Given a full schema, leave it alone // Given a full schema, leave it alone
return schema; return schema || null;
} }
/** /**
@ -70,34 +63,50 @@ function getRuleOptionsSchema(id) {
*/ */
function validateRuleOptions(id, options, source) { function validateRuleOptions(id, options, source) {
var validateRule = validators.rules[id], var validateRule = validators.rules[id],
message; message,
severity,
if (!validateRule) { localOptions,
validateRule = schemaValidator(getRuleOptionsSchema(id), { verbose: true }); schema = getRuleOptionsSchema(id),
validSeverity = true;
if (!validateRule && schema) {
validateRule = schemaValidator(schema, { verbose: true });
validators.rules[id] = validateRule; validators.rules[id] = validateRule;
} }
if (typeof options === "number") { // if it's not an array, it should be just a severity
options = [options]; if (Array.isArray(options)) {
localOptions = options.concat(); // clone
severity = localOptions.shift();
} else {
severity = options;
localOptions = [];
} }
validateRule(options); validSeverity = (severity === 0 || severity === 1 || severity === 2);
if (validateRule) {
validateRule(localOptions);
}
if (validateRule.errors) { if ((validateRule && validateRule.errors) || !validSeverity) {
message = [ message = [
source, ":\n", source, ":\n",
"\tConfiguration for rule \"", id, "\" is invalid:\n" "\tConfiguration for rule \"", id, "\" is invalid:\n"
]; ];
validateRule.errors.forEach(function(error) {
if (error.field === "data[\"0\"]") { // better error for severity if (!validSeverity) {
message.push( message.push(
"\tSeverity should be one of the following: 0 = off, 1 = warning, 2 = error (you passed \"", error.value, "\").\n"); "\tSeverity should be one of the following: 0 = off, 1 = warning, 2 = error (you passed \"", severity, "\").\n");
} else { }
if (validateRule && validateRule.errors) {
validateRule.errors.forEach(function(error) {
message.push( message.push(
"\tValue \"", error.value, "\" ", error.message, ".\n" "\tValue \"", error.value, "\" ", error.message, ".\n"
); );
} });
}); }
throw new Error(message.join("")); throw new Error(message.join(""));
} }
@ -122,7 +131,7 @@ function validateEnvironment(environment, source) {
if (typeof environment === "object") { if (typeof environment === "object") {
Object.keys(environment).forEach(function(env) { Object.keys(environment).forEach(function(env) {
if (!environments[env]) { if (!Environments.get(env)) {
var message = [ var message = [
source, ":\n", source, ":\n",
"\tEnvironment key \"", env, "\" is unknown\n" "\tEnvironment key \"", env, "\" is unknown\n"

87
tools/eslint/lib/config/environments.js

@ -0,0 +1,87 @@
/**
* @fileoverview Environments manager
* @author Nicholas C. Zakas
* @copyright 2016 Nicholas C. Zakas. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var debug = require("debug"),
envs = require("../../conf/environments");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
debug = debug("eslint:enviroments");
var environments = Object.create(null);
/**
* Loads the default environments.
* @returns {void}
* @private
*/
function load() {
Object.keys(envs).forEach(function(envName) {
environments[envName] = envs[envName];
});
}
// always load default environments upfront
load();
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
load: load,
/**
* Gets the environment with the given name.
* @param {string} name The name of the environment to retrieve.
* @returns {Object?} The environment object or null if not found.
*/
get: function(name) {
return environments[name] || null;
},
/**
* Defines an environment.
* @param {string} name The name of the environment.
* @param {Object} env The environment settings.
* @returns {void}
*/
define: function(name, env) {
environments[name] = env;
},
/**
* Imports all environments from a plugin.
* @param {Object} plugin The plugin object.
* @param {string} pluginName The name of the plugin.
* @returns {void}
*/
importPlugin: function(plugin, pluginName) {
if (plugin.environments) {
Object.keys(plugin.environments).forEach(function(envName) {
this.define(pluginName + "/" + envName, plugin.environments[envName]);
}, this);
}
},
/**
* Resets all environments. Only use for tests!
* @returns {void}
*/
testReset: function() {
environments = Object.create(null);
load();
}
};

144
tools/eslint/lib/config/plugins.js

@ -0,0 +1,144 @@
/**
* @fileoverview Plugins manager
* @author Nicholas C. Zakas
* @copyright 2016 Nicholas C. Zakas. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var debug = require("debug"),
Environments = require("./environments"),
rules = require("../rules");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
debug = debug("eslint:plugins");
var plugins = Object.create(null);
var PLUGIN_NAME_PREFIX = "eslint-plugin-",
NAMESPACE_REGEX = /^@.*\//i;
/**
* Removes the prefix `eslint-plugin-` from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without prefix.
*/
function removePrefix(pluginName) {
return pluginName.indexOf(PLUGIN_NAME_PREFIX) === 0 ? pluginName.substring(PLUGIN_NAME_PREFIX.length) : pluginName;
}
/**
* Gets the scope (namespace) of a plugin.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugins namepace if it has one.
*/
function getNamespace(pluginName) {
return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
}
/**
* Removes the namespace from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without the namespace.
*/
function removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
removePrefix: removePrefix,
getNamespace: getNamespace,
removeNamespace: removeNamespace,
/**
* Defines a plugin with a given name rather than loading from disk.
* @param {string} pluginName The name of the plugin to load.
* @param {Object} plugin The plugin object.
* @returns {void}
*/
define: function(pluginName, plugin) {
var pluginNameWithoutNamespace = removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace);
plugins[pluginNameWithoutPrefix] = plugin;
// load up environments and rules
Environments.importPlugin(plugin, pluginNameWithoutPrefix);
if (plugin.rules) {
rules.import(plugin.rules, pluginNameWithoutPrefix);
}
},
/**
* Gets a plugin with the given name.
* @param {string} pluginName The name of the plugin to retrieve.
* @returns {Object} The plugin or null if not loaded.
*/
get: function(pluginName) {
return plugins[pluginName] || null;
},
/**
* Returns all plugins that are loaded.
* @returns {Object} The plugins cache.
*/
getAll: function() {
return plugins;
},
/**
* Loads a plugin with the given name.
* @param {string} pluginName The name of the plugin to load.
* @returns {void}
* @throws {Error} If the plugin cannot be loaded.
*/
load: function(pluginName) {
var pluginNamespace = getNamespace(pluginName),
pluginNameWithoutNamespace = removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace),
plugin = null;
if (!plugins[pluginNameWithoutPrefix]) {
try {
plugin = require(pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix);
} catch (err) {
debug("Failed to load plugin eslint-plugin-" + pluginNameWithoutPrefix + ". Proceeding without it.");
err.message = "Failed to load plugin " + pluginName + ": " + err.message;
throw err;
}
this.define(pluginName, plugin);
}
},
/**
* Loads all plugins from an array.
* @param {string[]} pluginNames An array of plugins names.
* @returns {void}
* @throws {Error} If a plugin cannot be loaded.
*/
loadAll: function(pluginNames) {
pluginNames.forEach(this.load, this);
},
/**
* Resets plugin information. Use for tests only.
* @returns {void}
*/
testReset: function() {
plugins = Object.create(null);
}
};

197
tools/eslint/lib/eslint.js

@ -10,11 +10,11 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var estraverse = require("./util/estraverse"), var lodash = require("lodash"),
estraverse = require("./util/estraverse"),
escope = require("escope"), escope = require("escope"),
environments = require("../conf/environments"), Environments = require("./config/environments"),
blankScriptAST = require("../conf/blank-script.json"), blankScriptAST = require("../conf/blank-script.json"),
assign = require("object-assign"),
rules = require("./rules"), rules = require("./rules"),
RuleContext = require("./rule-context"), RuleContext = require("./rule-context"),
timing = require("./timing"), timing = require("./timing"),
@ -25,7 +25,8 @@ var estraverse = require("./util/estraverse"),
ConfigOps = require("./config/config-ops"), ConfigOps = require("./config/config-ops"),
validator = require("./config/config-validator"), validator = require("./config/config-validator"),
replacements = require("../conf/replacements.json"), replacements = require("../conf/replacements.json"),
assert = require("assert"); assert = require("assert"),
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer");
var DEFAULT_PARSER = require("../conf/eslint.json").parser; var DEFAULT_PARSER = require("../conf/eslint.json").parser;
@ -81,8 +82,10 @@ function parseJsonConfig(string, location, messages) {
} catch (ex) { } catch (ex) {
messages.push({ messages.push({
ruleId: null,
fatal: true, fatal: true,
severity: 2, severity: 2,
source: null,
message: "Failed to parse JSON from '" + string + "': " + ex.message, message: "Failed to parse JSON from '" + string + "': " + ex.message,
line: location.start.line, line: location.start.line,
column: location.start.column + 1 column: location.start.column + 1
@ -125,22 +128,23 @@ function addDeclaredGlobals(program, globalScope, config) {
var declaredGlobals = {}, var declaredGlobals = {},
exportedGlobals = {}, exportedGlobals = {},
explicitGlobals = {}, explicitGlobals = {},
builtin = environments.builtin; builtin = Environments.get("builtin");
assign(declaredGlobals, builtin); lodash.assign(declaredGlobals, builtin);
Object.keys(config.env).forEach(function(name) { Object.keys(config.env).forEach(function(name) {
if (config.env[name]) { if (config.env[name]) {
var environmentGlobals = environments[name] && environments[name].globals; var env = Environments.get(name),
environmentGlobals = env && env.globals;
if (environmentGlobals) { if (environmentGlobals) {
assign(declaredGlobals, environmentGlobals); lodash.assign(declaredGlobals, environmentGlobals);
} }
} }
}); });
assign(exportedGlobals, config.exported); lodash.assign(exportedGlobals, config.exported);
assign(declaredGlobals, config.globals); lodash.assign(declaredGlobals, config.globals);
assign(explicitGlobals, config.astGlobals); lodash.assign(explicitGlobals, config.astGlobals);
Object.keys(declaredGlobals).forEach(function(name) { Object.keys(declaredGlobals).forEach(function(name) {
var variable = globalScope.set.get(name); var variable = globalScope.set.get(name);
@ -172,6 +176,21 @@ function addDeclaredGlobals(program, globalScope, config) {
variable.eslintUsed = true; variable.eslintUsed = true;
} }
}); });
// "through" contains all references that their definition cannot be found.
// Since we augment the global scope using configuration, we need to update references and remove the ones that were added by configuration.
globalScope.through = globalScope.through.filter(function(reference) {
var name = reference.identifier.name;
var variable = globalScope.set.get(name);
if (variable) {
// Links the variable and the reference.
// And this reference is removed from `Scope#through`.
reference.resolved = variable;
variable.references.push(reference);
return false;
}
return true;
});
} }
/** /**
@ -269,16 +288,16 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
if (comment.type === "Block") { if (comment.type === "Block") {
switch (match[1]) { switch (match[1]) {
case "exported": case "exported":
assign(commentConfig.exported, parseBooleanConfig(value, comment)); lodash.assign(commentConfig.exported, parseBooleanConfig(value, comment));
break; break;
case "globals": case "globals":
case "global": case "global":
assign(commentConfig.astGlobals, parseBooleanConfig(value, comment)); lodash.assign(commentConfig.astGlobals, parseBooleanConfig(value, comment));
break; break;
case "eslint-env": case "eslint-env":
assign(commentConfig.env, parseListConfig(value)); lodash.assign(commentConfig.env, parseListConfig(value));
break; break;
case "eslint-disable": case "eslint-disable":
@ -312,11 +331,12 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
// apply environment configs // apply environment configs
Object.keys(commentConfig.env).forEach(function(name) { Object.keys(commentConfig.env).forEach(function(name) {
if (environments[name]) { var env = Environments.get(name);
commentConfig = ConfigOps.merge(commentConfig, environments[name]); if (env) {
commentConfig = ConfigOps.merge(commentConfig, env);
} }
}); });
assign(commentConfig.rules, commentRules); lodash.assign(commentConfig.rules, commentRules);
return ConfigOps.merge(config, commentConfig); return ConfigOps.merge(config, commentConfig);
} }
@ -354,7 +374,7 @@ function prepareConfig(config) {
delete config.global; delete config.global;
var copiedRules = {}, var copiedRules = {},
ecmaFeatures = {}, parserOptions = {},
preparedConfig; preparedConfig;
if (typeof config.rules === "object") { if (typeof config.rules === "object") {
@ -371,11 +391,12 @@ function prepareConfig(config) {
}); });
} }
// merge in environment ecmaFeatures // merge in environment parserOptions
if (typeof config.env === "object") { if (typeof config.env === "object") {
Object.keys(config.env).forEach(function(env) { Object.keys(config.env).forEach(function(envName) {
if (config.env[env] && environments[env] && environments[env].ecmaFeatures) { var env = Environments.get(envName);
assign(ecmaFeatures, environments[env].ecmaFeatures); if (config.env[envName] && env && env.parserOptions) {
parserOptions = ConfigOps.merge(parserOptions, env.parserOptions);
} }
}); });
} }
@ -386,12 +407,21 @@ function prepareConfig(config) {
globals: ConfigOps.merge({}, config.globals), globals: ConfigOps.merge({}, config.globals),
env: ConfigOps.merge({}, config.env || {}), env: ConfigOps.merge({}, config.env || {}),
settings: ConfigOps.merge({}, config.settings || {}), settings: ConfigOps.merge({}, config.settings || {}),
ecmaFeatures: ConfigOps.merge(ecmaFeatures, config.ecmaFeatures || {}) parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {})
}; };
// can't have global return inside of modules if (preparedConfig.parserOptions.sourceType === "module") {
if (preparedConfig.ecmaFeatures.modules) { if (!preparedConfig.parserOptions.ecmaFeatures) {
preparedConfig.ecmaFeatures.globalReturn = false; preparedConfig.parserOptions.ecmaFeatures = {};
}
// can't have global return inside of modules
preparedConfig.parserOptions.ecmaFeatures.globalReturn = false;
// also need at least ES6 six for modules
if (!preparedConfig.parserOptions.ecmaVersion || preparedConfig.parserOptions.ecmaVersion < 6) {
preparedConfig.parserOptions.ecmaVersion = 6;
}
} }
return preparedConfig; return preparedConfig;
@ -434,6 +464,7 @@ function getRuleReplacementMessage(ruleId) {
var newRules = replacements.rules[ruleId]; var newRules = replacements.rules[ruleId];
return "Rule \'" + ruleId + "\' was removed and replaced by: " + newRules.join(", "); return "Rule \'" + ruleId + "\' was removed and replaced by: " + newRules.join(", ");
} }
return null;
} }
var eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//g; var eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//g;
@ -448,12 +479,28 @@ function findEslintEnv(text) {
eslintEnvPattern.lastIndex = 0; eslintEnvPattern.lastIndex = 0;
while ((match = eslintEnvPattern.exec(text))) { while ((match = eslintEnvPattern.exec(text))) {
retv = assign(retv || {}, parseListConfig(match[1])); retv = lodash.assign(retv || {}, parseListConfig(match[1]));
} }
return retv; return retv;
} }
/**
* Strips Unicode BOM from a given text.
*
* @param {string} text - A text to strip.
* @returns {string} The stripped text.
*/
function stripUnicodeBOM(text) {
// Check Unicode BOM.
// In JavaScript, string data is stored as UTF-16, so BOM is 0xFEFF.
// http://www.ecma-international.org/ecma-262/6.0/#sec-unicode-format-control-characters
if (text.charCodeAt(0) === 0xFEFF) {
return text.slice(1);
}
return text;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -486,14 +533,24 @@ module.exports = (function() {
*/ */
function parse(text, config) { function parse(text, config) {
var parser; var parser,
parserOptions = {
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
attachComment: true
};
try { try {
parser = require(config.parser); parser = require(config.parser);
} catch (ex) { } catch (ex) {
messages.push({ messages.push({
ruleId: null,
fatal: true, fatal: true,
severity: 2, severity: 2,
source: null,
message: ex.message, message: ex.message,
line: 0, line: 0,
column: 0 column: 0
@ -502,6 +559,11 @@ module.exports = (function() {
return null; return null;
} }
// merge in any additional parser options
if (config.parserOptions) {
parserOptions = lodash.assign({}, config.parserOptions, parserOptions);
}
/* /*
* Check for parsing errors first. If there's a parsing error, nothing * Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error * else can happen. However, a parsing error does not throw an error
@ -509,28 +571,22 @@ module.exports = (function() {
* problem that ESLint identified just like any other. * problem that ESLint identified just like any other.
*/ */
try { try {
return parser.parse(text, { return parser.parse(text, parserOptions);
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
attachComment: true,
ecmaFeatures: config.ecmaFeatures
});
} catch (ex) { } catch (ex) {
// If the message includes a leading line number, strip it: // If the message includes a leading line number, strip it:
var message = ex.message.replace(/^line \d+:/i, "").trim(); var message = ex.message.replace(/^line \d+:/i, "").trim();
var source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
messages.push({ messages.push({
ruleId: null,
fatal: true, fatal: true,
severity: 2, severity: 2,
source: source,
message: "Parsing error: " + message, message: "Parsing error: " + message,
line: ex.lineNumber, line: ex.lineNumber,
column: ex.column + 1 column: ex.column
}); });
return null; return null;
@ -585,10 +641,21 @@ module.exports = (function() {
sourceCode = null; sourceCode = null;
}; };
/**
* Configuration object for the `verify` API. A JS representation of the eslintrc files.
* @typedef {Object} ESLintConfig
* @property {Object} rules The rule configuration to verify against.
* @property {string} [parser] Parser to use when generatig the AST.
* @property {Object} [parserOptions] Options for the parsed used.
* @property {Object} [settings] Global settings passed to each rule.
* @property {Object} [env] The environment to verify in.
* @property {Object} [globals] Available globalsto the code.
*/
/** /**
* Verifies the text against the rules specified by the second argument. * Verifies the text against the rules specified by the second argument.
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
* @param {Object} config An object whose keys specify the rules to use. * @param {ESLintConfig} config An ESLintConfig instance to configure everything.
* @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked. * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
* If this is not set, the filename will default to '<input>' in the rule context. If * If this is not set, the filename will default to '<input>' in the rule context. If
* an object, then it has "filename", "saveState", and "allowInlineConfig" properties. * an object, then it has "filename", "saveState", and "allowInlineConfig" properties.
@ -624,10 +691,10 @@ module.exports = (function() {
var envInFile = findEslintEnv(text || textOrSourceCode.text); var envInFile = findEslintEnv(text || textOrSourceCode.text);
if (envInFile) { if (envInFile) {
if (!config || !config.env) { if (!config || !config.env) {
config = assign({}, config || {}, {env: envInFile}); config = lodash.assign({}, config || {}, {env: envInFile});
} else { } else {
config = assign({}, config); config = lodash.assign({}, config);
config.env = assign({}, config.env, envInFile); config.env = lodash.assign({}, config.env, envInFile);
} }
} }
@ -636,17 +703,19 @@ module.exports = (function() {
// only do this for text // only do this for text
if (text !== null) { if (text !== null) {
// there's no input, just exit here // there's no input, just exit here
if (text.trim().length === 0) { if (text.trim().length === 0) {
sourceCode = new SourceCode(text, blankScriptAST); sourceCode = new SourceCode(text, blankScriptAST);
return messages; return messages;
} }
ast = parse(text.replace(/^#!([^\r\n]+)/, function(match, captured) { ast = parse(
shebang = captured; stripUnicodeBOM(text).replace(/^#!([^\r\n]+)/, function(match, captured) {
return "//" + captured; shebang = captured;
}), config); return "//" + captured;
}),
config
);
if (ast) { if (ast) {
sourceCode = new SourceCode(text, ast); sourceCode = new SourceCode(text, ast);
@ -689,10 +758,11 @@ module.exports = (function() {
options = getRuleOptions(config.rules[key]); options = getRuleOptions(config.rules[key]);
try { try {
rule = ruleCreator(new RuleContext( var ruleContext = new RuleContext(
key, api, severity, options, key, api, severity, options,
config.settings, config.ecmaFeatures config.settings, config.parserOptions, config.parser, ruleCreator.meta);
)); rule = ruleCreator.create ? ruleCreator.create(ruleContext) :
ruleCreator(ruleContext);
// add all the node types as listeners // add all the node types as listeners
Object.keys(rule).forEach(function(nodeType) { Object.keys(rule).forEach(function(nodeType) {
@ -711,18 +781,16 @@ module.exports = (function() {
currentConfig = config; currentConfig = config;
controller = new estraverse.Controller(); controller = new estraverse.Controller();
ecmaFeatures = currentConfig.ecmaFeatures; ecmaFeatures = currentConfig.parserOptions.ecmaFeatures || {};
ecmaVersion = (ecmaFeatures.blockBindings || ecmaFeatures.classes || ecmaVersion = currentConfig.parserOptions.ecmaVersion || 5;
ecmaFeatures.modules || ecmaFeatures.defaultParams ||
ecmaFeatures.destructuring) ? 6 : 5;
// gather data that may be needed by the rules // gather data that may be needed by the rules
scopeManager = escope.analyze(ast, { scopeManager = escope.analyze(ast, {
ignoreEval: true, ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn, nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion: ecmaVersion, ecmaVersion: ecmaVersion,
sourceType: ecmaFeatures.modules ? "module" : "script" sourceType: currentConfig.parserOptions.sourceType || "script"
}); });
currentScopes = scopeManager.scopes; currentScopes = scopeManager.scopes;
@ -754,6 +822,7 @@ module.exports = (function() {
} }
var eventGenerator = new NodeEventGenerator(api); var eventGenerator = new NodeEventGenerator(api);
eventGenerator = new CodePathAnalyzer(eventGenerator);
eventGenerator = new CommentEventGenerator(eventGenerator, sourceCode); eventGenerator = new CommentEventGenerator(eventGenerator, sourceCode);
/* /*
@ -770,7 +839,6 @@ module.exports = (function() {
eventGenerator.leaveNode(node); eventGenerator.leaveNode(node);
} }
}); });
} }
// sort by line and column // sort by line and column
@ -799,9 +867,10 @@ module.exports = (function() {
* @param {Object} opts Optional template data which produces a formatted message * @param {Object} opts Optional template data which produces a formatted message
* with symbols being replaced by this object's values. * with symbols being replaced by this object's values.
* @param {Object} fix A fix command description. * @param {Object} fix A fix command description.
* @param {Object} meta Metadata of the rule
* @returns {void} * @returns {void}
*/ */
api.report = function(ruleId, severity, node, location, message, opts, fix) { api.report = function(ruleId, severity, node, location, message, opts, fix, meta) {
if (node) { if (node) {
assert.strictEqual(typeof node, "object", "Node must be an object"); assert.strictEqual(typeof node, "object", "Node must be an object");
} }
@ -809,6 +878,7 @@ module.exports = (function() {
if (typeof location === "string") { if (typeof location === "string") {
assert.ok(node, "Node must be provided when reporting error if location is not provided"); assert.ok(node, "Node must be provided when reporting error if location is not provided");
meta = fix;
fix = opts; fix = opts;
opts = message; opts = message;
message = location; message = location;
@ -841,8 +911,8 @@ module.exports = (function() {
source: sourceCode.lines[location.line - 1] || "" source: sourceCode.lines[location.line - 1] || ""
}; };
// ensure there's range and text properties, otherwise it's not a valid fix // ensure there's range and text properties as well as metadata switch, otherwise it's not a valid fix
if (fix && Array.isArray(fix.range) && (typeof fix.text === "string")) { if (fix && Array.isArray(fix.range) && (typeof fix.text === "string") && (!meta || !meta.docs || meta.docs.fixable)) {
problem.fix = fix; problem.fix = fix;
} }
@ -912,7 +982,7 @@ module.exports = (function() {
// if current node introduces a scope, add it to the list // if current node introduces a scope, add it to the list
var current = controller.current(); var current = controller.current();
if (currentConfig.ecmaFeatures.blockBindings) { if (currentConfig.parserOptions.ecmaVersion >= 6) {
if (["BlockStatement", "SwitchStatement", "CatchClause", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].indexOf(current.type) >= 0) { if (["BlockStatement", "SwitchStatement", "CatchClause", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].indexOf(current.type) >= 0) {
parents.push(current); parents.push(current);
} }
@ -950,7 +1020,8 @@ module.exports = (function() {
*/ */
api.markVariableAsUsed = function(name) { api.markVariableAsUsed = function(name) {
var scope = this.getScope(), var scope = this.getScope(),
specialScope = currentConfig.ecmaFeatures.globalReturn || currentConfig.ecmaFeatures.modules, hasGlobalReturn = currentConfig.parserOptions.ecmaFeatures && currentConfig.parserOptions.ecmaFeatures.globalReturn,
specialScope = hasGlobalReturn || currentConfig.parserOptions.sourceType === "module",
variables, variables,
i, i,
len; len;

73
tools/eslint/lib/file-finder.js

@ -40,20 +40,42 @@ function getDirectoryEntries(directory) {
/** /**
* FileFinder * FileFinder
* @constructor * @constructor
* @param {...string} arguments The basename(s) of the file(s) to find. * @param {string[]} files The basename(s) of the file(s) to find.
* @param {stirng} cwd Current working directory
*/ */
function FileFinder() { function FileFinder(files, cwd) {
this.fileNames = Array.prototype.slice.call(arguments); this.fileNames = Array.isArray(files) ? files : [files];
this.cwd = cwd || process.cwd();
this.cache = {}; this.cache = {};
} }
/**
* Create a hash of filenames from a directory listing
* @param {string[]} entries Array of directory entries.
* @param {string} directory Path to a current directory.
* @param {string[]} supportedConfigs List of support filenames.
* @returns {Object} Hashmap of filenames
*/
function normalizeDirectoryEntries(entries, directory, supportedConfigs) {
var fileHash = {};
entries.forEach(function(entry) {
if (supportedConfigs.indexOf(entry) >= 0) {
var resolvedEntry = path.resolve(directory, entry);
if (fs.statSync(resolvedEntry).isFile()) {
fileHash[entry] = resolvedEntry;
}
}
});
return fileHash;
}
/** /**
* Find one instance of a specified file name in directory or in a parent directory. * Find one instance of a specified file name in directory or in a parent directory.
* Cache the results. * Cache the results.
* Does not check if a matching directory entry is a file, and intentionally * Does not check if a matching directory entry is a file, and intentionally
* only searches for the first file name in this.fileNames. * only searches for the first file name in this.fileNames.
* Is currently used by lib/ignored_paths.js to find an .eslintignore file. * Is currently used by lib/ignored_paths.js to find an .eslintignore file.
* @param {string} directory The directory to start the search from. * @param {string} directory The directory to start the search from.
* @returns {string} Path of the file found, or an empty string if not found. * @returns {string} Path of the file found, or an empty string if not found.
*/ */
FileFinder.prototype.findInDirectoryOrParents = function(directory) { FileFinder.prototype.findInDirectoryOrParents = function(directory) {
@ -62,12 +84,11 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) {
dirs, dirs,
filePath, filePath,
i, i,
name,
names, names,
searched; searched;
if (!directory) { if (!directory) {
directory = process.cwd(); directory = this.cwd;
} }
if (cache.hasOwnProperty(directory)) { if (cache.hasOwnProperty(directory)) {
@ -76,18 +97,18 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) {
dirs = []; dirs = [];
searched = 0; searched = 0;
name = this.fileNames[0]; names = this.fileNames;
names = Array.isArray(name) ? name : [name];
(function() { (function() {
while (directory !== child) { while (directory !== child) {
dirs[searched++] = directory; dirs[searched++] = directory;
var filesMap = normalizeDirectoryEntries(getDirectoryEntries(directory), directory, names);
for (var k = 0, found = false; k < names.length && !found; k++) { if (Object.keys(filesMap).length) {
for (var k = 0; k < names.length; k++) {
if (getDirectoryEntries(directory).indexOf(names[k]) !== -1 && fs.statSync(path.resolve(directory, names[k])).isFile()) { if (filesMap[names[k]]) {
filePath = path.resolve(directory, names[k]); filePath = filesMap[names[k]];
return; return;
}
} }
} }
@ -118,16 +139,14 @@ FileFinder.prototype.findAllInDirectoryAndParents = function(directory) {
var cache = this.cache, var cache = this.cache,
child, child,
dirs, dirs,
name,
fileNames, fileNames,
fileNamesCount,
filePath, filePath,
i, i,
j, j,
searched; searched;
if (!directory) { if (!directory) {
directory = process.cwd(); directory = this.cwd;
} }
if (cache.hasOwnProperty(directory)) { if (cache.hasOwnProperty(directory)) {
@ -137,33 +156,27 @@ FileFinder.prototype.findAllInDirectoryAndParents = function(directory) {
dirs = []; dirs = [];
searched = 0; searched = 0;
fileNames = this.fileNames; fileNames = this.fileNames;
fileNamesCount = fileNames.length;
do { do {
dirs[searched++] = directory; dirs[searched++] = directory;
cache[directory] = []; cache[directory] = [];
for (i = 0; i < fileNamesCount; i++) { var filesMap = normalizeDirectoryEntries(getDirectoryEntries(directory), directory, fileNames);
name = fileNames[i];
// convert to an array for easier handling if (Object.keys(filesMap).length) {
if (!Array.isArray(name)) { for (var k = 0; k < fileNames.length; k++) {
name = [name];
}
for (var k = 0, found = false; k < name.length && !found; k++) { if (filesMap[fileNames[k]]) {
filePath = filesMap[fileNames[k]];
if (getDirectoryEntries(directory).indexOf(name[k]) !== -1 && fs.statSync(path.resolve(directory, name[k])).isFile()) {
filePath = path.resolve(directory, name[k]);
found = true;
// Add the file path to the cache of each directory searched. // Add the file path to the cache of each directory searched.
for (j = 0; j < searched; j++) { for (j = 0; j < searched; j++) {
cache[dirs[j]].push(filePath); cache[dirs[j]].push(filePath);
} }
break;
} }
} }
} }
child = directory; child = directory;

3
tools/eslint/lib/formatters/checkstyle.js

@ -41,7 +41,8 @@ function xmlEscape(s) {
return "&quot;"; return "&quot;";
case "'": case "'":
return "&apos;"; return "&apos;";
// no default default:
throw new Error("unreachable");
} }
}); });
} }

8
tools/eslint/lib/formatters/html-template-message.html

@ -0,0 +1,8 @@
<tr style="display:none" class="f-<%= parentIndex %>">
<td><%= lineNumber %>:<%= columnNumber %></td>
<td class="clr-<%= severityNumber %>"><%= severityName %></td>
<td><%- message %></td>
<td>
<a href="http://eslint.org/docs/rules/<%= ruleId %>" target="_blank"><%= ruleId %></a>
</td>
</tr>

113
tools/eslint/lib/formatters/html-template-page.html

@ -0,0 +1,113 @@
<!DOCTYPE html>
<head>
<title>ESLint Report</title>
<style>
body {
font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size:16px;
font-weight:normal;
margin:0;
padding:0;
color:#333
}
#overview {
padding:20px 30px
}
td, th {
padding:5px 10px
}
h1 {
margin:0
}
table {
margin:30px;
width:calc(100% - 60px);
max-width:1000px;
border-radius:5px;
border:1px solid #ddd;
border-spacing:0px;
}
th {
font-weight:400;
font-size:normal;
text-align:left;
cursor:pointer
}
td.clr-1, td.clr-2, th span {
font-weight:700
}
th span {
float:right;
margin-left:20px
}
th span:after {
content:"";
clear:both;
display:block
}
tr:last-child td {
border-bottom:none
}
tr td:first-child, tr td:last-child {
color:#9da0a4
}
#overview.bg-0, tr.bg-0 th {
color:#468847;
background:#dff0d8;
border-bottom:1px solid #d6e9c6
}
#overview.bg-1, tr.bg-1 th {
color:#f0ad4e;
background:#fcf8e3;
border-bottom:1px solid #fbeed5
}
#overview.bg-2, tr.bg-2 th {
color:#b94a48;
background:#f2dede;
border-bottom:1px solid #eed3d7
}
td {
border-bottom:1px solid #ddd
}
td.clr-1 {
color:#f0ad4e
}
td.clr-2 {
color:#b94a48
}
td a {
color:#3a33d1;
text-decoration:none
}
td a:hover {
color:#272296;
text-decoration:underline
}
</style>
</head>
<body>
<div id="overview" class="bg-<%= reportColor %>">
<h1>ESLint Report</h1>
<div>
<span><%= reportSummary %></span> - Generated on <%= date %>
</div>
</div>
<table>
<tbody>
<%= results %>
</tbody>
</table>
<script type="text/javascript">
var groups = document.querySelectorAll("tr[data-group]");
for (i = 0; i < groups.length; i++) {
groups[i].addEventListener("click", function() {
var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
for (var j = 0; j < inGroup.length; j++) {
inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
}
});
}
</script>
</body>
</html>

6
tools/eslint/lib/formatters/html-template-result.html

@ -0,0 +1,6 @@
<tr class="bg-<%- color %>" data-group="f-<%- index %>">
<th colspan="4">
[+] <%- filePath %>
<span><%- summary %></span>
</th>
</tr>

130
tools/eslint/lib/formatters/html-template.html

@ -1,130 +0,0 @@
<!DOCTYPE html>
<head>
<title>ESLint Report</title>
<style>
body {
font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size:16px;
font-weight:normal;
margin:0;
padding:0;
color:#333
}
#overview {
padding:20px 30px
}
td, th {
padding:5px 10px
}
h1 {
margin:0
}
table {
margin:30px;
width:calc(100% - 60px);
max-width:1000px;
border-radius:5px;
border:1px solid #ddd;
border-spacing:0px;
}
th {
font-weight:400;
font-size:normal;
text-align:left;
cursor:pointer
}
td.clr-1, td.clr-2, th span {
font-weight:700
}
th span {
float:right;
margin-left:20px
}
th span:after {
content:"";
clear:both;
display:block
}
tr:last-child td {
border-bottom:none
}
tr td:first-child, tr td:last-child {
color:#9da0a4
}
#overview.bg-0, tr.bg-0 th {
color:#468847;
background:#dff0d8;
border-bottom:1px solid #d6e9c6
}
#overview.bg-1, tr.bg-1 th {
color:#f0ad4e;
background:#fcf8e3;
border-bottom:1px solid #fbeed5
}
#overview.bg-2, tr.bg-2 th {
color:#b94a48;
background:#f2dede;
border-bottom:1px solid #eed3d7
}
td {
border-bottom:1px solid #ddd
}
td.clr-1 {
color:#f0ad4e
}
td.clr-2 {
color:#b94a48
}
td a {
color:#3a33d1;
text-decoration:none
}
td a:hover {
color:#272296;
text-decoration:underline
}
</style>
</head>
<body>
<div id="overview" class="bg-{{getColor totalErrors totalWarnings}}">
<h1>ESLint Report</h1>
<div>
<span>{{renderText totalErrors totalWarnings}}</span> - Generated on {{date}}
</div>
</div>
<table>
<tbody>
{{#each results}}
<tr class="bg-{{getColor this.errorCount this.warningCount}}" data-group="f-{{@index}}">
<th colspan="4">
[+] {{this.filePath}}
<span>{{renderText this.errorCount this.warningCount}}</span>
</th>
</tr>
{{#each this.messages}}
<tr class="f-{{@../index}}" style="display:none">
<td>{{#if this.line}}{{this.line}}{{else}}0{{/if}}:{{#if this.column}}{{this.column}}{{else}}0{{/if}}</td>
{{getSeverity this.severity}}
<td>{{this.message}}</td>
<td>
<a href="http://eslint.org/docs/rules/{{this.ruleId}}" target="_blank">{{this.ruleId}}</a>
</td>
</tr>
{{/each}}
{{/each}}
</tbody>
</table>
<script type="text/javascript">
var groups = document.querySelectorAll("tr[data-group]");
for (i = 0; i < groups.length; i++) {
groups[i].addEventListener("click", function() {
var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
for (var j = 0; j < inGroup.length; j++) {
inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
}
});
}
</script>
</body>
</html>

88
tools/eslint/lib/formatters/html.js

@ -5,7 +5,7 @@
*/ */
"use strict"; "use strict";
var handlebars = require("handlebars").create(); var lodash = require("lodash");
var fs = require("fs"); var fs = require("fs");
var path = require("path"); var path = require("path");
@ -13,6 +13,10 @@ var path = require("path");
// Helpers // Helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
var messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
var resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8"));
/** /**
* Given a word and a count, append an s if count is not one. * Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form. * @param {string} word A word in its singular form.
@ -29,14 +33,14 @@ function pluralize(word, count) {
* @param {string} totalWarnings Total warnings * @param {string} totalWarnings Total warnings
* @returns {string} The formatted string, pluralized where necessary * @returns {string} The formatted string, pluralized where necessary
*/ */
handlebars.registerHelper("renderText", function(totalErrors, totalWarnings) { function renderSummary(totalErrors, totalWarnings) {
var totalProblems = totalErrors + totalWarnings; var totalProblems = totalErrors + totalWarnings;
var renderedText = totalProblems + " " + pluralize("problem", totalProblems); var renderedText = totalProblems + " " + pluralize("problem", totalProblems);
if (totalProblems !== 0) { if (totalProblems !== 0) {
renderedText += " (" + totalErrors + " " + pluralize("error", totalErrors) + ", " + totalWarnings + " " + pluralize("warning", totalWarnings) + ")"; renderedText += " (" + totalErrors + " " + pluralize("error", totalErrors) + ", " + totalWarnings + " " + pluralize("warning", totalWarnings) + ")";
} }
return renderedText; return renderedText;
}); }
/** /**
* Get the color based on whether there are errors/warnings... * Get the color based on whether there are errors/warnings...
@ -44,45 +48,83 @@ handlebars.registerHelper("renderText", function(totalErrors, totalWarnings) {
* @param {string} totalWarnings Total warnings * @param {string} totalWarnings Total warnings
* @returns {int} The color code (0 = green, 1 = yellow, 2 = red) * @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
*/ */
handlebars.registerHelper("getColor", function(totalErrors, totalWarnings) { function renderColor(totalErrors, totalWarnings) {
if (totalErrors !== 0) { if (totalErrors !== 0) {
return 2; return 2;
} else if (totalWarnings !== 0) { } else if (totalWarnings !== 0) {
return 1; return 1;
} }
return 0; return 0;
}); }
/** /**
* Get the HTML row content based on the severity of the message * Get HTML (table rows) describing the messages.
* @param {int} severity Severity of the message * @param {Array} messages Messages.
* @returns {string} The generated HTML row * @param {int} parentIndex Index of the parent HTML row.
* @returns {string} HTML (table rows) describing the messages.
*/ */
handlebars.registerHelper("getSeverity", function(severity) { function renderMessages(messages, parentIndex) {
// Return warning else error /**
return new handlebars.SafeString((severity === 1) ? "<td class=\"clr-1\">Warning</td>" : "<td class=\"clr-2\">Error</td>"); * Get HTML (table row) describing a message.
}); * @param {Object} message Message.
* @returns {string} HTML (table row) describing a message.
*/
return lodash.map(messages, function(message) {
var lineNumber,
columnNumber;
lineNumber = message.line || 0;
columnNumber = message.column || 0;
return messageTemplate({
parentIndex: parentIndex,
lineNumber: lineNumber,
columnNumber: columnNumber,
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId
});
}).join("\n");
}
/**
* @param {Array} results Test results.
* @returns {string} HTML string describing the results.
*/
function renderResults(results) {
return lodash.map(results, function(result, index) {
return resultTemplate({
index: index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(result.errorCount, result.warningCount)
}) + renderMessages(result.messages, index);
}).join("\n");
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(results) { module.exports = function(results) {
var totalErrors,
totalWarnings;
var template = fs.readFileSync(path.join(__dirname, "html-template.html"), "utf-8"); totalErrors = 0;
totalWarnings = 0;
var data = {
date: new Date(),
totalErrors: 0,
totalWarnings: 0,
results: results
};
// Iterate over results to get totals // Iterate over results to get totals
results.forEach(function(result) { results.forEach(function(result) {
data.totalErrors += result.errorCount; totalErrors += result.errorCount;
data.totalWarnings += result.warningCount; totalWarnings += result.warningCount;
}); });
return handlebars.compile(template)(data); return pageTemplate({
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results)
});
}; };

6
tools/eslint/lib/formatters/jslint-xml.js

@ -4,7 +4,7 @@
*/ */
"use strict"; "use strict";
var xmlescape = require("xml-escape"); var lodash = require("lodash");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
@ -25,8 +25,8 @@ module.exports = function(results) {
messages.forEach(function(message) { messages.forEach(function(message) {
output += "<issue line=\"" + message.line + "\" " + output += "<issue line=\"" + message.line + "\" " +
"char=\"" + message.column + "\" " + "char=\"" + message.column + "\" " +
"evidence=\"" + xmlescape(message.source || "") + "\" " + "evidence=\"" + lodash.escape(message.source || "") + "\" " +
"reason=\"" + xmlescape(message.message || "") + "reason=\"" + lodash.escape(message.message || "") +
(message.ruleId ? " (" + message.ruleId + ")" : "") + "\" />"; (message.ruleId ? " (" + message.ruleId + ")" : "") + "\" />";
}); });

6
tools/eslint/lib/formatters/junit.js

@ -4,7 +4,7 @@
*/ */
"use strict"; "use strict";
var xmlescape = require("xml-escape"); var lodash = require("lodash");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helper Functions // Helper Functions
@ -46,11 +46,11 @@ module.exports = function(results) {
messages.forEach(function(message) { messages.forEach(function(message) {
var type = message.fatal ? "error" : "failure"; var type = message.fatal ? "error" : "failure";
output += "<testcase time=\"0\" name=\"org.eslint." + (message.ruleId || "unknown") + "\">"; output += "<testcase time=\"0\" name=\"org.eslint." + (message.ruleId || "unknown") + "\">";
output += "<" + type + " message=\"" + xmlescape(message.message || "") + "\">"; output += "<" + type + " message=\"" + lodash.escape(message.message || "") + "\">";
output += "<![CDATA["; output += "<![CDATA[";
output += "line " + (message.line || 0) + ", col "; output += "line " + (message.line || 0) + ", col ";
output += (message.column || 0) + ", " + getMessageType(message); output += (message.column || 0) + ", " + getMessageType(message);
output += " - " + xmlescape(message.message || ""); output += " - " + lodash.escape(message.message || "");
output += (message.ruleId ? " (" + message.ruleId + ")" : ""); output += (message.ruleId ? " (" + message.ruleId + ")" : "");
output += "]]>"; output += "]]>";
output += "</" + type + ">"; output += "</" + type + ">";

4
tools/eslint/lib/formatters/stylish.js

@ -62,7 +62,7 @@ module.exports = function(results) {
message.column || 0, message.column || 0,
messageType, messageType,
message.message.replace(/\.$/, ""), message.message.replace(/\.$/, ""),
chalk.gray(message.ruleId || "") chalk.dim(message.ruleId || "")
]; ];
}), }),
{ {
@ -73,7 +73,7 @@ module.exports = function(results) {
} }
).split("\n").map(function(el) { ).split("\n").map(function(el) {
return el.replace(/(\d+)\s+(\d+)/, function(m, p1, p2) { return el.replace(/(\d+)\s+(\d+)/, function(m, p1, p2) {
return chalk.gray(p1 + ":" + p2); return chalk.dim(p1 + ":" + p2);
}); });
}).join("\n") + "\n\n"; }).join("\n") + "\n\n";
}); });

159
tools/eslint/lib/formatters/table.js

@ -0,0 +1,159 @@
/**
* @fileoverview "table reporter.
* @author Gajus Kuizinas <gajus@gajus.com>
* @copyright 2016 Gajus Kuizinas <gajus@gajus.com>. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var chalk,
table,
pluralize;
chalk = require("chalk");
table = require("table").default;
pluralize = require("pluralize");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Draws text table.
* @param {Array<Object>} messages Error messages relating to a specific file.
* @returns {string} A text table.
*/
function drawTable(messages) {
var rows;
rows = [];
if (messages.length === 0) {
return "";
}
rows.push([
chalk.bold("Line"),
chalk.bold("Column"),
chalk.bold("Type"),
chalk.bold("Message"),
chalk.bold("Rule ID")
]);
messages.forEach(function(message) {
var messageType;
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
} else {
messageType = chalk.yellow("warning");
}
rows.push([
message.line || 0,
message.column || 0,
messageType,
message.message,
message.ruleId || ""
]);
});
return table(rows, {
columns: {
0: {
width: 8,
wrapWord: true
},
1: {
width: 8,
wrapWord: true
},
2: {
width: 8,
wrapWord: true
},
3: {
paddingRight: 5,
width: 50,
wrapWord: true
},
4: {
width: 20,
wrapWord: true
}
},
drawHorizontalLine: function(index) {
return index === 1;
}
});
}
/**
* Draws a report (multiple tables).
* @param {Array} results Report results for every file.
* @returns {string} A column of text tables.
*/
function drawReport(results) {
var files;
files = results.map(function(result) {
if (!result.messages.length) {
return "";
}
return "\n" + result.filePath + "\n\n" + drawTable(result.messages);
});
files = files.filter(function(content) {
return content.trim();
});
return files.join("");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(report) {
var result,
errorCount,
warningCount;
result = "";
errorCount = 0;
warningCount = 0;
report.forEach(function(fileReport) {
errorCount += fileReport.errorCount;
warningCount += fileReport.warningCount;
});
if (errorCount || warningCount) {
result = drawReport(report);
}
result += "\n" + table([
[
chalk.red(pluralize("Error", errorCount, true))
],
[
chalk.yellow(pluralize("Warning", warningCount, true))
]
], {
columns: {
0: {
width: 110,
wrapWord: true
}
},
drawHorizontalLine: function() {
return true;
}
});
return result;
};

5
tools/eslint/lib/formatters/tap.js

@ -66,7 +66,10 @@ module.exports = function(results) {
// The first error will be logged as message key // The first error will be logged as message key
// This is to adhere to TAP 13 loosely defined specification of having a message key // This is to adhere to TAP 13 loosely defined specification of having a message key
if ("message" in diagnostics) { if ("message" in diagnostics) {
diagnostics.messages = [diagnostic]; if (typeof diagnostics.messages === "undefined") {
diagnostics.messages = [];
}
diagnostics.messages.push(diagnostic);
} else { } else {
diagnostics = diagnostic; diagnostics = diagnostic;
} }

64
tools/eslint/lib/formatters/visualstudio.js

@ -0,0 +1,64 @@
/**
* @fileoverview Visual Studio compatible formatter
* @author Ronald Pijnacker
* @copyright 2015 Ronald Pijnacker. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
} else {
return "warning";
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
var output = "",
total = 0;
results.forEach(function(result) {
var messages = result.messages;
total += messages.length;
messages.forEach(function(message) {
output += result.filePath;
output += "(" + (message.line || 0);
output += message.column ? "," + message.column : "";
output += "): " + getMessageType(message);
output += message.ruleId ? " " + message.ruleId : "";
output += " : " + message.message;
output += "\n";
});
});
if (total === 0) {
output += "no problems";
} else {
output += "\n" + total + " problem" + (total !== 1 ? "s" : "");
}
return output;
};

239
tools/eslint/lib/ignored-paths.js

@ -8,10 +8,11 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var fs = require("fs"), var lodash = require("lodash"),
fs = require("fs"),
path = require("path"),
debug = require("debug"), debug = require("debug"),
minimatch = require("minimatch"), ignore = require("ignore");
FileFinder = require("./file-finder");
debug = debug("eslint:ignored-paths"); debug = debug("eslint:ignored-paths");
@ -21,56 +22,94 @@ debug = debug("eslint:ignored-paths");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var ESLINT_IGNORE_FILENAME = ".eslintignore"; var ESLINT_IGNORE_FILENAME = ".eslintignore";
var DEFAULT_IGNORE_PATTERNS = [
"/node_modules/",
"/bower_components/"
];
var DEFAULT_OPTIONS = {
dotfiles: false,
cwd: process.cwd()
};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** /**
* Load and parse ignore patterns from the file at the given path * Find an ignore file in the current directory.
* @param {string} filepath Path to the ignore file. * @param {stirng} cwd Current working directory
* @returns {string[]} An array of ignore patterns or an empty array if no ignore file. * @returns {string} Path of ignore file or an empty string.
*/ */
function loadIgnoreFile(filepath) { function findIgnoreFile(cwd) {
var ignorePatterns = []; cwd = cwd || DEFAULT_OPTIONS.cwd;
/**
* Check if string is not empty
* @param {string} line string to examine
* @returns {boolean} True is its not empty
* @private
*/
function nonEmpty(line) {
return line.trim() !== "" && line[0] !== "#";
}
if (filepath) { var ignoreFilePath = path.resolve(cwd, ESLINT_IGNORE_FILENAME);
try { return fs.existsSync(ignoreFilePath) ? ignoreFilePath : "";
ignorePatterns = fs.readFileSync(filepath, "utf8").split(/\r?\n/).filter(nonEmpty); }
} catch (e) {
e.message = "Cannot read ignore file: " + filepath + "\nError: " + e.message;
throw e;
}
}
return ["node_modules/**"].concat(ignorePatterns); /**
* Replace Windows with Unix style paths and remove ./ prefix
* @param {string} filepath Path to normalize
* @returns {string} Normalized filepath
*/
function normalizeFilepath(filepath) {
filepath = filepath.replace(/\\/g, "/");
filepath = filepath.replace(/^\.\//, "");
return filepath;
} }
var ignoreFileFinder; /**
* Remove a prefix from a filepath
* @param {string} filepath Path to remove the prefix from
* @param {string} prefix Prefix to remove from filepath
* @returns {string} Normalized filepath
*/
function removePrefixFromFilepath(filepath, prefix) {
prefix += "/";
if (filepath.indexOf(prefix) === 0) {
filepath = filepath.substr(prefix.length);
}
return filepath;
}
/** /**
* Find an ignore file in the current directory or a parent directory. * Resolves a filepath
* @returns {string} Path of ignore file or an empty string. * @param {string} filepath Path resolve
* @param {string} baseDir Base directory to resolve the filepath from
* @returns {string} Resolved filepath
*/ */
function findIgnoreFile() { function resolveFilepath(filepath, baseDir) {
if (!ignoreFileFinder) { if (baseDir) {
ignoreFileFinder = new FileFinder(ESLINT_IGNORE_FILENAME); var base = normalizeFilepath(path.resolve(baseDir));
filepath = removePrefixFromFilepath(filepath, base);
filepath = removePrefixFromFilepath(filepath, fs.realpathSync(base));
} }
filepath.replace(/^\//, "");
return filepath;
}
return ignoreFileFinder.findInDirectoryOrParents(); /**
* Normalize and resolve a filepath relative to a given base directory
* @param {string} filepath Path resolve
* @param {string} baseDir Base directory to resolve the filepath from
* @returns {string} Normalized and resolved filepath
*/
function normalizeAndResolveFilepath(filepath, baseDir) {
filepath = normalizeFilepath(filepath);
return resolveFilepath(filepath, baseDir);
} }
/**
* Merge options with defaults
* @param {object} options Options to merge with DEFAULT_OPTIONS constant
* @returns {object} Merged options
*/
function mergeDefaultOptions(options) {
options = (options || {});
return lodash.assign({}, DEFAULT_OPTIONS, options);
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
@ -80,64 +119,116 @@ function findIgnoreFile() {
* IgnoredPaths * IgnoredPaths
* @constructor * @constructor
* @class IgnoredPaths * @class IgnoredPaths
* @param {Array} patterns to be matched against file paths * @param {Object} options object containing 'ignore', 'ignorePath' and 'patterns' properties
*/ */
function IgnoredPaths(patterns) { function IgnoredPaths(options) {
this.patterns = patterns;
}
/** options = mergeDefaultOptions(options);
* IgnoredPaths initializer
* @param {Object} options object containing 'ignore' and 'ignorePath' properties
* @returns {IgnoredPaths} object, with patterns loaded from the ignore file
*/
IgnoredPaths.load = function(options) {
var patterns;
options = options || {}; /**
* add pattern to node-ignore instance
* @param {object} ig, instance of node-ignore
* @param {string} pattern, pattern do add to ig
* @returns {array} raw ignore rules
*/
function addPattern(ig, pattern) {
return ig.addPattern(pattern);
}
if (options.ignore) { /**
patterns = loadIgnoreFile(options.ignorePath || findIgnoreFile()); * add ignore file to node-ignore instance
} else { * @param {object} ig, instance of node-ignore
patterns = []; * @param {string} filepath, file to add to ig
* @returns {array} raw ignore rules
*/
function addIgnoreFile(ig, filepath) {
return ig.addIgnoreFile(filepath);
} }
if (options.ignorePattern) { this.defaultPatterns = DEFAULT_IGNORE_PATTERNS.concat(options.patterns || []);
patterns = patterns.concat(options.ignorePattern); this.baseDir = ".";
this.ig = {
custom: new ignore.Ignore({
twoGlobstars: true,
ignore: []
}),
default: new ignore.Ignore({
twoGlobstars: true,
ignore: []
})
};
if (options.dotfiles !== true) {
addPattern(this.ig.default, ".*");
} }
return new IgnoredPaths(patterns); if (options.ignore !== false) {
};
addPattern(this.ig.default, this.defaultPatterns);
var ignorePath;
if (options.ignorePattern) {
addPattern(this.ig.custom, options.ignorePattern);
}
if (options.ignorePath) {
debug("Using specific ignore file");
try {
fs.statSync(options.ignorePath);
ignorePath = options.ignorePath;
} catch (e) {
e.message = "Cannot read ignore file: " + options.ignorePath + "\nError: " + e.message;
throw e;
}
} else {
debug("Looking for ignore file in " + options.cwd);
ignorePath = findIgnoreFile(options.cwd);
try {
fs.statSync(ignorePath);
debug("Loaded ignore file " + ignorePath);
} catch (e) {
debug("Could not find ignore file in cwd");
this.options = options;
}
}
if (ignorePath) {
debug("Adding " + ignorePath);
this.baseDir = path.dirname(ignorePath);
addIgnoreFile(this.ig.custom, ignorePath);
}
}
this.options = options;
}
/** /**
* Determine whether a file path is included in the configured ignore patterns * Determine whether a file path is included in the default or custom ignore patterns
* @param {string} filepath Path to check * @param {string} filepath Path to check
* @param {string} [category=null] check 'default', 'custom' or both (null)
* @returns {boolean} true if the file path matches one or more patterns, false otherwise * @returns {boolean} true if the file path matches one or more patterns, false otherwise
*/ */
IgnoredPaths.prototype.contains = function(filepath) { IgnoredPaths.prototype.contains = function(filepath, category) {
if (this.patterns === null) {
throw new Error("No ignore patterns loaded, call 'load' first");
}
filepath = filepath.replace("\\", "/"); var result = false;
filepath = filepath.replace(/^\.\//, ""); filepath = normalizeAndResolveFilepath(filepath, this.baseDir);
return this.patterns.reduce(function(ignored, pattern) {
var negated = pattern[0] === "!",
matches;
if (negated) { if ((typeof category === "undefined") || (category === "default")) {
pattern = pattern.slice(1); result = result || (this.ig.default.filter([filepath]).length === 0);
} }
// Remove leading "current folder" prefix if ((typeof category === "undefined") || (category === "custom")) {
if (pattern.indexOf("./") === 0) { result = result || (this.ig.custom.filter([filepath]).length === 0);
pattern = pattern.slice(2); }
}
matches = minimatch(filepath, pattern) || minimatch(filepath, pattern + "/**"); return result;
return matches ? !negated : ignored;
}, false);
}; };
module.exports = IgnoredPaths; module.exports = IgnoredPaths;

7
tools/eslint/lib/load-rules.js

@ -18,14 +18,15 @@ var fs = require("fs"),
/** /**
* Load all rule modules from specified directory. * Load all rule modules from specified directory.
* @param {String} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`. * @param {string} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`.
* @param {string} cwd Current working directory
* @returns {Object} Loaded rule modules by rule ids (file names). * @returns {Object} Loaded rule modules by rule ids (file names).
*/ */
module.exports = function(rulesDir) { module.exports = function(rulesDir, cwd) {
if (!rulesDir) { if (!rulesDir) {
rulesDir = path.join(__dirname, "rules"); rulesDir = path.join(__dirname, "rules");
} else { } else {
rulesDir = path.resolve(process.cwd(), rulesDir); rulesDir = path.resolve(cwd, rulesDir);
} }
var rules = Object.create(null); var rules = Object.create(null);

18
tools/eslint/lib/options.js

@ -18,8 +18,10 @@ var optionator = require("optionator");
// exports "parse(args)", "generateHelp()", and "generateHelpForOption(optionName)" // exports "parse(args)", "generateHelp()", and "generateHelpForOption(optionName)"
module.exports = optionator({ module.exports = optionator({
prepend: "eslint [options] file.js [file.js] [dir]", prepend: "eslint [options] file.js [file.js] [dir]",
concatRepeatedArrays: true, defaults: {
mergeRepeatedObjects: true, concatRepeatedArrays: true,
mergeRepeatedObjects: true
},
options: [ options: [
{ {
heading: "Basic configuration" heading: "Basic configuration"
@ -113,7 +115,10 @@ module.exports = optionator({
{ {
option: "ignore-pattern", option: "ignore-pattern",
type: "[String]", type: "[String]",
description: "Pattern of files to ignore (in addition to those in .eslintignore)" description: "Pattern of files to ignore (in addition to those in .eslintignore)",
concatRepeatedArrays: [true, {
oneValuePerFlag: true
}]
}, },
{ {
heading: "Using stdin" heading: "Using stdin"
@ -140,7 +145,7 @@ module.exports = optionator({
}, },
{ {
option: "max-warnings", option: "max-warnings",
type: "Number", type: "Int",
default: "-1", default: "-1",
description: "Number of warnings to trigger nonzero exit code" description: "Number of warnings to trigger nonzero exit code"
}, },
@ -204,6 +209,11 @@ module.exports = optionator({
type: "Boolean", type: "Boolean",
default: "true", default: "true",
description: "Allow comments to change eslint config/rules" description: "Allow comments to change eslint config/rules"
},
{
option: "print-config",
type: "Boolean",
description: "Print the configuration to be used"
} }
] ]
}); });

111
tools/eslint/lib/rule-context.js

@ -38,8 +38,7 @@ var PASSTHROUGHS = [
"getTokensAfter", "getTokensAfter",
"getTokensBefore", "getTokensBefore",
"getTokensBetween", "getTokensBetween",
"markVariableAsUsed", "markVariableAsUsed"
"isMarkedAsUsed"
]; ];
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -67,47 +66,38 @@ var PASSTHROUGHS = [
* @param {string} ruleId The ID of the rule using this object. * @param {string} ruleId The ID of the rule using this object.
* @param {eslint} eslint The eslint object. * @param {eslint} eslint The eslint object.
* @param {number} severity The configured severity level of the rule. * @param {number} severity The configured severity level of the rule.
* @param {array} options The configuration information to be added to the rule. * @param {Array} options The configuration information to be added to the rule.
* @param {object} settings The configuration settings passed from the config file. * @param {Object} settings The configuration settings passed from the config file.
* @param {object} ecmaFeatures The ecmaFeatures settings passed from the config file. * @param {Object} parserOptions The parserOptions settings passed from the config file.
* @param {Object} parserPath The parser setting passed from the config file.
* @param {Object} meta The metadata of the rule
*/ */
function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures) { function RuleContext(ruleId, eslint, severity, options, settings, parserOptions, parserPath, meta) {
// public.
/** this.id = ruleId;
* The read-only ID of the rule. this.options = options;
*/ this.settings = settings;
Object.defineProperty(this, "id", { this.parserOptions = parserOptions;
value: ruleId this.parserPath = parserPath;
}); this.meta = meta;
/** // private.
* The read-only options of the rule this.eslint = eslint;
*/ this.severity = severity;
Object.defineProperty(this, "options", {
value: options Object.freeze(this);
}); }
/** RuleContext.prototype = {
* The read-only settings shared between all rules constructor: RuleContext,
*/
Object.defineProperty(this, "settings", {
value: settings
});
/** /**
* The read-only ecmaFeatures shared across all rules * Passthrough to eslint.getSourceCode().
* @returns {SourceCode} The SourceCode object for the code.
*/ */
Object.defineProperty(this, "ecmaFeatures", { getSourceCode: function() {
value: Object.create(ecmaFeatures) return this.eslint.getSourceCode();
}); },
Object.freeze(this.ecmaFeatures);
// copy over passthrough methods
PASSTHROUGHS.forEach(function(name) {
this[name] = function() {
return eslint[name].apply(eslint, arguments);
};
}, this);
/** /**
* Passthrough to eslint.report() that automatically assigns the rule ID and severity. * Passthrough to eslint.report() that automatically assigns the rule ID and severity.
@ -119,8 +109,7 @@ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures)
* with symbols being replaced by this object's values. * with symbols being replaced by this object's values.
* @returns {void} * @returns {void}
*/ */
this.report = function(nodeOrDescriptor, location, message, opts) { report: function(nodeOrDescriptor, location, message, opts) {
var descriptor, var descriptor,
fix = null; fix = null;
@ -133,31 +122,39 @@ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures)
fix = descriptor.fix(new RuleFixer()); fix = descriptor.fix(new RuleFixer());
} }
eslint.report( this.eslint.report(
ruleId, severity, descriptor.node, this.id,
this.severity,
descriptor.node,
descriptor.loc || descriptor.node.loc.start, descriptor.loc || descriptor.node.loc.start,
descriptor.message, descriptor.data, fix descriptor.message,
descriptor.data,
fix,
this.meta
); );
return; return;
} }
// old style call // old style call
eslint.report(ruleId, severity, nodeOrDescriptor, location, message, opts); this.eslint.report(
}; this.id,
this.severity,
nodeOrDescriptor,
location,
message,
opts,
this.meta
);
}
};
/** // copy over passthrough methods
* Passthrough to eslint.getSourceCode(). PASSTHROUGHS.forEach(function(name) {
* @returns {SourceCode} The SourceCode object for the code. // All functions expected to have less arguments than 5.
*/ this[name] = function(a, b, c, d, e) {
this.getSourceCode = function() { return this.eslint[name](a, b, c, d, e);
return eslint.getSourceCode();
}; };
}, RuleContext.prototype);
}
RuleContext.prototype = {
constructor: RuleContext
};
module.exports = RuleContext; module.exports = RuleContext;

24
tools/eslint/lib/rules.js

@ -23,7 +23,7 @@ var rules = Object.create(null);
/** /**
* Registers a rule module for rule id in storage. * Registers a rule module for rule id in storage.
* @param {String} ruleId Rule id (file name). * @param {string} ruleId Rule id (file name).
* @param {Function} ruleModule Rule handler. * @param {Function} ruleModule Rule handler.
* @returns {void} * @returns {void}
*/ */
@ -33,11 +33,12 @@ function define(ruleId, ruleModule) {
/** /**
* Loads and registers all rules from passed rules directory. * Loads and registers all rules from passed rules directory.
* @param {String} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`. * @param {string} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`.
* @param {string} cwd Current working directory
* @returns {void} * @returns {void}
*/ */
function load(rulesDir) { function load(rulesDir, cwd) {
var newRules = loadRules(rulesDir); var newRules = loadRules(rulesDir, cwd);
Object.keys(newRules).forEach(function(ruleId) { Object.keys(newRules).forEach(function(ruleId) {
define(ruleId, newRules[ruleId]); define(ruleId, newRules[ruleId]);
}); });
@ -46,7 +47,7 @@ function load(rulesDir) {
/** /**
* Registers all given rules of a plugin. * Registers all given rules of a plugin.
* @param {Object} pluginRules A key/value map of rule definitions. * @param {Object} pluginRules A key/value map of rule definitions.
* @param {String} pluginName The name of the plugin without prefix (`eslint-plugin-`). * @param {string} pluginName The name of the plugin without prefix (`eslint-plugin-`).
* @returns {void} * @returns {void}
*/ */
function importPlugin(pluginRules, pluginName) { function importPlugin(pluginRules, pluginName) {
@ -60,7 +61,7 @@ function importPlugin(pluginRules, pluginName) {
/** /**
* Access rule handler by id (file name). * Access rule handler by id (file name).
* @param {String} ruleId Rule id (file name). * @param {string} ruleId Rule id (file name).
* @returns {Function} Rule handler. * @returns {Function} Rule handler.
*/ */
function get(ruleId) { function get(ruleId) {
@ -85,7 +86,16 @@ module.exports = {
load: load, load: load,
import: importPlugin, import: importPlugin,
get: get, get: get,
testClear: testClear testClear: testClear,
/**
* Resets rules to its starting state. Use for tests only.
* @returns {void}
*/
testReset: function() {
testClear();
load();
}
}; };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

135
tools/eslint/lib/rules/accessor-pairs.js

@ -66,80 +66,85 @@ function isPropertyDescriptor(node) {
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = {
var config = context.options[0] || {}; meta: {
var checkGetWithoutSet = config.getWithoutSet === true; docs: {
var checkSetWithoutGet = config.setWithoutGet !== false; description: "Enforces getter/setter pairs in objects",
category: "Best Practices",
/** recommended: false
* Checks a object expression to see if it has setter and getter both present or none. },
* @param {ASTNode} node The node to check. schema: [{
* @returns {void} "type": "object",
* @private "properties": {
*/ "getWithoutSet": {
function checkLonelySetGet(node) { "type": "boolean"
var isSetPresent = false; },
var isGetPresent = false; "setWithoutGet": {
var isDescriptor = isPropertyDescriptor(node); "type": "boolean"
}
for (var i = 0, end = node.properties.length; i < end; i++) { },
var property = node.properties[i]; "additionalProperties": false
}]
var propToCheck = ""; },
if (property.kind === "init") { create: function(context) {
if (isDescriptor && !property.computed) { var config = context.options[0] || {};
propToCheck = property.key.name; var checkGetWithoutSet = config.getWithoutSet === true;
var checkSetWithoutGet = config.setWithoutGet !== false;
/**
* Checks a object expression to see if it has setter and getter both present or none.
* @param {ASTNode} node The node to check.
* @returns {void}
* @private
*/
function checkLonelySetGet(node) {
var isSetPresent = false;
var isGetPresent = false;
var isDescriptor = isPropertyDescriptor(node);
for (var i = 0, end = node.properties.length; i < end; i++) {
var property = node.properties[i];
var propToCheck = "";
if (property.kind === "init") {
if (isDescriptor && !property.computed) {
propToCheck = property.key.name;
}
} else {
propToCheck = property.kind;
} }
} else {
propToCheck = property.kind;
}
switch (propToCheck) { switch (propToCheck) {
case "set": case "set":
isSetPresent = true; isSetPresent = true;
break; break;
case "get": case "get":
isGetPresent = true; isGetPresent = true;
break; break;
default: default:
// Do nothing // Do nothing
} }
if (isSetPresent && isGetPresent) { if (isSetPresent && isGetPresent) {
break; break;
}
} }
}
if (checkSetWithoutGet && isSetPresent && !isGetPresent) { if (checkSetWithoutGet && isSetPresent && !isGetPresent) {
context.report(node, "Getter is not present"); context.report(node, "Getter is not present");
} else if (checkGetWithoutSet && isGetPresent && !isSetPresent) { } else if (checkGetWithoutSet && isGetPresent && !isSetPresent) {
context.report(node, "Setter is not present"); context.report(node, "Setter is not present");
}
}
return {
"ObjectExpression": function(node) {
if (checkSetWithoutGet || checkGetWithoutSet) {
checkLonelySetGet(node);
} }
} }
};
};
module.exports.schema = [ return {
{ "ObjectExpression": function(node) {
"type": "object", if (checkSetWithoutGet || checkGetWithoutSet) {
"properties": { checkLonelySetGet(node);
"getWithoutSet": { }
"type": "boolean"
},
"setWithoutGet": {
"type": "boolean"
} }
}, };
"additionalProperties": false
} }
]; };

360
tools/eslint/lib/rules/array-bracket-spacing.js

@ -14,196 +14,204 @@ var astUtils = require("../ast-utils");
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = {
var spaced = context.options[0] === "always", meta: {
sourceCode = context.getSourceCode(); docs: {
description: "Enforce spacing inside array brackets",
/** category: "Stylistic Issues",
* Determines whether an option is set, relative to the spacing option. recommended: false,
* If spaced is "always", then check whether option is set to false. fixable: "whitespace"
* If spaced is "never", then check whether option is set to true. },
* @param {Object} option - The option to exclude. schema: [
* @returns {boolean} Whether or not the property is excluded. {
*/ "enum": ["always", "never"]
function isOptionSet(option) { },
return context.options[1] ? context.options[1][option] === !spaced : false; {
} "type": "object",
"properties": {
var options = { "singleValue": {
spaced: spaced, "type": "boolean"
singleElementException: isOptionSet("singleValue"), },
objectsInArraysException: isOptionSet("objectsInArrays"), "objectsInArrays": {
arraysInArraysException: isOptionSet("arraysInArrays") "type": "boolean"
}; },
"arraysInArrays": {
//-------------------------------------------------------------------------- "type": "boolean"
// Helpers }
//-------------------------------------------------------------------------- },
"additionalProperties": false
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoBeginningSpace(node, token) {
context.report({
node: node,
loc: token.loc.start,
message: "There should be no space after '" + token.value + "'",
fix: function(fixer) {
var nextToken = context.getSourceCode().getTokenAfter(token);
return fixer.removeRange([token.range[1], nextToken.range[0]]);
} }
}); ]
} },
create: function(context) {
var spaced = context.options[0] === "always",
sourceCode = context.getSourceCode();
/**
* Determines whether an option is set, relative to the spacing option.
* If spaced is "always", then check whether option is set to false.
* If spaced is "never", then check whether option is set to true.
* @param {Object} option - The option to exclude.
* @returns {boolean} Whether or not the property is excluded.
*/
function isOptionSet(option) {
return context.options[1] ? context.options[1][option] === !spaced : false;
}
/** var options = {
* Reports that there shouldn't be a space before the last token spaced: spaced,
* @param {ASTNode} node - The node to report in the event of an error. singleElementException: isOptionSet("singleValue"),
* @param {Token} token - The token to use for the report. objectsInArraysException: isOptionSet("objectsInArrays"),
* @returns {void} arraysInArraysException: isOptionSet("arraysInArrays")
*/ };
function reportNoEndingSpace(node, token) {
context.report({ //--------------------------------------------------------------------------
node: node, // Helpers
loc: token.loc.start, //--------------------------------------------------------------------------
message: "There should be no space before '" + token.value + "'",
fix: function(fixer) { /**
var previousToken = context.getSourceCode().getTokenBefore(token); * Reports that there shouldn't be a space after the first token
return fixer.removeRange([previousToken.range[1], token.range[0]]); * @param {ASTNode} node - The node to report in the event of an error.
} * @param {Token} token - The token to use for the report.
}); * @returns {void}
} */
function reportNoBeginningSpace(node, token) {
context.report({
node: node,
loc: token.loc.start,
message: "There should be no space after '" + token.value + "'",
fix: function(fixer) {
var nextToken = context.getSourceCode().getTokenAfter(token);
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/** /**
* Reports that there should be a space after the first token * Reports that there shouldn't be a space before the last token
* @param {ASTNode} node - The node to report in the event of an error. * @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report. * @param {Token} token - The token to use for the report.
* @returns {void} * @returns {void}
*/ */
function reportRequiredBeginningSpace(node, token) { function reportNoEndingSpace(node, token) {
context.report({ context.report({
node: node, node: node,
loc: token.loc.start, loc: token.loc.start,
message: "A space is required after '" + token.value + "'", message: "There should be no space before '" + token.value + "'",
fix: function(fixer) { fix: function(fixer) {
return fixer.insertTextAfter(token, " "); var previousToken = context.getSourceCode().getTokenBefore(token);
} return fixer.removeRange([previousToken.range[1], token.range[0]]);
}); }
} });
}
/** /**
* Reports that there should be a space before the last token * Reports that there should be a space after the first token
* @param {ASTNode} node - The node to report in the event of an error. * @param {ASTNode} node - The node to report in the event of an error.
* @param {Token} token - The token to use for the report. * @param {Token} token - The token to use for the report.
* @returns {void} * @returns {void}
*/ */
function reportRequiredEndingSpace(node, token) { function reportRequiredBeginningSpace(node, token) {
context.report({ context.report({
node: node, node: node,
loc: token.loc.start, loc: token.loc.start,
message: "A space is required before '" + token.value + "'", message: "A space is required after '" + token.value + "'",
fix: function(fixer) { fix: function(fixer) {
return fixer.insertTextBefore(token, " "); return fixer.insertTextAfter(token, " ");
} }
}); });
} }
/** /**
* Determines if a node is an object type * Reports that there should be a space before the last token
* @param {ASTNode} node - The node to check. * @param {ASTNode} node - The node to report in the event of an error.
* @returns {boolean} Whether or not the node is an object type. * @param {Token} token - The token to use for the report.
*/ * @returns {void}
function isObjectType(node) { */
return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern"); function reportRequiredEndingSpace(node, token) {
} context.report({
node: node,
loc: token.loc.start,
message: "A space is required before '" + token.value + "'",
fix: function(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/** /**
* Determines if a node is an array type * Determines if a node is an object type
* @param {ASTNode} node - The node to check. * @param {ASTNode} node - The node to check.
* @returns {boolean} Whether or not the node is an array type. * @returns {boolean} Whether or not the node is an object type.
*/ */
function isArrayType(node) { function isObjectType(node) {
return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern"); return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern");
} }
/** /**
* Validates the spacing around array brackets * Determines if a node is an array type
* @param {ASTNode} node - The node we're checking for spacing * @param {ASTNode} node - The node to check.
* @returns {void} * @returns {boolean} Whether or not the node is an array type.
*/ */
function validateArraySpacing(node) { function isArrayType(node) {
if (options.spaced && node.elements.length === 0) { return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern");
return;
} }
var first = context.getFirstToken(node), /**
second = context.getFirstToken(node, 1), * Validates the spacing around array brackets
penultimate = context.getLastToken(node, 1), * @param {ASTNode} node - The node we're checking for spacing
last = context.getLastToken(node), * @returns {void}
firstElement = node.elements[0], */
lastElement = node.elements[node.elements.length - 1]; function validateArraySpacing(node) {
if (options.spaced && node.elements.length === 0) {
var openingBracketMustBeSpaced = return;
options.objectsInArraysException && isObjectType(firstElement) ||
options.arraysInArraysException && isArrayType(firstElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
var closingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(lastElement) ||
options.arraysInArraysException && isArrayType(lastElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
if (astUtils.isTokenOnSameLine(first, second)) {
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
}
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
} }
}
if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) { var first = context.getFirstToken(node),
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) { second = context.getFirstToken(node, 1),
reportRequiredEndingSpace(node, last); penultimate = context.getLastToken(node, 1),
last = context.getLastToken(node),
firstElement = node.elements[0],
lastElement = node.elements[node.elements.length - 1];
var openingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(firstElement) ||
options.arraysInArraysException && isArrayType(firstElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
var closingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(lastElement) ||
options.arraysInArraysException && isArrayType(lastElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
if (astUtils.isTokenOnSameLine(first, second)) {
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
}
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
} }
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last); if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) {
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
}
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
} }
} }
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return { //--------------------------------------------------------------------------
ArrayPattern: validateArraySpacing, // Public
ArrayExpression: validateArraySpacing //--------------------------------------------------------------------------
};
}; return {
ArrayPattern: validateArraySpacing,
module.exports.schema = [ ArrayExpression: validateArraySpacing
{ };
"enum": ["always", "never"]
},
{
"type": "object",
"properties": {
"singleValue": {
"type": "boolean"
},
"objectsInArrays": {
"type": "boolean"
},
"arraysInArrays": {
"type": "boolean"
}
},
"additionalProperties": false
} }
]; };

236
tools/eslint/lib/rules/array-callback-return.js

@ -0,0 +1,236 @@
/**
* @fileoverview Rule to enforce return statements in callbacks of array's methods
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
var TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
var TARGET_METHODS = /^(?:every|filter|find(?:Index)?|map|reduce(?:Right)?|some|sort)$/;
/**
* Checks a given code path segment is reachable.
*
* @param {CodePathSegment} segment - A segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* Gets a readable location.
*
* - FunctionExpression -> the function name or `function` keyword.
* - ArrowFunctionExpression -> `=>` token.
*
* @param {ASTNode} node - A function node to get.
* @param {SourceCode} sourceCode - A source code to get tokens.
* @returns {ASTNode|Token} The node or the token of a location.
*/
function getLocation(node, sourceCode) {
if (node.type === "ArrowFunctionExpression") {
return sourceCode.getTokenBefore(node.body);
}
return node.id || node;
}
/**
* Gets the name of a given node if the node is a Identifier node.
*
* @param {ASTNode} node - A node to get.
* @returns {string} The name of the node, or an empty string.
*/
function getIdentifierName(node) {
return node.type === "Identifier" ? node.name : "";
}
/**
* Gets the value of a given node if the node is a Literal node or a
* TemplateLiteral node.
*
* @param {ASTNode} node - A node to get.
* @returns {string} The value of the node, or an empty string.
*/
function getConstantStringValue(node) {
switch (node.type) {
case "Literal":
return String(node.value);
case "TemplateLiteral":
return node.expressions.length === 0
? node.quasis[0].value.cooked
: "";
default:
return "";
}
}
/**
* Checks a given node is a MemberExpression node which has the specified name's
* property.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
*/
function isTargetMethod(node) {
return (
node.type === "MemberExpression" &&
TARGET_METHODS.test(
(node.computed ? getConstantStringValue : getIdentifierName)(node.property)
)
);
}
/**
* Checks whether or not a given node is a function expression which is the
* callback of an array method.
*
* @param {ASTNode} node - A node to check. This is one of
* FunctionExpression or ArrowFunctionExpression.
* @returns {boolean} `true` if the node is the callback of an array method.
*/
function isCallbackOfArrayMethod(node) {
while (node) {
var parent = node.parent;
switch (parent.type) {
// Looks up the destination.
// e.g.
// foo.every(nativeFoo || function foo() { ... });
case "LogicalExpression":
case "ConditionalExpression":
node = parent;
break;
// If the upper function is IIFE, checks the destination of the return value.
// e.g.
// foo.every((function() {
// // setup...
// return function callback() { ... };
// })());
case "ReturnStatement":
var func = astUtils.getUpperFunction(parent);
if (func === null || !astUtils.isCallee(func)) {
return false;
}
node = func.parent;
break;
// e.g.
// Array.from([], function() {});
// list.every(function() {});
case "CallExpression":
if (astUtils.isArrayFromMethod(parent.callee)) {
return (
parent.arguments.length >= 2 &&
parent.arguments[1] === node
);
}
if (isTargetMethod(parent.callee)) {
return (
parent.arguments.length >= 1 &&
parent.arguments[0] === node
);
}
return false;
// Otherwise this node is not target.
default:
return false;
}
}
/* istanbul ignore next: unreachable */
return false;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var funcInfo = {
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false
};
/**
* Checks whether or not the last code path segment is reachable.
* Then reports this function if the segment is reachable.
*
* If the last code path segment is reachable, there are paths which are not
* returned or thrown.
*
* @param {ASTNode} node - A node to check.
* @returns {void}
*/
function checkLastSegment(node) {
if (funcInfo.shouldCheck &&
funcInfo.codePath.currentSegments.some(isReachable)
) {
context.report({
node: node,
loc: getLocation(node, context.getSourceCode()).loc.start,
message: funcInfo.hasReturn
? "Expected to return a value at the end of this function."
: "Expected to return a value in this function."
});
}
}
return {
// Stacks this function's information.
"onCodePathStart": function(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath: codePath,
hasReturn: false,
shouldCheck:
TARGET_NODE_TYPE.test(node.type) &&
node.body.type === "BlockStatement" &&
isCallbackOfArrayMethod(node)
};
},
// Pops this function's information.
"onCodePathEnd": function() {
funcInfo = funcInfo.upper;
},
// Checks the return statement is valid.
"ReturnStatement": function(node) {
if (funcInfo.shouldCheck) {
funcInfo.hasReturn = true;
if (!node.argument) {
context.report({
node: node,
message: "Expected a return value."
});
}
}
},
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
};
module.exports.schema = [];

19
tools/eslint/lib/rules/arrow-body-style.js

@ -24,29 +24,16 @@ module.exports = function(context) {
if (arrowBody.type === "BlockStatement") { if (arrowBody.type === "BlockStatement") {
var blockBody = arrowBody.body; var blockBody = arrowBody.body;
if (blockBody.length > 1) { if (blockBody.length !== 1) {
return; return;
} }
if (blockBody.length === 0) { if (asNeeded && blockBody[0].type === "ReturnStatement") {
var hasComments = context.getComments(arrowBody).trailing.length > 0;
if (hasComments) {
return;
}
context.report({ context.report({
node: node, node: node,
loc: arrowBody.loc.start, loc: arrowBody.loc.start,
message: "Unexpected empty block in arrow body." message: "Unexpected block statement surrounding arrow body."
}); });
} else {
if (asNeeded && blockBody[0].type === "ReturnStatement") {
context.report({
node: node,
loc: arrowBody.loc.start,
message: "Unexpected block statement surrounding arrow body."
});
}
} }
} else { } else {
if (always) { if (always) {

2
tools/eslint/lib/rules/arrow-spacing.js

@ -1,5 +1,5 @@
/** /**
* @fileoverview Rule to require parens in arrow function arguments. * @fileoverview Rule to define spacing before/after arrow function's arrow.
* @author Jxck * @author Jxck
* @copyright 2015 Jxck. All rights reserved. * @copyright 2015 Jxck. All rights reserved.
*/ */

46
tools/eslint/lib/rules/block-scoped-var.js

@ -6,38 +6,11 @@
*/ */
"use strict"; "use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Collects unresolved references from the global scope, then creates a map to references from its name.
* @param {RuleContext} context - The current context.
* @returns {object} A map object. Its key is the variable names. Its value is the references of each variable.
*/
function collectUnresolvedReferences(context) {
var unresolved = Object.create(null);
var references = context.getScope().through;
for (var i = 0; i < references.length; ++i) {
var reference = references[i];
var name = reference.identifier.name;
if (name in unresolved === false) {
unresolved[name] = [];
}
unresolved[name].push(reference);
}
return unresolved;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var unresolvedReferences = Object.create(null);
var stack = []; var stack = [];
/** /**
@ -66,7 +39,7 @@ module.exports = function(context) {
var identifier = reference.identifier; var identifier = reference.identifier;
context.report( context.report(
identifier, identifier,
"\"{{name}}\" used outside of binding context.", "'{{name}}' used outside of binding context.",
{name: identifier.name}); {name: identifier.name});
} }
@ -80,8 +53,6 @@ module.exports = function(context) {
return; return;
} }
var isGlobal = context.getScope().type === "global";
// Defines a predicate to check whether or not a given reference is outside of valid scope. // Defines a predicate to check whether or not a given reference is outside of valid scope.
var scopeRange = stack[stack.length - 1]; var scopeRange = stack[stack.length - 1];
@ -99,23 +70,16 @@ module.exports = function(context) {
// Gets declared variables, and checks its references. // Gets declared variables, and checks its references.
var variables = context.getDeclaredVariables(node); var variables = context.getDeclaredVariables(node);
for (var i = 0; i < variables.length; ++i) { for (var i = 0; i < variables.length; ++i) {
var variable = variables[i];
var references = variable.references;
// Global variables are not resolved.
// In this case, use unresolved references.
if (isGlobal && variable.name in unresolvedReferences) {
references = unresolvedReferences[variable.name];
}
// Reports. // Reports.
references.filter(isOutsideOfScope).forEach(report); variables[i]
.references
.filter(isOutsideOfScope)
.forEach(report);
} }
} }
return { return {
"Program": function(node) { "Program": function(node) {
unresolvedReferences = collectUnresolvedReferences(context);
stack = [node.range]; stack = [node.range];
}, },

4
tools/eslint/lib/rules/block-spacing.js

@ -82,7 +82,7 @@ module.exports = function(context) {
context.report({ context.report({
node: node, node: node,
loc: openBrace.loc.start, loc: openBrace.loc.start,
message: message + " after \"{\".", message: message + " after '{'.",
fix: function(fixer) { fix: function(fixer) {
if (always) { if (always) {
return fixer.insertTextBefore(firstToken, " "); return fixer.insertTextBefore(firstToken, " ");
@ -96,7 +96,7 @@ module.exports = function(context) {
context.report({ context.report({
node: node, node: node,
loc: closeBrace.loc.start, loc: closeBrace.loc.start,
message: message + " before \"}\".", message: message + " before '}'.",
fix: function(fixer) { fix: function(fixer) {
if (always) { if (always) {
return fixer.insertTextAfter(lastToken, " "); return fixer.insertTextAfter(lastToken, " ");

46
tools/eslint/lib/rules/brace-style.js

@ -10,8 +10,9 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var style = context.options[0] || "1tbs"; var style = context.options[0] || "1tbs",
var params = context.options[1] || {}; params = context.options[1] || {},
sourceCode = context.getSourceCode();
var OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.", var OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.",
OPEN_MESSAGE_ALLMAN = "Opening curly brace appears on the same line as controlling statement.", OPEN_MESSAGE_ALLMAN = "Opening curly brace appears on the same line as controlling statement.",
@ -63,9 +64,9 @@ module.exports = function(context) {
return; return;
} }
previousToken = context.getTokenBefore(block); previousToken = sourceCode.getTokenBefore(block);
curlyToken = context.getFirstToken(block); curlyToken = sourceCode.getFirstToken(block);
curlyTokenEnd = context.getLastToken(block); curlyTokenEnd = sourceCode.getLastToken(block);
curlyTokensOnSameLine = curlyToken.loc.start.line === curlyTokenEnd.loc.start.line; curlyTokensOnSameLine = curlyToken.loc.start.line === curlyTokenEnd.loc.start.line;
if (style !== "allman" && previousToken.loc.start.line !== curlyToken.loc.start.line) { if (style !== "allman" && previousToken.loc.start.line !== curlyToken.loc.start.line) {
@ -94,29 +95,22 @@ module.exports = function(context) {
* @private * @private
*/ */
function checkIfStatement(node) { function checkIfStatement(node) {
var tokens, var tokens;
alternateIsBlock = false,
alternateIsIfBlock = false;
checkBlock("consequent", "alternate")(node); checkBlock("consequent", "alternate")(node);
if (node.alternate) { if (node.alternate) {
alternateIsBlock = isBlock(node.alternate); tokens = sourceCode.getTokensBefore(node.alternate, 2);
alternateIsIfBlock = node.alternate.type === "IfStatement" && isBlock(node.alternate.consequent);
if (alternateIsBlock || alternateIsIfBlock) {
tokens = context.getTokensBefore(node.alternate, 2);
if (style === "1tbs") { if (style === "1tbs") {
if (tokens[0].loc.start.line !== tokens[1].loc.start.line && if (tokens[0].loc.start.line !== tokens[1].loc.start.line &&
node.consequent.type === "BlockStatement" && node.consequent.type === "BlockStatement" &&
isCurlyPunctuator(tokens[0]) ) { isCurlyPunctuator(tokens[0]) ) {
context.report(node.alternate, CLOSE_MESSAGE); context.report(node.alternate, CLOSE_MESSAGE);
}
} else if (tokens[0].loc.start.line === tokens[1].loc.start.line) {
context.report(node.alternate, CLOSE_MESSAGE_STROUSTRUP_ALLMAN);
} }
} else if (tokens[0].loc.start.line === tokens[1].loc.start.line) {
context.report(node.alternate, CLOSE_MESSAGE_STROUSTRUP_ALLMAN);
} }
} }
@ -134,7 +128,7 @@ module.exports = function(context) {
checkBlock("block", "finalizer")(node); checkBlock("block", "finalizer")(node);
if (isBlock(node.finalizer)) { if (isBlock(node.finalizer)) {
tokens = context.getTokensBefore(node.finalizer, 2); tokens = sourceCode.getTokensBefore(node.finalizer, 2);
if (style === "1tbs") { if (style === "1tbs") {
if (tokens[0].loc.start.line !== tokens[1].loc.start.line) { if (tokens[0].loc.start.line !== tokens[1].loc.start.line) {
context.report(node.finalizer, CLOSE_MESSAGE); context.report(node.finalizer, CLOSE_MESSAGE);
@ -152,8 +146,8 @@ module.exports = function(context) {
* @private * @private
*/ */
function checkCatchClause(node) { function checkCatchClause(node) {
var previousToken = context.getTokenBefore(node), var previousToken = sourceCode.getTokenBefore(node),
firstToken = context.getFirstToken(node); firstToken = sourceCode.getFirstToken(node);
checkBlock("body")(node); checkBlock("body")(node);
@ -179,9 +173,9 @@ module.exports = function(context) {
function checkSwitchStatement(node) { function checkSwitchStatement(node) {
var tokens; var tokens;
if (node.cases && node.cases.length) { if (node.cases && node.cases.length) {
tokens = context.getTokensBefore(node.cases[0], 2); tokens = sourceCode.getTokensBefore(node.cases[0], 2);
} else { } else {
tokens = context.getLastTokens(node, 3); tokens = sourceCode.getLastTokens(node, 3);
} }
if (style !== "allman" && tokens[0].loc.start.line !== tokens[1].loc.start.line) { if (style !== "allman" && tokens[0].loc.start.line !== tokens[1].loc.start.line) {

3
tools/eslint/lib/rules/callback-return.js

@ -49,7 +49,6 @@ module.exports = function(context) {
* @returns {boolean} Whether or not this is part of a callback expression * @returns {boolean} Whether or not this is part of a callback expression
*/ */
function isCallbackExpression(node, parentNode) { function isCallbackExpression(node, parentNode) {
// ensure the parent node exists and is an expression // ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") { if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false; return false;
@ -66,6 +65,8 @@ module.exports = function(context) {
return true; return true;
} }
} }
return false;
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------

15
tools/eslint/lib/rules/camelcase.js

@ -16,6 +16,9 @@ module.exports = function(context) {
// Helpers // Helpers
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
var reported = [];
/** /**
* Checks if a string contains an underscore and isn't all upper-case * Checks if a string contains an underscore and isn't all upper-case
* @param {String} name The string to check. * @param {String} name The string to check.
@ -35,7 +38,10 @@ module.exports = function(context) {
* @private * @private
*/ */
function report(node) { function report(node) {
context.report(node, "Identifier '{{name}}' is not in camel case.", { name: node.name }); if (reported.indexOf(node) < 0) {
reported.push(node);
context.report(node, "Identifier '{{name}}' is not in camel case.", { name: node.name });
}
} }
var options = context.options[0] || {}, var options = context.options[0] || {},
@ -48,7 +54,6 @@ module.exports = function(context) {
return { return {
"Identifier": function(node) { "Identifier": function(node) {
// Leading and trailing underscores are commonly used to flag private/protected identifiers, strip them // Leading and trailing underscores are commonly used to flag private/protected identifiers, strip them
var name = node.name.replace(/^_+|_+$/g, ""), var name = node.name.replace(/^_+|_+$/g, ""),
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
@ -78,12 +83,16 @@ module.exports = function(context) {
// Properties have their own rules // Properties have their own rules
} else if (node.parent.type === "Property") { } else if (node.parent.type === "Property") {
// "never" check properties // "never" check properties
if (properties === "never") { if (properties === "never") {
return; return;
} }
if (node.parent.parent && node.parent.parent.type === "ObjectPattern" &&
node.parent.key === node && node.parent.value !== node) {
return;
}
if (isUnderscored(name) && effectiveParent.type !== "CallExpression") { if (isUnderscored(name) && effectiveParent.type !== "CallExpression") {
report(node); report(node);
} }

30
tools/eslint/lib/rules/comma-dangle.js

@ -70,7 +70,7 @@ module.exports = function(context) {
* This rule handles a given node as multiline when the closing parenthesis * This rule handles a given node as multiline when the closing parenthesis
* and the last element are not on the same line. * and the last element are not on the same line.
* *
* @param {ASTNode} node - A ndoe to check. * @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is multiline. * @returns {boolean} `true` if the node is multiline.
*/ */
function isMultiline(node) { function isMultiline(node) {
@ -81,7 +81,13 @@ module.exports = function(context) {
var sourceCode = context.getSourceCode(), var sourceCode = context.getSourceCode(),
penultimateToken = sourceCode.getLastToken(lastItem), penultimateToken = sourceCode.getLastToken(lastItem),
lastToken = sourceCode.getLastToken(node); lastToken = sourceCode.getTokenAfter(penultimateToken);
// parentheses are a pain
while (lastToken.value === ")") {
penultimateToken = lastToken;
lastToken = sourceCode.getTokenAfter(lastToken);
}
if (lastToken.value === ",") { if (lastToken.value === ",") {
penultimateToken = lastToken; penultimateToken = lastToken;
@ -181,12 +187,30 @@ module.exports = function(context) {
} }
} }
/**
* Only if a given node is not multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
*
* @param {ASTNode} node - A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function allowTrailingCommaIfMultiline(node) {
if (!isMultiline(node)) {
forbidTrailingComma(node);
}
}
// Chooses a checking function. // Chooses a checking function.
var checkForTrailingComma; var checkForTrailingComma;
if (mode === "always") { if (mode === "always") {
checkForTrailingComma = forceTrailingComma; checkForTrailingComma = forceTrailingComma;
} else if (mode === "always-multiline") { } else if (mode === "always-multiline") {
checkForTrailingComma = forceTrailingCommaIfMultiline; checkForTrailingComma = forceTrailingCommaIfMultiline;
} else if (mode === "only-multiline") {
checkForTrailingComma = allowTrailingCommaIfMultiline;
} else { } else {
checkForTrailingComma = forbidTrailingComma; checkForTrailingComma = forbidTrailingComma;
} }
@ -203,6 +227,6 @@ module.exports = function(context) {
module.exports.schema = [ module.exports.schema = [
{ {
"enum": ["always", "always-multiline", "never"] "enum": ["always", "always-multiline", "only-multiline", "never"]
} }
]; ];

2
tools/eslint/lib/rules/comma-spacing.js

@ -95,7 +95,7 @@ module.exports = function(context) {
} }
if (tokens.right && !options.after && tokens.right.type === "Line") { if (tokens.right && !options.after && tokens.right.type === "Line") {
return false; return;
} }
if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) && if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&

5
tools/eslint/lib/rules/complexity.js

@ -12,7 +12,7 @@
module.exports = function(context) { module.exports = function(context) {
var THRESHOLD = context.options[0]; var THRESHOLD = (typeof context.options[0] !== "undefined") ? context.options[0] : 20;
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Helpers // Helpers
@ -116,6 +116,7 @@ module.exports = function(context) {
module.exports.schema = [ module.exports.schema = [
{ {
"type": "integer" "type": "integer",
"minimum": 0
} }
]; ];

135
tools/eslint/lib/rules/consistent-return.js

@ -5,71 +5,108 @@
"use strict"; "use strict";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { /**
* Checks whether or not a given code path segment is unreachable.
var functions = []; * @param {CodePathSegment} segment - A CodePathSegment to check.
* @returns {boolean} `true` if the segment is unreachable.
*/
function isUnreachable(segment) {
return !segment.reachable;
}
//-------------------------------------------------------------------------- //------------------------------------------------------------------------------
// Helpers // Rule Definition
//-------------------------------------------------------------------------- //------------------------------------------------------------------------------
/** module.exports = function(context) {
* Marks entrance into a function by pushing a new object onto the functions var funcInfo = null;
* stack.
* @returns {void}
* @private
*/
function enterFunction() {
functions.push({});
}
/** /**
* Marks exit of a function by popping off the functions stack. * Checks whether of not the implicit returning is consistent if the last
* code path segment is reachable.
*
* @param {ASTNode} node - A program/function node to check.
* @returns {void} * @returns {void}
* @private
*/ */
function exitFunction() { function checkLastSegment(node) {
functions.pop(); var loc, type;
}
// Skip if it expected no return value or unreachable.
// When unreachable, all paths are returned or thrown.
if (!funcInfo.hasReturnValue ||
funcInfo.codePath.currentSegments.every(isUnreachable)
) {
return;
}
// Adjust a location and a message.
if (node.type === "Program") {
// The head of program.
loc = {line: 1, column: 0};
type = "program";
} else if (node.type === "ArrowFunctionExpression") {
// `=>` token
loc = context.getSourceCode().getTokenBefore(node.body).loc.start;
type = "function";
} else if (
node.parent.type === "MethodDefinition" ||
(node.parent.type === "Property" && node.parent.method)
) {
// Method name.
loc = node.parent.key.loc.start;
type = "method";
} else {
// Function name or `function` keyword.
loc = (node.id || node).loc.start;
type = "function";
}
//-------------------------------------------------------------------------- // Reports.
// Public context.report({
//-------------------------------------------------------------------------- node: node,
loc: loc,
message: "Expected to return a value at the end of this {{type}}.",
data: {type: type}
});
}
return { return {
// Initializes/Disposes state of each code path.
"Program": enterFunction, "onCodePathStart": function(codePath) {
"FunctionDeclaration": enterFunction, funcInfo = {
"FunctionExpression": enterFunction, upper: funcInfo,
"ArrowFunctionExpression": enterFunction, codePath: codePath,
hasReturn: false,
"Program:exit": exitFunction, hasReturnValue: false,
"FunctionDeclaration:exit": exitFunction, message: ""
"FunctionExpression:exit": exitFunction, };
"ArrowFunctionExpression:exit": exitFunction, },
"onCodePathEnd": function() {
funcInfo = funcInfo.upper;
},
// Reports a given return statement if it's inconsistent.
"ReturnStatement": function(node) { "ReturnStatement": function(node) {
var hasReturnValue = Boolean(node.argument);
var returnInfo = functions[functions.length - 1],
returnTypeDefined = "type" in returnInfo; if (!funcInfo.hasReturn) {
funcInfo.hasReturn = true;
if (returnTypeDefined) { funcInfo.hasReturnValue = hasReturnValue;
funcInfo.message = "Expected " + (hasReturnValue ? "a" : "no") + " return value.";
if (returnInfo.type !== !!node.argument) { } else if (funcInfo.hasReturnValue !== hasReturnValue) {
context.report(node, "Expected " + (returnInfo.type ? "a" : "no") + " return value."); context.report({node: node, message: funcInfo.message});
}
} else {
returnInfo.type = !!node.argument;
} }
},
} // Reports a given program/function if the implicit returning is not consistent.
"Program:exit": checkLastSegment,
"FunctionDeclaration:exit": checkLastSegment,
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
}; };
}; };
module.exports.schema = []; module.exports.schema = [];

68
tools/eslint/lib/rules/consistent-this.js

@ -11,15 +11,22 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var alias = context.options[0]; var aliases = [];
if (context.options.length === 0) {
aliases.push("that");
} else {
aliases = context.options;
}
/** /**
* Reports that a variable declarator or assignment expression is assigning * Reports that a variable declarator or assignment expression is assigning
* a non-'this' value to the specified alias. * a non-'this' value to the specified alias.
* @param {ASTNode} node - The assigning node. * @param {ASTNode} node - The assigning node.
* @param {string} alias - the name of the alias that was incorrectly used.
* @returns {void} * @returns {void}
*/ */
function reportBadAssignment(node) { function reportBadAssignment(node, alias) {
context.report(node, context.report(node,
"Designated alias '{{alias}}' is not assigned to 'this'.", "Designated alias '{{alias}}' is not assigned to 'this'.",
{ alias: alias }); { alias: alias });
@ -36,9 +43,9 @@ module.exports = function(context) {
function checkAssignment(node, name, value) { function checkAssignment(node, name, value) {
var isThis = value.type === "ThisExpression"; var isThis = value.type === "ThisExpression";
if (name === alias) { if (aliases.indexOf(name) !== -1) {
if (!isThis || node.operator && node.operator !== "=") { if (!isThis || node.operator && node.operator !== "=") {
reportBadAssignment(node); reportBadAssignment(node, name);
} }
} else if (isThis) { } else if (isThis) {
context.report(node, context.report(node,
@ -49,41 +56,55 @@ module.exports = function(context) {
/** /**
* Ensures that a variable declaration of the alias in a program or function * Ensures that a variable declaration of the alias in a program or function
* is assigned to the correct value. * is assigned to the correct value.
* @param {string} alias alias the check the assignment of.
* @param {object} scope scope of the current code we are checking.
* @private
* @returns {void} * @returns {void}
*/ */
function ensureWasAssigned() { function checkWasAssigned(alias, scope) {
var scope = context.getScope();
var variable = scope.set.get(alias); var variable = scope.set.get(alias);
if (!variable) { if (!variable) {
return; return;
} }
if (variable.defs.some(function(def) { if (variable.defs.some(function(def) {
return def.node.type === "VariableDeclarator" && return def.node.type === "VariableDeclarator" &&
def.node.init !== null; def.node.init !== null;
})) { })) {
return; return;
} }
var lookup = (variable.references.length === 0 && scope.type === "global") ? scope : variable;
// The alias has been declared and not assigned: check it was // The alias has been declared and not assigned: check it was
// assigned later in the same scope. // assigned later in the same scope.
if (!lookup.references.some(function(reference) { if (!variable.references.some(function(reference) {
var write = reference.writeExpr; var write = reference.writeExpr;
return (
if (reference.from === scope && reference.from === scope &&
write && write.type === "ThisExpression" && write && write.type === "ThisExpression" &&
write.parent.operator === "=") { write.parent.operator === "="
return true; );
}
})) { })) {
variable.defs.map(function(def) { variable.defs.map(function(def) {
return def.node; return def.node;
}).forEach(reportBadAssignment); }).forEach(function(node) {
reportBadAssignment(node, alias);
});
} }
} }
/**
* Check each alias to ensure that is was assinged to the correct value.
* @returns {void}
*/
function ensureWasAssigned() {
var scope = context.getScope();
aliases.forEach(function(alias) {
checkWasAssigned(alias, scope);
});
}
return { return {
"Program:exit": ensureWasAssigned, "Program:exit": ensureWasAssigned,
"FunctionExpression:exit": ensureWasAssigned, "FunctionExpression:exit": ensureWasAssigned,
@ -108,8 +129,11 @@ module.exports = function(context) {
}; };
module.exports.schema = [ module.exports.schema = {
{ "type": "array",
"type": "string" "items": {
} "type": "string",
]; "minLength": 1
},
"uniqueItems": true
};

217
tools/eslint/lib/rules/constructor-super.js

@ -6,101 +6,208 @@
"use strict"; "use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is a constructor.
* @param {ASTNode} node - A node to check. This node type is one of
* `Program`, `FunctionDeclaration`, `FunctionExpression`, and
* `ArrowFunctionExpression`.
* @returns {boolean} `true` if the node is a constructor.
*/
function isConstructorFunction(node) {
return (
node.type === "FunctionExpression" &&
node.parent.type === "MethodDefinition" &&
node.parent.kind === "constructor"
);
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
// {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
// Information for each constructor.
// - upper: Information of the upper constructor.
// - hasExtends: A flag which shows whether own class has a valid `extends`
// part.
// - scope: The scope of own class.
// - codePath: The code path object of the constructor.
var funcInfo = null;
/** // {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
* Searches a class node from ancestors of a node. // Information for each code path segment.
* @param {Node} node - A node to get. // - calledInSomePaths: A flag of be called `super()` in some code paths.
* @returns {ClassDeclaration|ClassExpression|null} the found class node or `null`. // - calledInEveryPaths: A flag of be called `super()` in all code paths.
*/ var segInfoMap = Object.create(null);
function getClassInAncestor(node) {
while (node) {
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
return node;
}
node = node.parent;
}
/* istanbul ignore next */
return null;
}
/** /**
* Checks whether or not a node is the null literal. * Gets the flag which shows `super()` is called in some paths.
* @param {Node} node - A node to check. * @param {CodePathSegment} segment - A code path segment to get.
* @returns {boolean} whether or not a node is the null literal. * @returns {boolean} The flag which shows `super()` is called in some paths
*/ */
function isNullLiteral(node) { function isCalledInSomePath(segment) {
return node && node.type === "Literal" && node.value === null; return segInfoMap[segment.id].calledInSomePaths;
} }
/** /**
* Checks whether or not the current traversal context is on constructors. * Gets the flag which shows `super()` is called in all paths.
* @param {{scope: Scope}} item - A checking context to check. * @param {CodePathSegment} segment - A code path segment to get.
* @returns {boolean} whether or not the current traversal context is on constructors. * @returns {boolean} The flag which shows `super()` is called in all paths.
*/ */
function isOnConstructor(item) { function isCalledInEveryPath(segment) {
return item && item.scope === context.getScope().variableScope.upper.variableScope; return segInfoMap[segment.id].calledInEveryPaths;
} }
// A stack for checking context.
var stack = [];
return { return {
/** /**
* Start checking. * Stacks a constructor information.
* @param {MethodDefinition} node - A target node. * @param {CodePath} codePath - A code path which was started.
* @param {ASTNode} node - The current node.
* @returns {void} * @returns {void}
*/ */
"MethodDefinition": function(node) { "onCodePathStart": function(codePath, node) {
if (node.kind !== "constructor") { if (!isConstructorFunction(node)) {
return; return;
} }
stack.push({
superCallings: [], // Class > ClassBody > MethodDefinition > FunctionExpression
scope: context.getScope().variableScope var classNode = node.parent.parent.parent;
}); funcInfo = {
upper: funcInfo,
hasExtends: Boolean(
classNode.superClass &&
!astUtils.isNullOrUndefined(classNode.superClass)
),
scope: context.getScope(),
codePath: codePath
};
}, },
/** /**
* Checks the result, then reports invalid/missing `super()`. * Pops a constructor information.
* @param {MethodDefinition} node - A target node. * And reports if `super()` lacked.
* @param {CodePath} codePath - A code path which was ended.
* @param {ASTNode} node - The current node.
* @returns {void} * @returns {void}
*/ */
"MethodDefinition:exit": function(node) { "onCodePathEnd": function(codePath, node) {
if (node.kind !== "constructor") { if (!isConstructorFunction(node)) {
return; return;
} }
var result = stack.pop();
var classNode = getClassInAncestor(node); // Skip if own class which has a valid `extends` part.
/* istanbul ignore if */ var hasExtends = funcInfo.hasExtends;
if (!classNode) { funcInfo = funcInfo.upper;
if (!hasExtends) {
return; return;
} }
if (classNode.superClass === null || isNullLiteral(classNode.superClass)) { // Reports if `super()` lacked.
result.superCallings.forEach(function(superCalling) { var segments = codePath.returnedSegments;
context.report(superCalling, "unexpected `super()`."); var calledInEveryPaths = segments.every(isCalledInEveryPath);
var calledInSomePaths = segments.some(isCalledInSomePath);
if (!calledInEveryPaths) {
context.report({
message: calledInSomePaths ?
"Lacked a call of 'super()' in some code paths." :
"Expected to call 'super()'.",
node: node.parent
}); });
} else if (result.superCallings.length === 0) {
context.report(node.key, "this constructor requires `super()`.");
} }
}, },
/** /**
* Checks the result of checking, then reports invalid/missing `super()`. * Initialize information of a given code path segment.
* @param {MethodDefinition} node - A target node. * @param {CodePathSegment} segment - A code path segment to initialize.
* @returns {void} * @returns {void}
*/ */
"CallExpression": function(node) { "onCodePathSegmentStart": function(segment) {
var item = stack[stack.length - 1]; // Skip if this is not in a constructor of a class which has a valid
if (isOnConstructor(item) && node.callee.type === "Super") { // `extends` part.
item.superCallings.push(node); if (!(
funcInfo &&
funcInfo.hasExtends &&
funcInfo.scope === context.getScope().variableScope
)) {
return;
}
// Initialize info.
var info = segInfoMap[segment.id] = {
calledInSomePaths: false,
calledInEveryPaths: false
};
// When there are previous segments, aggregates these.
var prevSegments = segment.prevSegments;
if (prevSegments.length > 0) {
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
} }
},
/**
* Checks for a call of `super()`.
* @param {ASTNode} node - A CallExpression node to check.
* @returns {void}
*/
"CallExpression:exit": function(node) {
// Skip if the node is not `super()`.
if (node.callee.type !== "Super") {
return;
}
// Skip if this is not in a constructor.
if (!(funcInfo && funcInfo.scope === context.getScope().variableScope)) {
return;
}
// Reports if needed.
if (funcInfo.hasExtends) {
// This class has a valid `extends` part.
// Checks duplicate `super()`;
var segments = funcInfo.codePath.currentSegments;
var duplicate = false;
for (var i = 0; i < segments.length; ++i) {
var info = segInfoMap[segments[i].id];
duplicate = duplicate || info.calledInSomePaths;
info.calledInSomePaths = info.calledInEveryPaths = true;
}
if (duplicate) {
context.report({
message: "Unexpected duplicate 'super()'.",
node: node
});
}
} else {
// This class does not have a valid `extends` part.
// Disallow `super()`.
context.report({
message: "Unexpected 'super()'.",
node: node
});
}
},
/**
* Resets state.
* @returns {void}
*/
"Program:exit": function() {
segInfoMap = Object.create(null);
} }
}; };
}; };

14
tools/eslint/lib/rules/curly.js

@ -264,22 +264,16 @@ module.exports.schema = {
{ {
"type": "array", "type": "array",
"items": [ "items": [
{
"enum": [0, 1, 2]
},
{ {
"enum": ["all"] "enum": ["all"]
} }
], ],
"minItems": 1, "minItems": 0,
"maxItems": 2 "maxItems": 1
}, },
{ {
"type": "array", "type": "array",
"items": [ "items": [
{
"enum": [0, 1, 2]
},
{ {
"enum": ["multi", "multi-line", "multi-or-nest"] "enum": ["multi", "multi-line", "multi-or-nest"]
}, },
@ -287,8 +281,8 @@ module.exports.schema = {
"enum": ["consistent"] "enum": ["consistent"]
} }
], ],
"minItems": 1, "minItems": 0,
"maxItems": 3 "maxItems": 2
} }
] ]
}; };

3
tools/eslint/lib/rules/eol-last.js

@ -22,9 +22,6 @@ module.exports = function(context) {
location = {column: 1}, location = {column: 1},
linebreakStyle = context.options[0] || "unix", linebreakStyle = context.options[0] || "unix",
linebreak = linebreakStyle === "unix" ? "\n" : "\r\n"; linebreak = linebreakStyle === "unix" ? "\n" : "\r\n";
if (src.length === 0) {
return;
}
if (src[src.length - 1] !== "\n") { if (src[src.length - 1] !== "\n") {
// file is not newline-terminated // file is not newline-terminated

22
tools/eslint/lib/rules/eqeqeq.js

@ -13,12 +13,6 @@
module.exports = function(context) { module.exports = function(context) {
var sourceCode = context.getSourceCode(),
replacements = {
"==": "===",
"!=": "!=="
};
/** /**
* Checks if an expression is a typeof expression * Checks if an expression is a typeof expression
* @param {ASTNode} node The node to check * @param {ASTNode} node The node to check
@ -91,21 +85,7 @@ module.exports = function(context) {
node: node, node: node,
loc: getOperatorLocation(node), loc: getOperatorLocation(node),
message: "Expected '{{op}}=' and instead saw '{{op}}'.", message: "Expected '{{op}}=' and instead saw '{{op}}'.",
data: { op: node.operator }, data: { op: node.operator }
fix: function(fixer) {
var tokens = sourceCode.getTokensBetween(node.left, node.right),
opToken,
i;
for (i = 0; i < tokens.length; ++i) {
if (tokens[i].value === node.operator) {
opToken = tokens[i];
break;
}
}
return fixer.replaceTextRange(opToken.range, replacements[node.operator]);
}
}); });
} }

2
tools/eslint/lib/rules/func-style.js

@ -24,7 +24,7 @@ module.exports = function(context) {
"FunctionDeclaration": function(node) { "FunctionDeclaration": function(node) {
stack.push(false); stack.push(false);
if (!enforceDeclarations) { if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
context.report(node, "Expected a function expression."); context.report(node, "Expected a function expression.");
} }
}, },

38
tools/eslint/lib/rules/global-require.js

@ -17,11 +17,45 @@ var ACCEPTABLE_PARENTS = [
"VariableDeclaration" "VariableDeclaration"
]; ];
/**
* Finds the escope reference in the given scope.
* @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node.
* @returns {Reference|null} Returns the found reference or null if none were found.
*/
function findReference(scope, node) {
var references = scope.references.filter(function(reference) {
return reference.identifier.range[0] === node.range[0] &&
reference.identifier.range[1] === node.range[1];
});
/* istanbul ignore else: correctly returns null */
if (references.length === 1) {
return references[0];
} else {
return null;
}
}
/**
* Checks if the given identifier node is shadowed in the given scope.
* @param {Object} scope The current scope.
* @param {ASTNode} node The identifier node to check.
* @returns {boolean} Whether or not the name is shadowed.
*/
function isShadowed(scope, node) {
var reference = findReference(scope, node);
return reference && reference.resolved && reference.resolved.defs.length > 0;
}
module.exports = function(context) { module.exports = function(context) {
return { return {
"CallExpression": function(node) { "CallExpression": function(node) {
if (node.callee.name === "require") { var currentScope = context.getScope(),
var isGoodRequire = context.getAncestors().every(function(parent) { isGoodRequire;
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
isGoodRequire = context.getAncestors().every(function(parent) {
return ACCEPTABLE_PARENTS.indexOf(parent.type) > -1; return ACCEPTABLE_PARENTS.indexOf(parent.type) > -1;
}); });
if (!isGoodRequire) { if (!isGoodRequire) {

110
tools/eslint/lib/rules/id-blacklist.js

@ -0,0 +1,110 @@
/**
* @fileoverview Rule that warns when identifier names that are
blacklisted in the configuration are used.
* @author Keith Cirkel (http://keithcirkel.co.uk)
* Based on id-match rule:
* @author Matthieu Larcher
* @copyright 2015 Matthieu Larcher. All rights reserved.
* See LICENSE in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
var blacklist = context.options;
/**
* Checks if a string matches the provided pattern
* @param {String} name The string to check.
* @returns {boolean} if the string is a match
* @private
*/
function isInvalid(name) {
return blacklist.indexOf(name) !== -1;
}
/**
* Verifies if we should report an error or not based on the effective
* parent node and the identifier name.
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
* @param {String} name The identifier name of the identifier node
* @returns {boolean} whether an error should be reported or not
*/
function shouldReport(effectiveParent, name) {
return effectiveParent.type !== "CallExpression"
&& effectiveParent.type !== "NewExpression" &&
isInvalid(name);
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
context.report(node, "Identifier '{{name}}' is blacklisted", {
name: node.name
});
}
return {
"Identifier": function(node) {
var name = node.name,
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
// MemberExpressions get special rules
if (node.parent.type === "MemberExpression") {
// Always check object names
if (node.parent.object.type === "Identifier" &&
node.parent.object.name === node.name) {
if (isInvalid(name)) {
report(node);
}
// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" &&
(effectiveParent.right.type !== "MemberExpression" ||
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === node.name)) {
if (isInvalid(name)) {
report(node);
}
}
// Properties have their own rules
} else if (node.parent.type === "Property") {
if (shouldReport(effectiveParent, name)) {
report(node);
}
// Report anything that is a match and not a CallExpression
} else if (shouldReport(effectiveParent, name)) {
report(node);
}
}
};
};
module.exports.schema = {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
};

53
tools/eslint/lib/rules/indent.js

@ -32,7 +32,7 @@
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var util = require("util"); var util = require("util");
var assign = require("object-assign"); var lodash = require("lodash");
module.exports = function(context) { module.exports = function(context) {
@ -70,7 +70,7 @@ module.exports = function(context) {
const: variableDeclaratorRules const: variableDeclaratorRules
}; };
} else if (typeof variableDeclaratorRules === "object") { } else if (typeof variableDeclaratorRules === "object") {
assign(options.VariableDeclarator, variableDeclaratorRules); lodash.assign(options.VariableDeclarator, variableDeclaratorRules);
} }
} }
} }
@ -321,7 +321,7 @@ module.exports = function(context) {
(calleeNode.parent.type === "Property" || (calleeNode.parent.type === "Property" ||
calleeNode.parent.type === "ArrayExpression")) { calleeNode.parent.type === "ArrayExpression")) {
// If function is part of array or object, comma can be put at left // If function is part of array or object, comma can be put at left
indent = getNodeIndent(calleeNode, false, true); indent = getNodeIndent(calleeNode, false, false);
} else { } else {
// If function is standalone, simple calculate indent // If function is standalone, simple calculate indent
indent = getNodeIndent(calleeNode); indent = getNodeIndent(calleeNode);
@ -429,9 +429,11 @@ module.exports = function(context) {
nodeIndent = getNodeIndent(effectiveParent); nodeIndent = getNodeIndent(effectiveParent);
if (parentVarNode && parentVarNode.loc.start.line !== node.loc.start.line) { if (parentVarNode && parentVarNode.loc.start.line !== node.loc.start.line) {
if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) { if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) {
nodeIndent = nodeIndent + (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]); if (parentVarNode.loc.start.line === effectiveParent.loc.start.line) {
} else if (parent.loc.start.line !== node.loc.start.line && parentVarNode === parentVarNode.parent.declarations[0]) { nodeIndent = nodeIndent + (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]);
nodeIndent = nodeIndent + indentSize; } else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") {
nodeIndent = nodeIndent + indentSize;
}
} }
} else if (!parentVarNode && !isFirstArrayElementOnSameLine(parent) && effectiveParent.type !== "MemberExpression" && effectiveParent.type !== "ExpressionStatement" && effectiveParent.type !== "AssignmentExpression" && effectiveParent.type !== "Property") { } else if (!parentVarNode && !isFirstArrayElementOnSameLine(parent) && effectiveParent.type !== "MemberExpression" && effectiveParent.type !== "ExpressionStatement" && effectiveParent.type !== "AssignmentExpression" && effectiveParent.type !== "Property") {
nodeIndent = nodeIndent + indentSize; nodeIndent = nodeIndent + indentSize;
@ -623,6 +625,8 @@ module.exports = function(context) {
} }
}, },
"ClassBody": blockIndentationCheck,
"BlockStatement": blockIndentationCheck, "BlockStatement": blockIndentationCheck,
"WhileStatement": blockLessNodes, "WhileStatement": blockLessNodes,
@ -684,7 +688,8 @@ module.exports.schema = [
"enum": ["tab"] "enum": ["tab"]
}, },
{ {
"type": "integer" "type": "integer",
"minimum": 0
} }
] ]
}, },
@ -692,21 +697,33 @@ module.exports.schema = [
"type": "object", "type": "object",
"properties": { "properties": {
"SwitchCase": { "SwitchCase": {
"type": "integer" "type": "integer",
"minimum": 0
}, },
"VariableDeclarator": { "VariableDeclarator": {
"type": ["integer", "object"], "oneOf": [
"properties": { {
"var": { "type": "integer",
"type": "integer" "minimum": 0
}, },
"let": { {
"type": "integer" "type": "object",
}, "properties": {
"const": { "var": {
"type": "integer" "type": "integer",
"minimum": 0
},
"let": {
"type": "integer",
"minimum": 0
},
"const": {
"type": "integer",
"minimum": 0
}
}
} }
} ]
} }
}, },
"additionalProperties": false "additionalProperties": false

14
tools/eslint/lib/rules/init-declarations.js

@ -84,22 +84,16 @@ module.exports.schema = {
{ {
"type": "array", "type": "array",
"items": [ "items": [
{
"enum": [0, 1, 2]
},
{ {
"enum": ["always"] "enum": ["always"]
} }
], ],
"minItems": 1, "minItems": 0,
"maxItems": 2 "maxItems": 1
}, },
{ {
"type": "array", "type": "array",
"items": [ "items": [
{
"enum": [0, 1, 2]
},
{ {
"enum": ["never"] "enum": ["never"]
}, },
@ -113,8 +107,8 @@ module.exports.schema = {
"additionalProperties": false "additionalProperties": false
} }
], ],
"minItems": 1, "minItems": 0,
"maxItems": 3 "maxItems": 2
} }
] ]
}; };

18
tools/eslint/lib/rules/jsx-quotes.js

@ -19,11 +19,17 @@ var astUtils = require("../ast-utils");
var QUOTE_SETTINGS = { var QUOTE_SETTINGS = {
"prefer-double": { "prefer-double": {
quote: "\"", quote: "\"",
description: "singlequote" description: "singlequote",
convert: function(str) {
return str.replace(/'/g, "\"");
}
}, },
"prefer-single": { "prefer-single": {
quote: "'", quote: "'",
description: "doublequote" description: "doublequote",
convert: function(str) {
return str.replace(/"/g, "'");
}
} }
}; };
@ -50,7 +56,13 @@ module.exports = function(context) {
var attributeValue = node.value; var attributeValue = node.value;
if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) { if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) {
context.report(attributeValue, "Unexpected usage of {{description}}.", setting); context.report({
node: attributeValue,
message: "Unexpected usage of " + setting.description + ".",
fix: function(fixer) {
return fixer.replaceText(attributeValue, setting.convert(attributeValue.raw));
}
});
} }
} }
}; };

216
tools/eslint/lib/rules/key-spacing.js

@ -72,13 +72,43 @@ function isSingleLine(node) {
return (node.loc.end.line === node.loc.start.line); return (node.loc.end.line === node.loc.start.line);
} }
/** Sets option values from the configured options with defaults
* @param {Object} toOptions Object to be initialized
* @param {Object} fromOptions Object to be initialized from
* @returns {Object} The object with correctly initialized options and values
*/
function initOptions(toOptions, fromOptions) {
toOptions.mode = fromOptions.mode || "strict";
// Set align if exists - multiLine case
if (typeof fromOptions.align !== "undefined") {
toOptions.align = fromOptions.align;
}
// Set value of beforeColon
if (typeof fromOptions.beforeColon !== "undefined") {
toOptions.beforeColon = +fromOptions.beforeColon;
} else {
toOptions.beforeColon = 0;
}
// Set value of afterColon
if (typeof fromOptions.afterColon !== "undefined") {
toOptions.afterColon = +fromOptions.afterColon;
} else {
toOptions.afterColon = 1;
}
return toOptions;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var messages = { var messages = {
key: "{{error}} space after {{computed}}key \"{{key}}\".", key: "{{error}} space after {{computed}}key '{{key}}'.",
value: "{{error}} space before value for {{computed}}key \"{{key}}\"." value: "{{error}} space before value for {{computed}}key '{{key}}'."
}; };
module.exports = function(context) { module.exports = function(context) {
@ -93,10 +123,22 @@ module.exports = function(context) {
*/ */
var options = context.options[0] || {}, var options = context.options[0] || {},
align = options.align, multiLineOptions = initOptions({}, (options.multiLine || options)),
mode = options.mode || "strict", singleLineOptions = initOptions({}, (options.singleLine || options));
beforeColon = +!!options.beforeColon, // Defaults to false
afterColon = +!(options.afterColon === false); // Defaults to true /**
* Determines if the given property is key-value property.
* @param {ASTNode} property Property node to check.
* @returns {Boolean} Whether the property is a key-value property.
*/
function isKeyValueProperty(property) {
return !(
property.method ||
property.shorthand ||
property.kind !== "init" ||
property.type !== "Property" // Could be "ExperimentalSpreadProperty" or "SpreadProperty"
);
}
/** /**
* Starting from the given a node (a property.key node here) looks forward * Starting from the given a node (a property.key node here) looks forward
@ -115,6 +157,21 @@ module.exports = function(context) {
return prevNode; return prevNode;
} }
/**
* Starting from the given a node (a property.key node here) looks forward
* until it finds the colon punctuator and returns it.
* @param {ASTNode} node The node to start looking from.
* @returns {ASTNode} The colon punctuator.
*/
function getNextColon(node) {
while (node && (node.type !== "Punctuator" || node.value !== ":")) {
node = context.getTokenAfter(node);
}
return node;
}
/** /**
* Gets an object literal property's key as the identifier name or string value. * Gets an object literal property's key as the identifier name or string value.
* @param {ASTNode} property Property node whose key to retrieve. * @param {ASTNode} property Property node whose key to retrieve.
@ -137,15 +194,19 @@ module.exports = function(context) {
* @param {string} side Side being verified - either "key" or "value". * @param {string} side Side being verified - either "key" or "value".
* @param {string} whitespace Actual whitespace string. * @param {string} whitespace Actual whitespace string.
* @param {int} expected Expected whitespace length. * @param {int} expected Expected whitespace length.
* @param {string} mode Value of the mode as "strict" or "minimum"
* @returns {void} * @returns {void}
*/ */
function report(property, side, whitespace, expected) { function report(property, side, whitespace, expected, mode) {
var diff = whitespace.length - expected, var diff = whitespace.length - expected,
key = property.key, key = property.key,
firstTokenAfterColon = context.getTokenAfter(key, 1), firstTokenAfterColon = context.getTokenAfter(getNextColon(key)),
location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start; location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start;
if ((diff && mode === "strict" || diff < 0 && mode === "minimum") && if ((
diff && mode === "strict" ||
diff < 0 && mode === "minimum" ||
diff > 0 && !expected && mode === "minimum") &&
!(expected && containsLineTerminator(whitespace)) !(expected && containsLineTerminator(whitespace))
) { ) {
context.report(property[side], location, messages[side], { context.report(property[side], location, messages[side], {
@ -165,11 +226,6 @@ module.exports = function(context) {
function getKeyWidth(property) { function getKeyWidth(property) {
var startToken, endToken; var startToken, endToken;
// Ignore shorthand methods and properties, as they have no colon
if (property.method || property.shorthand) {
return 0;
}
startToken = context.getFirstToken(property); startToken = context.getFirstToken(property);
endToken = getLastTokenBeforeColon(property.key); endToken = getLastTokenBeforeColon(property.key);
@ -192,6 +248,7 @@ module.exports = function(context) {
afterColon: whitespace[2] afterColon: whitespace[2]
}; };
} }
return null;
} }
/** /**
@ -215,7 +272,9 @@ module.exports = function(context) {
} }
return groups; return groups;
}, [[]]); }, [
[]
]);
} }
/** /**
@ -227,7 +286,11 @@ module.exports = function(context) {
var length = properties.length, var length = properties.length,
widths = properties.map(getKeyWidth), // Width of keys, including quotes widths = properties.map(getKeyWidth), // Width of keys, including quotes
targetWidth = Math.max.apply(null, widths), targetWidth = Math.max.apply(null, widths),
i, property, whitespace, width; i, property, whitespace, width,
align = multiLineOptions.align,
beforeColon = multiLineOptions.beforeColon,
afterColon = multiLineOptions.afterColon,
mode = multiLineOptions.mode;
// Conditionally include one space before or after colon // Conditionally include one space before or after colon
targetWidth += (align === "colon" ? beforeColon : afterColon); targetWidth += (align === "colon" ? beforeColon : afterColon);
@ -235,19 +298,16 @@ module.exports = function(context) {
for (i = 0; i < length; i++) { for (i = 0; i < length; i++) {
property = properties[i]; property = properties[i];
whitespace = getPropertyWhitespace(property); whitespace = getPropertyWhitespace(property);
if (whitespace) { // Object literal getters/setters lack a colon
if (!whitespace) { width = widths[i];
continue; // Object literal getters/setters lack a colon
} if (align === "value") {
report(property, "key", whitespace.beforeColon, beforeColon, mode);
width = widths[i]; report(property, "value", whitespace.afterColon, targetWidth - width, mode);
} else { // align = "colon"
if (align === "value") { report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
report(property, "key", whitespace.beforeColon, beforeColon); report(property, "value", whitespace.afterColon, afterColon, mode);
report(property, "value", whitespace.afterColon, targetWidth - width); }
} else { // align = "colon"
report(property, "key", whitespace.beforeColon, targetWidth - width);
report(property, "value", whitespace.afterColon, afterColon);
} }
} }
} }
@ -259,20 +319,21 @@ module.exports = function(context) {
*/ */
function verifyAlignment(node) { function verifyAlignment(node) {
createGroups(node).forEach(function(group) { createGroups(node).forEach(function(group) {
verifyGroupAlignment(group); verifyGroupAlignment(group.filter(isKeyValueProperty));
}); });
} }
/** /**
* Verifies spacing of property conforms to specified options. * Verifies spacing of property conforms to specified options.
* @param {ASTNode} node Property node being evaluated. * @param {ASTNode} node Property node being evaluated.
* @param {Object} lineOptions Configured singleLine or multiLine options
* @returns {void} * @returns {void}
*/ */
function verifySpacing(node) { function verifySpacing(node, lineOptions) {
var whitespace = getPropertyWhitespace(node); var actual = getPropertyWhitespace(node);
if (whitespace) { // Object literal getters/setters lack colons if (actual) { // Object literal getters/setters lack colons
report(node, "key", whitespace.beforeColon, beforeColon); report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
report(node, "value", whitespace.afterColon, afterColon); report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
} }
} }
@ -285,7 +346,7 @@ module.exports = function(context) {
var length = properties.length; var length = properties.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
verifySpacing(properties[i]); verifySpacing(properties[i], singleLineOptions);
} }
} }
@ -293,7 +354,7 @@ module.exports = function(context) {
// Public API // Public API
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
if (align) { // Verify vertical alignment if (multiLineOptions.align) { // Verify vertical alignment
return { return {
"ObjectExpression": function(node) { "ObjectExpression": function(node) {
@ -305,11 +366,11 @@ module.exports = function(context) {
} }
}; };
} else { // Strictly obey beforeColon and afterColon in each property } else { // Obey beforeColon and afterColon in each property as configured
return { return {
"Property": function(node) { "Property": function(node) {
verifySpacing(node); verifySpacing(node, isSingleLine(node) ? singleLineOptions : multiLineOptions);
} }
}; };
@ -317,23 +378,64 @@ module.exports = function(context) {
}; };
module.exports.schema = [ module.exports.schema = [{
{ "anyOf": [
"type": "object", {
"properties": { "type": "object",
"align": { "properties": {
"enum": ["colon", "value"] "align": {
}, "enum": ["colon", "value"]
"mode": { },
"enum": ["strict", "minimum"] "mode": {
}, "enum": ["strict", "minimum"]
"beforeColon": { },
"type": "boolean" "beforeColon": {
"type": "boolean"
},
"afterColon": {
"type": "boolean"
}
}, },
"afterColon": { "additionalProperties": false
"type": "boolean"
}
}, },
"additionalProperties": false {
} "type": "object",
]; "properties": {
"singleLine": {
"type": "object",
"properties": {
"mode": {
"enum": ["strict", "minimum"]
},
"beforeColon": {
"type": "boolean"
},
"afterColon": {
"type": "boolean"
}
},
"additionalProperties": false
},
"multiLine": {
"type": "object",
"properties": {
"align": {
"enum": ["colon", "value"]
},
"mode": {
"enum": ["strict", "minimum"]
},
"beforeColon": {
"type": "boolean"
},
"afterColon": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}];

520
tools/eslint/lib/rules/keyword-spacing.js

@ -0,0 +1,520 @@
/**
* @fileoverview Rule to enforce spacing before and after keywords.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var astUtils = require("../ast-utils"),
keywords = require("../util/keywords");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
var PREV_TOKEN = /^[\)\]\}>]$/;
var NEXT_TOKEN = /^(?:[\(\[\{<~!]|\+\+?|--?)$/;
var PREV_TOKEN_M = /^[\)\]\}>*]$/;
var NEXT_TOKEN_M = /^[\{*]$/;
var TEMPLATE_OPEN_PAREN = /\$\{$/;
var TEMPLATE_CLOSE_PAREN = /^\}/;
var CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/;
var KEYS = keywords.concat(["as", "await", "from", "get", "let", "of", "set", "yield"]);
// check duplications.
(function() {
KEYS.sort();
for (var i = 1; i < KEYS.length; ++i) {
if (KEYS[i] === KEYS[i - 1]) {
throw new Error("Duplication was found in the keyword list: " + KEYS[i]);
}
}
}());
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given token is a "Template" token ends with "${".
*
* @param {Token} token - A token to check.
* @returns {boolean} `true` if the token is a "Template" token ends with "${".
*/
function isOpenParenOfTemplate(token) {
return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
}
/**
* Checks whether or not a given token is a "Template" token starts with "}".
*
* @param {Token} token - A token to check.
* @returns {boolean} `true` if the token is a "Template" token starts with "}".
*/
function isCloseParenOfTemplate(token) {
return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var sourceCode = context.getSourceCode();
/**
* Reports a given token if there are not space(s) before the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function expectSpaceBefore(token, pattern) {
pattern = pattern || PREV_TOKEN;
var prevToken = sourceCode.getTokenBefore(token);
if (prevToken &&
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
!isOpenParenOfTemplate(prevToken) &&
astUtils.isTokenOnSameLine(prevToken, token) &&
!sourceCode.isSpaceBetweenTokens(prevToken, token)
) {
context.report({
loc: token.loc.start,
message: "Expected space(s) before \"{{value}}\".",
data: token,
fix: function(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
}
/**
* Reports a given token if there are space(s) before the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function unexpectSpaceBefore(token, pattern) {
pattern = pattern || PREV_TOKEN;
var prevToken = sourceCode.getTokenBefore(token);
if (prevToken &&
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
!isOpenParenOfTemplate(prevToken) &&
astUtils.isTokenOnSameLine(prevToken, token) &&
sourceCode.isSpaceBetweenTokens(prevToken, token)
) {
context.report({
loc: token.loc.start,
message: "Unexpected space(s) before \"{{value}}\".",
data: token,
fix: function(fixer) {
return fixer.removeRange([prevToken.range[1], token.range[0]]);
}
});
}
}
/**
* Reports a given token if there are not space(s) after the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function expectSpaceAfter(token, pattern) {
pattern = pattern || NEXT_TOKEN;
var nextToken = sourceCode.getTokenAfter(token);
if (nextToken &&
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
!isCloseParenOfTemplate(nextToken) &&
astUtils.isTokenOnSameLine(token, nextToken) &&
!sourceCode.isSpaceBetweenTokens(token, nextToken)
) {
context.report({
loc: token.loc.start,
message: "Expected space(s) after \"{{value}}\".",
data: token,
fix: function(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
}
/**
* Reports a given token if there are space(s) after the token.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function unexpectSpaceAfter(token, pattern) {
pattern = pattern || NEXT_TOKEN;
var nextToken = sourceCode.getTokenAfter(token);
if (nextToken &&
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
!isCloseParenOfTemplate(nextToken) &&
astUtils.isTokenOnSameLine(token, nextToken) &&
sourceCode.isSpaceBetweenTokens(token, nextToken)
) {
context.report({
loc: token.loc.start,
message: "Unexpected space(s) after \"{{value}}\".",
data: token,
fix: function(fixer) {
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
}
/**
* Parses the option object and determines check methods for each keyword.
*
* @param {object|undefined} options - The option object to parse.
* @returns {object} - Normalized option object.
* Keys are keywords (there are for every keyword).
* Values are instances of `{"before": function, "after": function}`.
*/
function parseOptions(options) {
var before = !options || options.before !== false;
var after = !options || options.after !== false;
var defaultValue = {
before: before ? expectSpaceBefore : unexpectSpaceBefore,
after: after ? expectSpaceAfter : unexpectSpaceAfter
};
var overrides = (options && options.overrides) || {};
var retv = Object.create(null);
for (var i = 0; i < KEYS.length; ++i) {
var key = KEYS[i];
var override = overrides[key];
if (override) {
var thisBefore = ("before" in override) ? override.before : before;
var thisAfter = ("after" in override) ? override.after : after;
retv[key] = {
before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
};
} else {
retv[key] = defaultValue;
}
}
return retv;
}
var checkMethodMap = parseOptions(context.options[0]);
/**
* Reports a given token if usage of spacing followed by the token is
* invalid.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
* token to check.
* @returns {void}
*/
function checkSpacingBefore(token, pattern) {
checkMethodMap[token.value].before(token, pattern);
}
/**
* Reports a given token if usage of spacing preceded by the token is
* invalid.
*
* @param {Token} token - A token to report.
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
* token to check.
* @returns {void}
*/
function checkSpacingAfter(token, pattern) {
checkMethodMap[token.value].after(token, pattern);
}
/**
* Reports a given token if usage of spacing around the token is invalid.
*
* @param {Token} token - A token to report.
* @returns {void}
*/
function checkSpacingAround(token) {
checkSpacingBefore(token);
checkSpacingAfter(token);
}
/**
* Reports the first token of a given node if the first token is a keyword
* and usage of spacing around the token is invalid.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingAroundFirstToken(node) {
var firstToken = node && sourceCode.getFirstToken(node);
if (firstToken && firstToken.type === "Keyword") {
checkSpacingAround(firstToken);
}
}
/**
* Reports the first token of a given node if the first token is a keyword
* and usage of spacing followed by the token is invalid.
*
* This is used for unary operators (e.g. `typeof`), `function`, and `super`.
* Other rules are handling usage of spacing preceded by those keywords.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingBeforeFirstToken(node) {
var firstToken = node && sourceCode.getFirstToken(node);
if (firstToken && firstToken.type === "Keyword") {
checkSpacingBefore(firstToken);
}
}
/**
* Reports the previous token of a given node if the token is a keyword and
* usage of spacing around the token is invalid.
*
* @param {ASTNode|null} node - A node to report.
* @returns {void}
*/
function checkSpacingAroundTokenBefore(node) {
if (node) {
var token = sourceCode.getTokenBefore(node);
while (token.type !== "Keyword") {
token = sourceCode.getTokenBefore(token);
}
checkSpacingAround(token);
}
}
/**
* Reports `class` and `extends` keywords of a given node if usage of
* spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForClass(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.superClass);
}
/**
* Reports `if` and `else` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForIfStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.alternate);
}
/**
* Reports `try`, `catch`, and `finally` keywords of a given node if usage
* of spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForTryStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundFirstToken(node.handler);
checkSpacingAroundTokenBefore(node.finalizer);
}
/**
* Reports `do` and `while` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForDoWhileStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.test);
}
/**
* Reports `for` and `in` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForForInStatement(node) {
checkSpacingAroundFirstToken(node);
checkSpacingAroundTokenBefore(node.right);
}
/**
* Reports `for` and `of` keywords of a given node if usage of spacing
* around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForForOfStatement(node) {
checkSpacingAroundFirstToken(node);
// `of` is not a keyword token.
var token = sourceCode.getTokenBefore(node.right);
while (token.value !== "of") {
token = sourceCode.getTokenBefore(token);
}
checkSpacingAround(token);
}
/**
* Reports `import`, `export`, `as`, and `from` keywords of a given node if
* usage of spacing around those keywords is invalid.
*
* This rule handles the `*` token in module declarations.
*
* import*as A from "./a"; /*error Expected space(s) after "import".
* error Expected space(s) before "as".
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForModuleDeclaration(node) {
var firstToken = sourceCode.getFirstToken(node);
checkSpacingBefore(firstToken, PREV_TOKEN_M);
checkSpacingAfter(firstToken, NEXT_TOKEN_M);
if (node.source) {
var fromToken = sourceCode.getTokenBefore(node.source);
checkSpacingBefore(fromToken, PREV_TOKEN_M);
checkSpacingAfter(fromToken, NEXT_TOKEN_M);
}
}
/**
* Reports `as` keyword of a given node if usage of spacing around this
* keyword is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForImportNamespaceSpecifier(node) {
var asToken = sourceCode.getFirstToken(node, 1);
checkSpacingBefore(asToken, PREV_TOKEN_M);
}
/**
* Reports `static`, `get`, and `set` keywords of a given node if usage of
* spacing around those keywords is invalid.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function checkSpacingForProperty(node) {
if (node.static) {
checkSpacingAroundFirstToken(node);
}
if (node.kind === "get" || node.kind === "set") {
var token = sourceCode.getFirstToken(
node,
node.static ? 1 : 0
);
checkSpacingAround(token);
}
}
return {
// Statements
DebuggerStatement: checkSpacingAroundFirstToken,
WithStatement: checkSpacingAroundFirstToken,
// Statements - Control flow
BreakStatement: checkSpacingAroundFirstToken,
ContinueStatement: checkSpacingAroundFirstToken,
ReturnStatement: checkSpacingAroundFirstToken,
ThrowStatement: checkSpacingAroundFirstToken,
TryStatement: checkSpacingForTryStatement,
// Statements - Choice
IfStatement: checkSpacingForIfStatement,
SwitchStatement: checkSpacingAroundFirstToken,
SwitchCase: checkSpacingAroundFirstToken,
// Statements - Loops
DoWhileStatement: checkSpacingForDoWhileStatement,
ForInStatement: checkSpacingForForInStatement,
ForOfStatement: checkSpacingForForOfStatement,
ForStatement: checkSpacingAroundFirstToken,
WhileStatement: checkSpacingAroundFirstToken,
// Statements - Declarations
ClassDeclaration: checkSpacingForClass,
ExportNamedDeclaration: checkSpacingForModuleDeclaration,
ExportDefaultDeclaration: checkSpacingAroundFirstToken,
ExportAllDeclaration: checkSpacingForModuleDeclaration,
FunctionDeclaration: checkSpacingBeforeFirstToken,
ImportDeclaration: checkSpacingForModuleDeclaration,
VariableDeclaration: checkSpacingAroundFirstToken,
// Expressions
ClassExpression: checkSpacingForClass,
FunctionExpression: checkSpacingBeforeFirstToken,
NewExpression: checkSpacingBeforeFirstToken,
Super: checkSpacingBeforeFirstToken,
ThisExpression: checkSpacingBeforeFirstToken,
UnaryExpression: checkSpacingBeforeFirstToken,
YieldExpression: checkSpacingBeforeFirstToken,
// Others
ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
MethodDefinition: checkSpacingForProperty,
Property: checkSpacingForProperty
};
};
module.exports.schema = [
{
"type": "object",
"properties": {
"before": {"type": "boolean"},
"after": {"type": "boolean"},
"overrides": {
"type": "object",
"properties": KEYS.reduce(function(retv, key) {
retv[key] = {
"type": "object",
"properties": {
"before": {"type": "boolean"},
"after": {"type": "boolean"}
},
"additionalProperties": false
};
return retv;
}, {}),
"additionalProperties": false
}
},
"additionalProperties": false
}
];

4
tools/eslint/lib/rules/lines-around-comment.js

@ -11,7 +11,7 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var assign = require("object-assign"); var lodash = require("lodash");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -67,7 +67,7 @@ function contains(val, array) {
module.exports = function(context) { module.exports = function(context) {
var options = context.options[0] ? assign({}, context.options[0]) : {}; var options = context.options[0] ? lodash.assign({}, context.options[0]) : {};
options.beforeLineComment = options.beforeLineComment || false; options.beforeLineComment = options.beforeLineComment || false;
options.afterLineComment = options.afterLineComment || false; options.afterLineComment = options.afterLineComment || false;
options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;

3
tools/eslint/lib/rules/max-depth.js

@ -105,6 +105,7 @@ module.exports = function(context) {
module.exports.schema = [ module.exports.schema = [
{ {
"type": "integer" "type": "integer",
"minimum": 0
} }
]; ];

172
tools/eslint/lib/rules/max-len.js

@ -19,32 +19,43 @@ module.exports = function(context) {
var URL_REGEXP = /[^:/?#]:\/\/[^?#]/; var URL_REGEXP = /[^:/?#]:\/\/[^?#]/;
/** /**
* Creates a string that is made up of repeating a given string a certain * Computes the length of a line that may contain tabs. The width of each
* number of times. This uses exponentiation of squares to achieve significant * tab will be the number of spaces to the next tab stop.
* performance gains over the more traditional implementation of such * @param {string} line The line.
* functionality. * @param {int} tabWidth The width of each tab stop in spaces.
* @param {string} str The string to repeat. * @returns {int} The computed line length.
* @param {int} num The number of times to repeat the string.
* @returns {string} The created string.
* @private * @private
*/ */
function stringRepeat(str, num) { function computeLineLength(line, tabWidth) {
var result = ""; var extraCharacterCount = 0;
for (num |= 0; num > 0; num >>>= 1, str += str) { line.replace(/\t/g, function(match, offset) {
if (num & 1) { var totalOffset = offset + extraCharacterCount,
result += str; previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0,
} spaceCount = tabWidth - previousTabStopOffset;
} extraCharacterCount += spaceCount - 1; // -1 for the replaced tab
return result; });
return line.length + extraCharacterCount;
}
// The options object must be the last option specified…
var lastOption = context.options[context.options.length - 1];
var options = typeof lastOption === "object" ? Object.create(lastOption) : {};
// …but max code length…
if (typeof context.options[0] === "number") {
options.code = context.options[0];
}
// …and tabWidth can be optionally specified directly as integers.
if (typeof context.options[1] === "number") {
options.tabWidth = context.options[1];
} }
var maxLength = context.options[0] || 80, var maxLength = options.code || 80,
tabWidth = context.options[1] || 4, tabWidth = options.tabWidth || 4,
ignoreOptions = context.options[2] || {}, ignorePattern = options.ignorePattern || null,
ignorePattern = ignoreOptions.ignorePattern || null, ignoreComments = options.ignoreComments || false,
ignoreComments = ignoreOptions.ignoreComments || false, ignoreTrailingComments = options.ignoreTrailingComments || options.ignoreComments || false,
ignoreUrls = ignoreOptions.ignoreUrls || false, ignoreUrls = options.ignoreUrls || false,
tabString = stringRepeat(" ", tabWidth); maxCommentLength = options.comments;
if (ignorePattern) { if (ignorePattern) {
ignorePattern = new RegExp(ignorePattern); ignorePattern = new RegExp(ignorePattern);
@ -64,10 +75,26 @@ module.exports = function(context) {
*/ */
function isTrailingComment(line, lineNumber, comment) { function isTrailingComment(line, lineNumber, comment) {
return comment && return comment &&
(comment.loc.start.line <= lineNumber && lineNumber <= comment.loc.end.line) && (comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) &&
(comment.loc.end.line > lineNumber || comment.loc.end.column === line.length); (comment.loc.end.line > lineNumber || comment.loc.end.column === line.length);
} }
/**
* Tells if a comment encompasses the entire line.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
* @param {ASTNode} comment The comment to remove
* @returns {boolean} If the comment covers the entire line
*/
function isFullLineComment(line, lineNumber, comment) {
var start = comment.loc.start,
end = comment.loc.end;
return comment &&
(start.line < lineNumber || (start.line === lineNumber && start.column === 0)) &&
(end.line > lineNumber || end.column === line.length);
}
/** /**
* Gets the line after the comment and any remaining trailing whitespace is * Gets the line after the comment and any remaining trailing whitespace is
* stripped. * stripped.
@ -77,13 +104,8 @@ module.exports = function(context) {
* @returns {string} Line without comment and trailing whitepace * @returns {string} Line without comment and trailing whitepace
*/ */
function stripTrailingComment(line, lineNumber, comment) { function stripTrailingComment(line, lineNumber, comment) {
if (comment.loc.start.line < lineNumber) { // loc.column is zero-indexed
// this entire line is a comment return line.slice(0, comment.loc.start.column).replace(/\s+$/, "");
return "";
} else {
// loc.column is zero-indexed
return line.slice(0, comment.loc.start.column).replace(/\s+$/, "");
}
} }
/** /**
@ -96,13 +118,17 @@ module.exports = function(context) {
// split (honors line-ending) // split (honors line-ending)
var lines = context.getSourceLines(), var lines = context.getSourceLines(),
// list of comments to ignore // list of comments to ignore
comments = ignoreComments ? context.getAllComments() : [], comments = ignoreComments || maxCommentLength ? context.getAllComments() : [],
// we iterate over comments in parallel with the lines // we iterate over comments in parallel with the lines
commentsIndex = 0; commentsIndex = 0;
lines.forEach(function(line, i) { lines.forEach(function(line, i) {
// i is zero-indexed, line numbers are one-indexed // i is zero-indexed, line numbers are one-indexed
var lineNumber = i + 1; var lineNumber = i + 1;
// if we're checking comment length; we need to know whether this
// line is a comment
var lineIsComment = false;
// we can short-circuit the comment checks if we're already out of comments to check // we can short-circuit the comment checks if we're already out of comments to check
if (commentsIndex < comments.length) { if (commentsIndex < comments.length) {
// iterate over comments until we find one past the current line // iterate over comments until we find one past the current line
@ -111,7 +137,10 @@ module.exports = function(context) {
} while (comment && comment.loc.start.line <= lineNumber); } while (comment && comment.loc.start.line <= lineNumber);
// and step back by one // and step back by one
comment = comments[--commentsIndex]; comment = comments[--commentsIndex];
if (isTrailingComment(line, lineNumber, comment)) {
if (isFullLineComment(line, lineNumber, comment)) {
lineIsComment = true;
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
line = stripTrailingComment(line, lineNumber, comment); line = stripTrailingComment(line, lineNumber, comment);
} }
} }
@ -120,8 +149,16 @@ module.exports = function(context) {
// ignore this line // ignore this line
return; return;
} }
// replace the tabs
if (line.replace(/\t/g, tabString).length > maxLength) { var lineLength = computeLineLength(line, tabWidth);
if (lineIsComment && ignoreComments) {
return;
}
if (lineIsComment && lineLength > maxCommentLength) {
context.report(node, { line: lineNumber, column: 0 }, "Line " + (i + 1) + " exceeds the maximum comment line length of " + maxCommentLength + ".");
} else if (lineLength > maxLength) {
context.report(node, { line: lineNumber, column: 0 }, "Line " + (i + 1) + " exceeds the maximum line length of " + maxLength + "."); context.report(node, { line: lineNumber, column: 0 }, "Line " + (i + 1) + " exceeds the maximum line length of " + maxLength + ".");
} }
}); });
@ -138,28 +175,49 @@ module.exports = function(context) {
}; };
module.exports.schema = [ var OPTIONS_SCHEMA = {
{ "type": "object",
"type": "integer", "properties": {
"minimum": 0 "code": {
}, "type": "integer",
{ "minimum": 0
"type": "integer",
"minimum": 0
},
{
"type": "object",
"properties": {
"ignorePattern": {
"type": "string"
},
"ignoreComments": {
"type": "boolean"
},
"ignoreUrls": {
"type": "boolean"
}
}, },
"additionalProperties": false "comments": {
} "type": "integer",
"minimum": 0
},
"tabWidth": {
"type": "integer",
"minimum": 0
},
"ignorePattern": {
"type": "string"
},
"ignoreComments": {
"type": "boolean"
},
"ignoreUrls": {
"type": "boolean"
},
"ignoreTrailingComments": {
"type": "boolean"
}
},
"additionalProperties": false
};
var OPTIONS_OR_INTEGER_SCHEMA = {
"anyOf": [
OPTIONS_SCHEMA,
{
"type": "integer",
"minimum": 0
}
]
};
module.exports.schema = [
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_SCHEMA
]; ];

3
tools/eslint/lib/rules/max-nested-callbacks.js

@ -68,6 +68,7 @@ module.exports = function(context) {
module.exports.schema = [ module.exports.schema = [
{ {
"type": "integer" "type": "integer",
"minimum": 0
} }
]; ];

3
tools/eslint/lib/rules/max-params.js

@ -40,6 +40,7 @@ module.exports = function(context) {
module.exports.schema = [ module.exports.schema = [
{ {
"type": "integer" "type": "integer",
"minimum": 0
} }
]; ];

55
tools/eslint/lib/rules/max-statements.js

@ -17,7 +17,26 @@ module.exports = function(context) {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
var functionStack = [], var functionStack = [],
maxStatements = context.options[0] || 10; maxStatements = context.options[0] || 10,
ignoreTopLevelFunctions = context.options[1] && context.options[1].ignoreTopLevelFunctions || false,
topLevelFunctions = [];
/**
* Reports a node if it has too many statements
* @param {ASTNode} node node to evaluate
* @param {int} count Number of statements in node
* @param {int} max Maximum number of statements allowed
* @returns {void}
* @private
*/
function reportIfTooManyStatements(node, count, max) {
if (count > max) {
context.report(
node,
"This function has too many statements ({{count}}). Maximum allowed is {{max}}.",
{ count: count, max: max });
}
}
/** /**
* When parsing a new function, store it in our function stack * When parsing a new function, store it in our function stack
@ -36,10 +55,10 @@ module.exports = function(context) {
*/ */
function endFunction(node) { function endFunction(node) {
var count = functionStack.pop(); var count = functionStack.pop();
if (ignoreTopLevelFunctions && functionStack.length === 0) {
if (count > maxStatements) { topLevelFunctions.push({ node: node, count: count});
context.report(node, "This function has too many statements ({{count}}). Maximum allowed is {{max}}.", } else {
{ count: count, max: maxStatements }); reportIfTooManyStatements(node, count, maxStatements);
} }
} }
@ -66,13 +85,35 @@ module.exports = function(context) {
"FunctionDeclaration:exit": endFunction, "FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction, "FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction "ArrowFunctionExpression:exit": endFunction,
"Program:exit": function() {
if (topLevelFunctions.length === 1) {
return;
}
topLevelFunctions.forEach(function(element) {
var count = element.count;
var node = element.node;
reportIfTooManyStatements(node, count, maxStatements);
});
}
}; };
}; };
module.exports.schema = [ module.exports.schema = [
{ {
"type": "integer" "type": "integer",
"minimum": 0
},
{
"type": "object",
"properties": {
"ignoreTopLevelFunctions": {
"type": "boolean"
}
},
"additionalProperties": false
} }
]; ];

4
tools/eslint/lib/rules/new-cap.js

@ -11,7 +11,7 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var assign = require("object-assign"); var lodash = require("lodash");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -77,7 +77,7 @@ function calculateCapIsNewExceptions(config) {
module.exports = function(context) { module.exports = function(context) {
var config = context.options[0] ? assign({}, context.options[0]) : {}; var config = context.options[0] ? lodash.assign({}, context.options[0]) : {};
config.newIsCap = config.newIsCap !== false; config.newIsCap = config.newIsCap !== false;
config.capIsNew = config.capIsNew !== false; config.capIsNew = config.capIsNew !== false;
var skipProperties = config.properties === false; var skipProperties = config.properties === false;

31
tools/eslint/lib/rules/newline-after-var.js

@ -17,8 +17,9 @@ module.exports = function(context) {
var ALWAYS_MESSAGE = "Expected blank line after variable declarations.", var ALWAYS_MESSAGE = "Expected blank line after variable declarations.",
NEVER_MESSAGE = "Unexpected blank line after variable declarations."; NEVER_MESSAGE = "Unexpected blank line after variable declarations.";
// Default `mode` to "always". This means that invalid options will also var sourceCode = context.getSourceCode();
// be treated as "always" and the only special case is "never"
// Default `mode` to "always".
var mode = context.options[0] === "never" ? "never" : "always"; var mode = context.options[0] === "never" ? "never" : "always";
// Cache starting and ending line numbers of comments for faster lookup // Cache starting and ending line numbers of comments for faster lookup
@ -64,24 +65,14 @@ module.exports = function(context) {
} }
/** /**
* Determine if provided nodeType is a function specifier * Determine if provided node is the last of their parent block.
* @private
* @param {string} nodeType - nodeType to test
* @returns {boolean} True if `nodeType` is a function specifier
*/
function isFunctionSpecifier(nodeType) {
return nodeType === "FunctionDeclaration" || nodeType === "FunctionExpression" ||
nodeType === "ArrowFunctionExpression";
}
/**
* Determine if provided node is the last of his parent
* @private * @private
* @param {ASTNode} node - node to test * @param {ASTNode} node - node to test
* @returns {boolean} True if `node` is last of his parent * @returns {boolean} True if `node` is last of their parent block.
*/ */
function isLastNode(node) { function isLastNode(node) {
return node.parent.body[node.parent.body.length - 1] === node; var token = sourceCode.getTokenAfter(node);
return !token || (token.type === "Punctuator" && token.value === "}");
} }
/** /**
@ -108,8 +99,8 @@ module.exports = function(context) {
* @returns {void} * @returns {void}
*/ */
function checkForBlankLine(node) { function checkForBlankLine(node) {
var lastToken = context.getLastToken(node), var lastToken = sourceCode.getLastToken(node),
nextToken = context.getTokenAfter(node), nextToken = sourceCode.getTokenAfter(node),
nextLineNum = lastToken.loc.end.line + 1, nextLineNum = lastToken.loc.end.line + 1,
noNextLineToken, noNextLineToken,
hasNextLineComment; hasNextLineComment;
@ -135,8 +126,8 @@ module.exports = function(context) {
return; return;
} }
// Ignore if it is last statement in a function // Ignore if it is last statement in a block
if (node.parent.parent && isFunctionSpecifier(node.parent.parent.type) && isLastNode(node)) { if (isLastNode(node)) {
return; return;
} }

113
tools/eslint/lib/rules/newline-per-chained-call.js

@ -0,0 +1,113 @@
/**
* @fileoverview Rule to ensure newline per method call when chaining calls
* @author Rajendra Patil
* @copyright 2016 Rajendra Patil. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var options = context.options[0] || {},
codeStateMap = {},
ignoreChainWithDepth = options.ignoreChainWithDepth || 2;
/**
* Check and Capture State if the chained calls/members
* @param {ASTNode} node The node to check
* @param {object} codeState The state of the code current code to be filled
* @returns {void}
*/
function checkAndCaptureStateRecursively(node, codeState) {
var valid = false,
objectLineNumber,
propertyLineNumber;
if (node.callee) {
node = node.callee;
codeState.hasFunctionCall = true;
}
if (node.object) {
codeState.depth++;
objectLineNumber = node.object.loc.end.line;
propertyLineNumber = node.property.loc.end.line;
valid = node.computed || propertyLineNumber > objectLineNumber;
if (!valid) {
codeState.reports.push({
node: node,
text: "Expected line break after `{{code}}`.",
depth: codeState.depth
});
}
// Recurse
checkAndCaptureStateRecursively(node.object, codeState);
}
}
/**
* Verify and report the captured state report
* @param {object} codeState contains the captured state with `hasFunctionCall, reports and depth`
* @returns {void}
*/
function reportState(codeState) {
var report;
if (codeState.hasFunctionCall && codeState.depth > ignoreChainWithDepth && codeState.reports) {
while (codeState.reports.length) {
report = codeState.reports.shift();
context.report(report.node, report.text, {
code: context.getSourceCode().getText(report.node.object).replace(/\r\n|\r|\n/g, "\\n") // Not to print newlines in error report
});
}
}
}
/**
* Initialize the node state object with default values.
* @returns {void}
*/
function initializeState() {
return {
visited: false,
hasFunctionCall: false,
depth: 1,
reports: []
};
}
/**
* Process the said node and recurse internally
* @param {ASTNode} node The node to check
* @returns {void}
*/
function processNode(node) {
var stateKey = [node.loc.start.line, node.loc.start.column].join("@"),
codeState = codeStateMap[stateKey] = (codeStateMap[stateKey] || initializeState());
if (!codeState.visited) {
codeState.visited = true;
checkAndCaptureStateRecursively(node, codeState);
}
reportState(codeState);
}
return {
"CallExpression": processNode,
"MemberExpression": processNode
};
};
module.exports.schema = [{
"type": "object",
"properties": {
"ignoreChainWithDepth": {
"type": "integer",
"minimum": 1,
"maximum": 10
}
},
"additionalProperties": false
}];

29
tools/eslint/lib/rules/no-alert.js

@ -33,7 +33,7 @@ function report(context, node, identifierName) {
/** /**
* Returns the property name of a MemberExpression. * Returns the property name of a MemberExpression.
* @param {ASTNode} memberExpressionNode The MemberExpression node. * @param {ASTNode} memberExpressionNode The MemberExpression node.
* @returns {string|undefined} Returns the property name if available, undefined else. * @returns {string|null} Returns the property name if available, null else.
*/ */
function getPropertyName(memberExpressionNode) { function getPropertyName(memberExpressionNode) {
if (memberExpressionNode.computed) { if (memberExpressionNode.computed) {
@ -43,13 +43,14 @@ function getPropertyName(memberExpressionNode) {
} else { } else {
return memberExpressionNode.property.name; return memberExpressionNode.property.name;
} }
return null;
} }
/** /**
* Finds the escope reference in the given scope. * Finds the escope reference in the given scope.
* @param {Object} scope The scope to search. * @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node. * @param {ASTNode} node The identifier node.
* @returns {Reference|undefined} Returns the found reference or undefined if none were found. * @returns {Reference|null} Returns the found reference or null if none were found.
*/ */
function findReference(scope, node) { function findReference(scope, node) {
var references = scope.references.filter(function(reference) { var references = scope.references.filter(function(reference) {
@ -60,17 +61,7 @@ function findReference(scope, node) {
if (references.length === 1) { if (references.length === 1) {
return references[0]; return references[0];
} }
} return null;
/**
* Checks if the given identifier name is shadowed in the given global scope.
* @param {Object} globalScope The global scope.
* @param {string} identifierName The identifier name to check
* @returns {boolean} Whether or not the name is shadowed globally.
*/
function isGloballyShadowed(globalScope, identifierName) {
var variable = globalScope.set.get(identifierName);
return Boolean(variable && variable.defs.length > 0);
} }
/** /**
@ -81,16 +72,8 @@ function isGloballyShadowed(globalScope, identifierName) {
* @returns {boolean} Whether or not the name is shadowed. * @returns {boolean} Whether or not the name is shadowed.
*/ */
function isShadowed(scope, globalScope, node) { function isShadowed(scope, globalScope, node) {
var reference = findReference(scope, node), var reference = findReference(scope, node);
identifierName = node.name; return reference && reference.resolved && reference.resolved.defs.length > 0;
if (reference) {
if (reference.resolved || isGloballyShadowed(globalScope, identifierName)) {
return true;
}
}
return false;
} }
/** /**

88
tools/eslint/lib/rules/no-arrow-condition.js

@ -1,88 +0,0 @@
/**
* @fileoverview A rule to warn against using arrow functions in conditions.
* @author Jxck <https://github.com/Jxck>
* @copyright 2015 Luke Karrys. All rights reserved.
* The MIT License (MIT)
* Copyright (c) 2015 Jxck
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a node is an arrow function expression.
* @param {ASTNode} node - node to test
* @returns {boolean} `true` if the node is an arrow function expression.
*/
function isArrowFunction(node) {
return node.test && node.test.type === "ArrowFunctionExpression";
}
/**
* Checks whether or not a node is a conditional expression.
* @param {ASTNode} node - node to test
* @returns {boolean} `true` if the node is a conditional expression.
*/
function isConditional(node) {
return node.body && node.body.type === "ConditionalExpression";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
/**
* Reports if a conditional statement is an arrow function.
* @param {ASTNode} node - A node to check and report.
* @returns {void}
*/
function checkCondition(node) {
if (isArrowFunction(node)) {
context.report(node, "Arrow function `=>` used inside {{statementType}} instead of comparison operator.", {statementType: node.type});
}
}
/**
* Reports if an arrow function contains an ambiguous conditional.
* @param {ASTNode} node - A node to check and report.
* @returns {void}
*/
function checkArrowFunc(node) {
if (isConditional(node)) {
context.report(node, "Arrow function used ambiguously with a conditional expression.");
}
}
return {
"IfStatement": checkCondition,
"WhileStatement": checkCondition,
"ForStatement": checkCondition,
"ConditionalExpression": checkCondition,
"ArrowFunctionExpression": checkArrowFunc
};
};
module.exports.schema = [];

60
tools/eslint/lib/rules/no-bitwise.js

@ -5,17 +5,23 @@
"use strict"; "use strict";
//
// Set of bitwise operators.
//
var BITWISE_OPERATORS = [
"^", "|", "&", "<<", ">>", ">>>",
"^=", "|=", "&=", "<<=", ">>=", ">>>=",
"~"
];
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var options = context.options[0] || {};
var BITWISE_OPERATORS = [ var allowed = options.allow || [];
"^", "|", "&", "<<", ">>", ">>>", var int32Hint = options.int32Hint === true;
"^=", "|=", "&=", "<<=", ">>=", ">>>=",
"~"
];
/** /**
* Reports an unexpected use of a bitwise operator. * Reports an unexpected use of a bitwise operator.
@ -35,13 +41,32 @@ module.exports = function(context) {
return BITWISE_OPERATORS.indexOf(node.operator) !== -1; return BITWISE_OPERATORS.indexOf(node.operator) !== -1;
} }
/**
* Checks if exceptions were provided, e.g. `{ allow: ['~', '|'] }`.
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node has a bitwise operator.
*/
function allowedOperator(node) {
return allowed.indexOf(node.operator) !== -1;
}
/**
* Checks if the given bitwise operator is used for integer typecasting, i.e. "|0"
* @param {ASTNode} node The node to check.
* @returns {boolean} whether the node is used in integer typecasting.
*/
function isInt32Hint(node) {
return int32Hint && node.operator === "|" && node.right &&
node.right.type === "Literal" && node.right.value === 0;
}
/** /**
* Report if the given node contains a bitwise operator. * Report if the given node contains a bitwise operator.
* @param {ASTNode} node The node to check. * @param {ASTNode} node The node to check.
* @returns {void} * @returns {void}
*/ */
function checkNodeForBitwiseOperator(node) { function checkNodeForBitwiseOperator(node) {
if (hasBitwiseOperator(node)) { if (hasBitwiseOperator(node) && !allowedOperator(node) && !isInt32Hint(node)) {
report(node); report(node);
} }
} }
@ -54,4 +79,21 @@ module.exports = function(context) {
}; };
module.exports.schema = []; module.exports.schema = [
{
"type": "object",
"properties": {
"allow": {
"type": "array",
"items": {
"enum": BITWISE_OPERATORS
},
"uniqueItems": true
},
"int32Hint": {
"type": "boolean"
}
},
"additionalProperties": false
}
];

2
tools/eslint/lib/rules/no-class-assign.js

@ -23,7 +23,7 @@ module.exports = function(context) {
astUtils.getModifyingReferences(variable.references).forEach(function(reference) { astUtils.getModifyingReferences(variable.references).forEach(function(reference) {
context.report( context.report(
reference.identifier, reference.identifier,
"`{{name}}` is a class.", "'{{name}}' is a class.",
{name: reference.identifier.name}); {name: reference.identifier.name});
}); });

65
tools/eslint/lib/rules/no-confusing-arrow.js

@ -0,0 +1,65 @@
/**
* @fileoverview A rule to warn against using arrow functions when they could be
* confused with comparisions
* @author Jxck <https://github.com/Jxck>
* @copyright 2015 Luke Karrys. All rights reserved.
* The MIT License (MIT)
* Copyright (c) 2015 Jxck
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a node is a conditional expression.
* @param {ASTNode} node - node to test
* @returns {boolean} `true` if the node is a conditional expression.
*/
function isConditional(node) {
return node.body && node.body.type === "ConditionalExpression";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
/**
* Reports if an arrow function contains an ambiguous conditional.
* @param {ASTNode} node - A node to check and report.
* @returns {void}
*/
function checkArrowFunc(node) {
if (isConditional(node)) {
context.report(node, "Arrow function used ambiguously with a conditional expression.");
}
}
return {
"ArrowFunctionExpression": checkArrowFunc
};
};
module.exports.schema = [];

35
tools/eslint/lib/rules/no-console.js

@ -1,6 +1,7 @@
/** /**
* @fileoverview Rule to flag use of console object * @fileoverview Rule to flag use of console object
* @author Nicholas C. Zakas * @author Nicholas C. Zakas
* @copyright 2016 Eric Correia. All rights reserved.
*/ */
"use strict"; "use strict";
@ -16,12 +17,40 @@ module.exports = function(context) {
"MemberExpression": function(node) { "MemberExpression": function(node) {
if (node.object.name === "console") { if (node.object.name === "console") {
context.report(node, "Unexpected console statement."); var blockConsole = true;
}
if ( context.options.length > 0 ) {
var allowedProperties = context.options[0].allow;
var passedProperty = node.property.name;
var propertyIsAllowed = (allowedProperties.indexOf(passedProperty) > -1);
if (propertyIsAllowed) {
blockConsole = false;
}
}
if (blockConsole) {
context.report(node, "Unexpected console statement.");
}
}
} }
}; };
}; };
module.exports.schema = []; module.exports.schema = [
{
"type": "object",
"properties": {
"allow": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
}
},
"additionalProperties": false
}
];

2
tools/eslint/lib/rules/no-const-assign.js

@ -23,7 +23,7 @@ module.exports = function(context) {
astUtils.getModifyingReferences(variable.references).forEach(function(reference) { astUtils.getModifyingReferences(variable.references).forEach(function(reference) {
context.report( context.report(
reference.identifier, reference.identifier,
"`{{name}}` is constant.", "'{{name}}' is constant.",
{name: reference.identifier.name}); {name: reference.identifier.name});
}); });
} }

2
tools/eslint/lib/rules/no-constant-condition.js

@ -34,7 +34,7 @@ module.exports = function(context) {
return isConstant(node.argument); return isConstant(node.argument);
case "BinaryExpression": case "BinaryExpression":
case "LogicalExpression": case "LogicalExpression":
return isConstant(node.left) && isConstant(node.right); return isConstant(node.left) && isConstant(node.right) && node.operator !== "in";
case "AssignmentExpression": case "AssignmentExpression":
return (node.operator === "=") && isConstant(node.right); return (node.operator === "=") && isConstant(node.right);
case "SequenceExpression": case "SequenceExpression":

13
tools/eslint/lib/rules/no-control-regex.js

@ -18,35 +18,28 @@ module.exports = function(context) {
* @private * @private
*/ */
function getRegExp(node) { function getRegExp(node) {
if (node.value instanceof RegExp) { if (node.value instanceof RegExp) {
return node.value; return node.value;
} else if (typeof node.value === "string") { } else if (typeof node.value === "string") {
var parent = context.getAncestors().pop(); var parent = context.getAncestors().pop();
if ((parent.type === "NewExpression" || parent.type === "CallExpression") && if ((parent.type === "NewExpression" || parent.type === "CallExpression") &&
parent.callee.type === "Identifier" && parent.callee.name === "RegExp") { parent.callee.type === "Identifier" && parent.callee.name === "RegExp"
) {
// there could be an invalid regular expression string // there could be an invalid regular expression string
try { try {
return new RegExp(node.value); return new RegExp(node.value);
} catch (ex) { } catch (ex) {
return null; return null;
} }
} }
} else {
return null;
} }
return null;
} }
return { return {
"Literal": function(node) { "Literal": function(node) {
var computedValue, var computedValue,
regex = getRegExp(node); regex = getRegExp(node);

20
tools/eslint/lib/rules/no-dupe-class-members.js

@ -36,6 +36,22 @@ module.exports = function(context) {
return stateMap[key][isStatic ? "static" : "nonStatic"]; return stateMap[key][isStatic ? "static" : "nonStatic"];
} }
/**
* Gets the name text of a given node.
*
* @param {ASTNode} node - A node to get the name.
* @returns {string} The name text of the node.
*/
function getName(node) {
switch (node.type) {
case "Identifier": return node.name;
case "Literal": return String(node.value);
/* istanbul ignore next: syntax error */
default: return "";
}
}
return { return {
// Initializes the stack of state of member declarations. // Initializes the stack of state of member declarations.
"Program": function() { "Program": function() {
@ -58,7 +74,7 @@ module.exports = function(context) {
return; return;
} }
var name = node.key.name; var name = getName(node.key);
var state = getState(name, node.static); var state = getState(name, node.static);
var isDuplicate = false; var isDuplicate = false;
if (node.kind === "get") { if (node.kind === "get") {
@ -73,7 +89,7 @@ module.exports = function(context) {
} }
if (isDuplicate) { if (isDuplicate) {
context.report(node, "Duplicate name \"{{name}}\".", {name: name}); context.report(node, "Duplicate name '{{name}}'.", {name: name});
} }
} }
}; };

149
tools/eslint/lib/rules/no-empty-function.js

@ -0,0 +1,149 @@
/**
* @fileoverview Rule to disallow empty functions.
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
var ALLOW_OPTIONS = Object.freeze([
"functions",
"arrowFunctions",
"generatorFunctions",
"methods",
"generatorMethods",
"getters",
"setters",
"constructors"
]);
var SHOW_KIND = Object.freeze({
functions: "function",
arrowFunctions: "arrow function",
generatorFunctions: "generator function",
asyncFunctions: "async function",
methods: "method",
generatorMethods: "generator method",
asyncMethods: "async method",
getters: "getter",
setters: "setter",
constructors: "constructor"
});
/**
* Gets the kind of a given function node.
*
* @param {ASTNode} node - A function node to get. This is one of
* an ArrowFunctionExpression, a FunctionDeclaration, or a
* FunctionExpression.
* @returns {string} The kind of the function. This is one of "functions",
* "arrowFunctions", "generatorFunctions", "asyncFunctions", "methods",
* "generatorMethods", "asyncMethods", "getters", "setters", and
* "constructors".
*/
function getKind(node) {
var parent = node.parent;
var kind = "";
if (node.type === "ArrowFunctionExpression") {
return "arrowFunctions";
}
// Detects main kind.
if (parent.type === "Property") {
if (parent.kind === "get") {
return "getters";
}
if (parent.kind === "set") {
return "setters";
}
kind = parent.method ? "methods" : "functions";
} else if (parent.type === "MethodDefinition") {
if (parent.kind === "get") {
return "getters";
}
if (parent.kind === "set") {
return "setters";
}
if (parent.kind === "constructor") {
return "constructors";
}
kind = "methods";
} else {
kind = "functions";
}
// Detects prefix.
var prefix = "";
if (node.generator) {
prefix = "generator";
} else if (node.async) {
prefix = "async";
} else {
return kind;
}
return prefix + kind[0].toUpperCase() + kind.slice(1);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var options = context.options[0] || {};
var allowed = options.allow || [];
/**
* Reports a given function node if the node matches the following patterns.
*
* - Not allowed by options.
* - The body is empty.
* - The body doesn't have any comments.
*
* @param {ASTNode} node - A function node to report. This is one of
* an ArrowFunctionExpression, a FunctionDeclaration, or a
* FunctionExpression.
* @returns {void}
*/
function reportIfEmpty(node) {
var kind = getKind(node);
if (allowed.indexOf(kind) === -1 &&
node.body.type === "BlockStatement" &&
node.body.body.length === 0 &&
context.getComments(node.body).trailing.length === 0
) {
context.report({
node: node,
loc: node.body.loc.start,
message: "Unexpected empty " + SHOW_KIND[kind] + "."
});
}
}
return {
ArrowFunctionExpression: reportIfEmpty,
FunctionDeclaration: reportIfEmpty,
FunctionExpression: reportIfEmpty
};
};
module.exports.schema = [
{
type: "object",
properties: {
allow: {
type: "array",
items: {enum: ALLOW_OPTIONS},
uniqueItems: true
}
},
additionalProperties: false
}
];

27
tools/eslint/lib/rules/no-empty-label.js

@ -1,27 +0,0 @@
/**
* @fileoverview Rule to flag when label is not used for a loop or switch
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
return {
"LabeledStatement": function(node) {
var type = node.body.type;
if (type !== "ForStatement" && type !== "WhileStatement" && type !== "DoWhileStatement" && type !== "SwitchStatement" && type !== "ForInStatement" && type !== "ForOfStatement") {
context.report(node, "Unexpected label \"{{l}}\"", {l: node.label.name});
}
}
};
};
module.exports.schema = [];

9
tools/eslint/lib/rules/no-empty.js

@ -10,21 +10,18 @@
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { var FUNCTION_TYPE = /^(?:ArrowFunctionExpression|Function(?:Declaration|Expression))$/;
module.exports = function(context) {
return { return {
"BlockStatement": function(node) { "BlockStatement": function(node) {
var parent = node.parent,
parentType = parent.type;
// if the body is not empty, we can just return immediately // if the body is not empty, we can just return immediately
if (node.body.length !== 0) { if (node.body.length !== 0) {
return; return;
} }
// a function is generally allowed to be empty // a function is generally allowed to be empty
if (parentType === "FunctionDeclaration" || parentType === "FunctionExpression" || parentType === "ArrowFunctionExpression") { if (FUNCTION_TYPE.test(node.parent.type)) {
return; return;
} }

272
tools/eslint/lib/rules/no-eval.js

@ -1,26 +1,290 @@
/** /**
* @fileoverview Rule to flag use of eval() statement * @fileoverview Rule to flag use of eval() statement
* @author Nicholas C. Zakas * @author Nicholas C. Zakas
* @copyright 2015 Toru Nagashima. All rights reserved.
* @copyright 2015 Mathias Schreck. All rights reserved. * @copyright 2015 Mathias Schreck. All rights reserved.
* @copyright 2013 Nicholas C. Zakas. All rights reserved. * @copyright 2013 Nicholas C. Zakas. All rights reserved.
*/ */
"use strict"; "use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
var candidatesOfGlobalObject = Object.freeze([
"global",
"window"
]);
/**
* Checks a given node is a Identifier node of the specified name.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a Identifier node of the name.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks a given node is a Literal node of the specified string value.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a Literal node of the name.
*/
function isConstant(node, name) {
switch (node.type) {
case "Literal":
return node.value === name;
case "TemplateLiteral":
return (
node.expressions.length === 0 &&
node.quasis[0].value.cooked === name
);
default:
return false;
}
}
/**
* Checks a given node is a MemberExpression node which has the specified name's
* property.
*
* @param {ASTNode} node - A node to check.
* @param {string} name - A name to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
*/
function isMember(node, name) {
return (
node.type === "MemberExpression" &&
(node.computed ? isConstant : isIdentifier)(node.property, name)
);
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var allowIndirect = Boolean(
context.options[0] &&
context.options[0].allowIndirect
);
var sourceCode = context.getSourceCode();
var funcInfo = null;
/**
* Pushs a variable scope (Program or Function) information to the stack.
*
* This is used in order to check whether or not `this` binding is a
* reference to the global object.
*
* @param {ASTNode} node - A node of the scope. This is one of Program,
* FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression.
* @returns {void}
*/
function enterVarScope(node) {
var strict = context.getScope().isStrict;
funcInfo = {
upper: funcInfo,
node: node,
strict: strict,
defaultThis: false,
initialized: strict
};
}
/**
* Pops a variable scope from the stack.
*
* @returns {void}
*/
function exitVarScope() {
funcInfo = funcInfo.upper;
}
/**
* Reports a given node.
*
* `node` is `Identifier` or `MemberExpression`.
* The parent of `node` might be `CallExpression`.
*
* The location of the report is always `eval` `Identifier` (or possibly
* `Literal`). The type of the report is `CallExpression` if the parent is
* `CallExpression`. Otherwise, it's the given node type.
*
* @param {ASTNode} node - A node to report.
* @returns {void}
*/
function report(node) {
var locationNode = node;
var parent = node.parent;
if (node.type === "MemberExpression") {
locationNode = node.property;
}
if (parent.type === "CallExpression" && parent.callee === node) {
node = parent;
}
context.report({
node: node,
loc: locationNode.loc.start,
message: "eval can be harmful."
});
}
/**
* Reports accesses of `eval` via the global object.
*
* @param {escope.Scope} globalScope - The global scope.
* @returns {void}
*/
function reportAccessingEvalViaGlobalObject(globalScope) {
for (var i = 0; i < candidatesOfGlobalObject.length; ++i) {
var name = candidatesOfGlobalObject[i];
var variable = astUtils.getVariableByName(globalScope, name);
if (!variable) {
continue;
}
var references = variable.references;
for (var j = 0; j < references.length; ++j) {
var identifier = references[j].identifier;
var node = identifier.parent;
// To detect code like `window.window.eval`.
while (isMember(node, name)) {
node = node.parent;
}
// Reports.
if (isMember(node, "eval")) {
report(node);
}
}
}
}
/**
* Reports all accesses of `eval` (excludes direct calls to eval).
*
* @param {escope.Scope} globalScope - The global scope.
* @returns {void}
*/
function reportAccessingEval(globalScope) {
var variable = astUtils.getVariableByName(globalScope, "eval");
if (!variable) {
return;
}
var references = variable.references;
for (var i = 0; i < references.length; ++i) {
var reference = references[i];
var id = reference.identifier;
if (id.name === "eval" && !astUtils.isCallee(id)) {
// Is accessing to eval (excludes direct calls to eval)
report(id);
}
}
}
if (allowIndirect) {
// Checks only direct calls to eval.
// It's simple!
return {
"CallExpression:exit": function(node) {
var callee = node.callee;
if (isIdentifier(callee, "eval")) {
report(callee);
}
}
};
}
return { return {
"CallExpression": function(node) { "CallExpression:exit": function(node) {
if (node.callee.name === "eval") { var callee = node.callee;
context.report(node, "eval can be harmful."); if (isIdentifier(callee, "eval")) {
report(callee);
}
},
"Program": function(node) {
var scope = context.getScope(),
features = context.parserOptions.ecmaFeatures || {},
strict =
scope.isStrict ||
node.sourceType === "module" ||
(features.globalReturn && scope.childScopes[0].isStrict);
funcInfo = {
upper: null,
node: node,
strict: strict,
defaultThis: true,
initialized: true
};
},
"Program:exit": function() {
var globalScope = context.getScope();
exitVarScope();
reportAccessingEval(globalScope);
reportAccessingEvalViaGlobalObject(globalScope);
},
"FunctionDeclaration": enterVarScope,
"FunctionDeclaration:exit": exitVarScope,
"FunctionExpression": enterVarScope,
"FunctionExpression:exit": exitVarScope,
"ArrowFunctionExpression": enterVarScope,
"ArrowFunctionExpression:exit": exitVarScope,
"ThisExpression": function(node) {
if (!isMember(node.parent, "eval")) {
return;
}
// `this.eval` is found.
// Checks whether or not the value of `this` is the global object.
if (!funcInfo.initialized) {
funcInfo.initialized = true;
funcInfo.defaultThis = astUtils.isDefaultThisBinding(
funcInfo.node,
sourceCode
);
}
if (!funcInfo.strict && funcInfo.defaultThis) {
// `this.eval` is possible built-in `eval`.
report(node.parent);
} }
} }
}; };
}; };
module.exports.schema = []; module.exports.schema = [
{
"type": "object",
"properties": {
"allowIndirect": {"type": "boolean"}
},
"additionalProperties": false
}
];

161
tools/eslint/lib/rules/no-extra-bind.js

@ -2,6 +2,7 @@
* @fileoverview Rule to flag unnecessary bind calls * @fileoverview Rule to flag unnecessary bind calls
* @author Bence Dányi <bence@danyi.me> * @author Bence Dányi <bence@danyi.me>
* @copyright 2014 Bence Dányi. All rights reserved. * @copyright 2014 Bence Dányi. All rights reserved.
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE in root directory for full license. * See LICENSE in root directory for full license.
*/ */
"use strict"; "use strict";
@ -11,74 +12,136 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var scopeInfo = null;
var scope = [{ /**
depth: -1, * Reports a given function node.
found: 0 *
}]; * @param {ASTNode} node - A node to report. This is a FunctionExpression or
* an ArrowFunctionExpression.
* @returns {void}
*/
function report(node) {
context.report({
node: node.parent.parent,
message: "The function binding is unnecessary.",
loc: node.parent.property.loc.start
});
}
/**
* Gets the property name of a given node.
* If the property name is dynamic, this returns an empty string.
*
* @param {ASTNode} node - A node to check. This is a MemberExpression.
* @returns {string} The property name of the node.
*/
function getPropertyName(node) {
if (node.computed) {
switch (node.property.type) {
case "Literal":
return String(node.property.value);
case "TemplateLiteral":
if (node.property.expressions.length === 0) {
return node.property.quasis[0].value.cooked;
}
// fallthrough
default:
return false;
}
}
return node.property.name;
}
/** /**
* Get the topmost scope * Checks whether or not a given function node is the callee of `.bind()`
* @returns {Object} The topmost scope * method.
*
* e.g. `(function() {}.bind(foo))`
*
* @param {ASTNode} node - A node to report. This is a FunctionExpression or
* an ArrowFunctionExpression.
* @returns {boolean} `true` if the node is the callee of `.bind()` method.
*/ */
function getTopScope() { function isCalleeOfBindMethod(node) {
return scope[scope.length - 1]; var parent = node.parent;
var grandparent = parent.parent;
return (
grandparent &&
grandparent.type === "CallExpression" &&
grandparent.callee === parent &&
grandparent.arguments.length === 1 &&
parent.type === "MemberExpression" &&
parent.object === node &&
getPropertyName(parent) === "bind"
);
} }
/** /**
* Increment the depth of the top scope * Adds a scope information object to the stack.
*
* @param {ASTNode} node - A node to add. This node is a FunctionExpression
* or a FunctionDeclaration node.
* @returns {void} * @returns {void}
*/ */
function incrementScopeDepth() { function enterFunction(node) {
var top = getTopScope(); scopeInfo = {
top.depth++; isBound: isCalleeOfBindMethod(node),
thisFound: false,
upper: scopeInfo
};
} }
/** /**
* Decrement the depth of the top scope * Removes the scope information object from the top of the stack.
* At the same time, this reports the function node if the function has
* `.bind()` and the `this` keywords found.
*
* @param {ASTNode} node - A node to remove. This node is a
* FunctionExpression or a FunctionDeclaration node.
* @returns {void} * @returns {void}
*/ */
function decrementScopeDepth() { function exitFunction(node) {
var top = getTopScope(); if (scopeInfo.isBound && !scopeInfo.thisFound) {
top.depth--; report(node);
}
scopeInfo = scopeInfo.upper;
} }
return { /**
"CallExpression": function(node) { * Reports a given arrow function if the function is callee of `.bind()`
if (node.arguments.length === 1 && * method.
node.callee.type === "MemberExpression" && *
node.callee.property.name === "bind" && * @param {ASTNode} node - A node to report. This node is an
/FunctionExpression$/.test(node.callee.object.type)) { * ArrowFunctionExpression.
scope.push({ * @returns {void}
call: node, */
depth: -1, function exitArrowFunction(node) {
found: 0 if (isCalleeOfBindMethod(node)) {
}); report(node);
} }
}, }
"CallExpression:exit": function(node) {
var top = getTopScope(),
isArrowFunction = node.callee.type === "MemberExpression" && node.callee.object.type === "ArrowFunctionExpression";
if (top.call === node && (top.found === 0 || isArrowFunction)) { /**
context.report(node, "The function binding is unnecessary."); * Set the mark as the `this` keyword was found in this scope.
scope.pop(); *
} * @returns {void}
}, */
"ArrowFunctionExpression": incrementScopeDepth, function markAsThisFound() {
"ArrowFunctionExpression:exit": decrementScopeDepth, if (scopeInfo) {
"FunctionExpression": incrementScopeDepth, scopeInfo.thisFound = true;
"FunctionExpression:exit": decrementScopeDepth,
"FunctionDeclaration": incrementScopeDepth,
"FunctionDeclaration:exit": decrementScopeDepth,
"ThisExpression": function() {
var top = getTopScope();
if (top.depth === 0) {
top.found++;
}
} }
}; }
return {
"ArrowFunctionExpression:exit": exitArrowFunction,
"FunctionDeclaration": enterFunction,
"FunctionDeclaration:exit": exitFunction,
"FunctionExpression": enterFunction,
"FunctionExpression:exit": exitFunction,
"ThisExpression": markAsThisFound
};
}; };
module.exports.schema = []; module.exports.schema = [];

79
tools/eslint/lib/rules/no-extra-boolean-cast.js

@ -11,6 +11,33 @@
module.exports = function(context) { module.exports = function(context) {
// Node types which have a test which will coerce values to booleans.
var BOOLEAN_NODE_TYPES = [
"IfStatement",
"DoWhileStatement",
"WhileStatement",
"ConditionalExpression",
"ForStatement"
];
/**
* Check if a node is in a context where its value would be coerced to a boolean at runtime.
*
* @param {Object} node The node
* @param {Object} parent Its parent
* @returns {Boolean} If it is in a boolean context
*/
function isInBooleanContext(node, parent) {
return (
(BOOLEAN_NODE_TYPES.indexOf(parent.type) !== -1 &&
node === parent.test) ||
// !<bool>
(parent.type === "UnaryExpression" &&
parent.operator === "!")
);
}
return { return {
"UnaryExpression": function(node) { "UnaryExpression": function(node) {
var ancestors = context.getAncestors(), var ancestors = context.getAncestors(),
@ -24,44 +51,24 @@ module.exports = function(context) {
return; return;
} }
// if (<bool>) ... if (isInBooleanContext(parent, grandparent) ||
if (grandparent.type === "IfStatement") { // Boolean(<bool>) and new Boolean(<bool>)
context.report(node, "Redundant double negation in an if statement condition."); ((grandparent.type === "CallExpression" || grandparent.type === "NewExpression") &&
// do ... while (<bool>)
} else if (grandparent.type === "DoWhileStatement") {
context.report(node, "Redundant double negation in a do while loop condition.");
// while (<bool>) ...
} else if (grandparent.type === "WhileStatement") {
context.report(node, "Redundant double negation in a while loop condition.");
// <bool> ? ... : ...
} else if ((grandparent.type === "ConditionalExpression" &&
parent === grandparent.test)) {
context.report(node, "Redundant double negation in a ternary condition.");
// for (...; <bool>; ...) ...
} else if ((grandparent.type === "ForStatement" &&
parent === grandparent.test)) {
context.report(node, "Redundant double negation in a for loop condition.");
// !<bool>
} else if ((grandparent.type === "UnaryExpression" &&
grandparent.operator === "!")) {
context.report(node, "Redundant multiple negation.");
// Boolean(<bool>)
} else if ((grandparent.type === "CallExpression" &&
grandparent.callee.type === "Identifier" && grandparent.callee.type === "Identifier" &&
grandparent.callee.name === "Boolean")) { grandparent.callee.name === "Boolean")
context.report(node, "Redundant double negation in call to Boolean()."); ) {
context.report(node, "Redundant double negation.");
}
},
"CallExpression": function(node) {
var parent = node.parent;
// new Boolean(<bool>) if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
} else if ((grandparent.type === "NewExpression" && return;
grandparent.callee.type === "Identifier" && }
grandparent.callee.name === "Boolean")) {
context.report(node, "Redundant double negation in Boolean constructor call."); if (isInBooleanContext(node, parent)) {
context.report(node, "Redundant Boolean call.");
} }
} }
}; };

132
tools/eslint/lib/rules/no-extra-label.js

@ -0,0 +1,132 @@
/**
* @fileoverview Rule to disallow unnecessary labels
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var scopeInfo = null;
/**
* Creates a new scope with a breakable statement.
*
* @param {ASTNode} node - A node to create. This is a BreakableStatement.
* @returns {void}
*/
function enterBreakableStatement(node) {
scopeInfo = {
label: astUtils.getLabel(node),
breakable: true,
upper: scopeInfo
};
}
/**
* Removes the top scope of the stack.
*
* @returns {void}
*/
function exitBreakableStatement() {
scopeInfo = scopeInfo.upper;
}
/**
* Creates a new scope with a labeled statement.
*
* This ignores it if the body is a breakable statement.
* In this case it's handled in the `enterBreakableStatement` function.
*
* @param {ASTNode} node - A node to create. This is a LabeledStatement.
* @returns {void}
*/
function enterLabeledStatement(node) {
if (!astUtils.isBreakableStatement(node.body)) {
scopeInfo = {
label: node.label.name,
breakable: false,
upper: scopeInfo
};
}
}
/**
* Removes the top scope of the stack.
*
* This ignores it if the body is a breakable statement.
* In this case it's handled in the `exitBreakableStatement` function.
*
* @param {ASTNode} node - A node. This is a LabeledStatement.
* @returns {void}
*/
function exitLabeledStatement(node) {
if (!astUtils.isBreakableStatement(node.body)) {
scopeInfo = scopeInfo.upper;
}
}
/**
* Reports a given control node if it's unnecessary.
*
* @param {ASTNode} node - A node. This is a BreakStatement or a
* ContinueStatement.
* @returns {void}
*/
function reportIfUnnecessary(node) {
if (!node.label) {
return;
}
var labelNode = node.label;
var label = labelNode.name;
var info = scopeInfo;
while (info) {
if (info.breakable || info.label === label) {
if (info.breakable && info.label === label) {
context.report({
node: labelNode,
message: "This label '{{name}}' is unnecessary.",
data: labelNode
});
}
return;
}
info = info.upper;
}
}
return {
"WhileStatement": enterBreakableStatement,
"WhileStatement:exit": exitBreakableStatement,
"DoWhileStatement": enterBreakableStatement,
"DoWhileStatement:exit": exitBreakableStatement,
"ForStatement": enterBreakableStatement,
"ForStatement:exit": exitBreakableStatement,
"ForInStatement": enterBreakableStatement,
"ForInStatement:exit": exitBreakableStatement,
"ForOfStatement": enterBreakableStatement,
"ForOfStatement:exit": exitBreakableStatement,
"SwitchStatement": enterBreakableStatement,
"SwitchStatement:exit": exitBreakableStatement,
"LabeledStatement": enterLabeledStatement,
"LabeledStatement:exit": exitLabeledStatement,
"BreakStatement": reportIfUnnecessary,
"ContinueStatement": reportIfUnnecessary
};
};
module.exports.schema = [];

126
tools/eslint/lib/rules/no-extra-parens.js

@ -13,6 +13,8 @@
module.exports = function(context) { module.exports = function(context) {
var ALL_NODES = context.options[0] !== "functions"; var ALL_NODES = context.options[0] !== "functions";
var EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false;
var sourceCode = context.getSourceCode();
/** /**
* Determines if this rule should be enforced for a node given the current configuration. * Determines if this rule should be enforced for a node given the current configuration.
@ -75,6 +77,32 @@ module.exports = function(context) {
return ruleApplies(node) && isParenthesisedTwice(node); return ruleApplies(node) && isParenthesisedTwice(node);
} }
/**
* Determines if a node test expression is allowed to have a parenthesised assignment
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the assignment can be parenthesised.
* @private
*/
function isCondAssignException(node) {
return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression";
}
/**
* Determines if a node following a [no LineTerminator here] restriction is
* surrounded by (potentially) invalid extra parentheses.
* @param {Token} token - The token preceding the [no LineTerminator here] restriction.
* @param {ASTNode} node - The node to be checked.
* @returns {boolean} True if the node is incorrectly parenthesised.
* @private
*/
function hasExcessParensNoLineTerminator(token, node) {
if (token.loc.end.line === node.loc.start.line) {
return hasExcessParens(node);
}
return hasDoubleExcessParens(node);
}
/** /**
* Checks whether or not a given node is located at the head of ExpressionStatement. * Checks whether or not a given node is located at the head of ExpressionStatement.
* @param {ASTNode} node - A node to check. * @param {ASTNode} node - A node to check.
@ -300,7 +328,7 @@ module.exports = function(context) {
return; return;
} }
// Object literals *must* be parenthesized // Object literals *must* be parenthesised
if (node.body.type === "ObjectExpression" && hasDoubleExcessParens(node.body)) { if (node.body.type === "ObjectExpression" && hasDoubleExcessParens(node.body)) {
report(node.body); report(node.body);
return; return;
@ -326,30 +354,26 @@ module.exports = function(context) {
} }
}, },
"DoWhileStatement": function(node) { "DoWhileStatement": function(node) {
if (hasDoubleExcessParens(node.test)) { if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test); report(node.test);
} }
}, },
"ExpressionStatement": function(node) { "ExpressionStatement": function(node) {
var firstToken; var firstToken, secondToken, firstTokens;
if (hasExcessParens(node.expression)) { if (hasExcessParens(node.expression)) {
firstToken = context.getFirstToken(node.expression); firstTokens = context.getFirstTokens(node.expression, 2);
firstToken = firstTokens[0];
// Pure object literals ({}) do not need parentheses but secondToken = firstTokens[1];
// member expressions do ({}.toString())
if (( if (
firstToken.value !== "{" || !firstToken ||
node.expression.type === "ObjectExpression" firstToken.value !== "{" &&
) && firstToken.value !== "function" &&
// For such as `(function(){}.foo.bar)` firstToken.value !== "class" &&
( (
firstToken.value !== "function" || firstToken.value !== "let" ||
node.expression.type === "FunctionExpression" !secondToken ||
) && secondToken.value !== "["
// For such as `(class{}.foo.bar)`
(
firstToken.value !== "class" ||
node.expression.type === "ClassExpression"
) )
) { ) {
report(node.expression); report(node.expression);
@ -371,7 +395,7 @@ module.exports = function(context) {
report(node.init); report(node.init);
} }
if (node.test && hasExcessParens(node.test)) { if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test); report(node.test);
} }
@ -380,7 +404,7 @@ module.exports = function(context) {
} }
}, },
"IfStatement": function(node) { "IfStatement": function(node) {
if (hasDoubleExcessParens(node.test)) { if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test); report(node.test);
} }
}, },
@ -422,7 +446,10 @@ module.exports = function(context) {
}); });
}, },
"ReturnStatement": function(node) { "ReturnStatement": function(node) {
if (node.argument && hasExcessParens(node.argument) && var returnToken = sourceCode.getFirstToken(node);
if (node.argument &&
hasExcessParensNoLineTerminator(returnToken, node.argument) &&
// RegExp literal is allowed to have parens (#1589) // RegExp literal is allowed to have parens (#1589)
!(node.argument.type === "Literal" && node.argument.regex)) { !(node.argument.type === "Literal" && node.argument.regex)) {
report(node.argument); report(node.argument);
@ -446,7 +473,9 @@ module.exports = function(context) {
} }
}, },
"ThrowStatement": function(node) { "ThrowStatement": function(node) {
if (hasExcessParens(node.argument)) { var throwToken = sourceCode.getFirstToken(node);
if (hasExcessParensNoLineTerminator(throwToken, node.argument)) {
report(node.argument); report(node.argument);
} }
}, },
@ -461,7 +490,7 @@ module.exports = function(context) {
} }
}, },
"WhileStatement": function(node) { "WhileStatement": function(node) {
if (hasDoubleExcessParens(node.test)) { if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test); report(node.test);
} }
}, },
@ -469,13 +498,52 @@ module.exports = function(context) {
if (hasDoubleExcessParens(node.object)) { if (hasDoubleExcessParens(node.object)) {
report(node.object); report(node.object);
} }
},
"YieldExpression": function(node) {
var yieldToken;
if (node.argument) {
yieldToken = sourceCode.getFirstToken(node);
if ((precedence(node.argument) >= precedence(node) &&
hasExcessParensNoLineTerminator(yieldToken, node.argument)) ||
hasDoubleExcessParens(node.argument)) {
report(node.argument);
}
}
} }
}; };
}; };
module.exports.schema = [ module.exports.schema = {
{ "anyOf": [
"enum": ["all", "functions"] {
} "type": "array",
]; "items": [
{
"enum": ["functions"]
}
],
"minItems": 0,
"maxItems": 1
},
{
"type": "array",
"items": [
{
"enum": ["all"]
},
{
"type": "object",
"properties": {
"conditionalAssign": {"type": "boolean"}
},
"additionalProperties": false
}
],
"minItems": 0,
"maxItems": 2
}
]
};

129
tools/eslint/lib/rules/no-fallthrough.js

@ -4,93 +4,84 @@
*/ */
"use strict"; "use strict";
var FALLTHROUGH_COMMENT = /falls?\s?through/i;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { var getLast = require("../util").getLast;
var switches = [];
return {
"SwitchCase": function(node) { //------------------------------------------------------------------------------
// Helpers
var consequent = node.consequent, //------------------------------------------------------------------------------
switchData = switches[switches.length - 1],
i,
comments,
comment;
/* var FALLTHROUGH_COMMENT = /falls?\s?through/i;
* Some developers wrap case bodies in blocks, so if there is just one
* node and it's a block statement, check inside.
*/
if (consequent.length === 1 && consequent[0].type === "BlockStatement") {
consequent = consequent[0];
}
// checking on previous case /**
if (!switchData.lastCaseClosed) { * Checks whether or not a given node has a fallthrough comment.
* @param {ASTNode} node - A SwitchCase node to get comments.
* @param {RuleContext} context - A rule context which stores comments.
* @returns {boolean} `true` if the node has a fallthrough comment.
*/
function hasFallthroughComment(node, context) {
var sourceCode = context.getSourceCode();
var comment = getLast(sourceCode.getComments(node).leading);
// a fall through comment will be the last trailing comment of the last case return Boolean(comment && FALLTHROUGH_COMMENT.test(comment.value));
comments = context.getComments(switchData.lastCase).trailing; }
comment = comments[comments.length - 1];
// unless the user doesn't like semicolons, in which case it's first leading comment of this case /**
if (!comment) { * Checks whether or not a given code path segment is reachable.
comments = context.getComments(node).leading; * @param {CodePathSegment} segment - A CodePathSegment to check.
comment = comments[comments.length - 1]; * @returns {boolean} `true` if the segment is reachable.
} */
function isReachable(segment) {
return segment.reachable;
}
// check for comment //------------------------------------------------------------------------------
if (!comment || !FALLTHROUGH_COMMENT.test(comment.value)) { // Rule Definition
context.report(node, //------------------------------------------------------------------------------
"Expected a \"break\" statement before \"{{code}}\".",
{ code: node.test ? "case" : "default" });
}
}
// now dealing with the current case module.exports = function(context) {
switchData.lastCaseClosed = false; var currentCodePath = null;
switchData.lastCase = node;
// try to verify using statements - go backwards as a fast path for the search // We need to use leading comments of the next SwitchCase node because
if (consequent.length) { // trailing comments is wrong if semicolons are omitted.
for (i = consequent.length - 1; i >= 0; i--) { var fallthroughCase = null;
if (/(?:Break|Continue|Return|Throw)Statement/.test(consequent[i].type)) {
switchData.lastCaseClosed = true;
break;
}
}
} else {
// the case statement has no statements, so it must logically fall through
switchData.lastCaseClosed = true;
}
/* return {
* Any warnings are triggered when the next SwitchCase occurs. "onCodePathStart": function(codePath) {
* There is no need to warn on the last SwitchCase, since it can't currentCodePath = codePath;
* fall through to anything. },
*/ "onCodePathEnd": function() {
currentCodePath = currentCodePath.upper;
}, },
"SwitchStatement": function(node) { "SwitchCase": function(node) {
switches.push({ // Checks whether or not there is a fallthrough comment.
node: node, // And reports the previous fallthrough node if that does not exist.
lastCaseClosed: true, if (fallthroughCase && !hasFallthroughComment(node, context)) {
lastCase: null context.report({
}); message: "Expected a 'break' statement before '{{type}}'.",
data: {type: node.test ? "case" : "default"},
node: node
});
}
fallthroughCase = null;
}, },
"SwitchStatement:exit": function() { "SwitchCase:exit": function(node) {
switches.pop(); // `reachable` meant fall through because statements preceded by
// `break`, `return`, or `throw` are unreachable.
// And allows empty cases and the last case.
if (currentCodePath.currentSegments.some(isReachable) &&
node.consequent.length > 0 &&
getLast(node.parent.cases) !== node
) {
fallthroughCase = node;
}
} }
}; };
}; };
module.exports.schema = []; module.exports.schema = [];

33
tools/eslint/lib/rules/no-func-assign.js

@ -13,29 +13,6 @@ var astUtils = require("../ast-utils");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
module.exports = function(context) { module.exports = function(context) {
var unresolved = Object.create(null);
/**
* Collects unresolved references from the global scope, then creates a map to references from its name.
* Usage of the map is explained at `checkVariable(variable)`.
* @returns {void}
*/
function collectUnresolvedReferences() {
unresolved = Object.create(null);
var references = context.getScope().through;
for (var i = 0; i < references.length; ++i) {
var reference = references[i];
var name = reference.identifier.name;
if (name in unresolved === false) {
unresolved[name] = [];
}
unresolved[name].push(reference);
}
}
/** /**
* Reports a reference if is non initializer and writable. * Reports a reference if is non initializer and writable.
* @param {References} references - Collection of reference to check. * @param {References} references - Collection of reference to check.
@ -57,13 +34,7 @@ module.exports = function(context) {
*/ */
function checkVariable(variable) { function checkVariable(variable) {
if (variable.defs[0].type === "FunctionName") { if (variable.defs[0].type === "FunctionName") {
// If the function is in global scope, its references are not resolved (by escope's design). checkReference(variable.references);
// So when references of the function are nothing, this checks in unresolved.
if (variable.references.length > 0) {
checkReference(variable.references);
} else if (unresolved[variable.name]) {
checkReference(unresolved[variable.name]);
}
} }
} }
@ -77,11 +48,9 @@ module.exports = function(context) {
} }
return { return {
"Program": collectUnresolvedReferences,
"FunctionDeclaration": checkForFunction, "FunctionDeclaration": checkForFunction,
"FunctionExpression": checkForFunction "FunctionExpression": checkForFunction
}; };
}; };
module.exports.schema = []; module.exports.schema = [];

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save