Browse Source

tools: update ESLint to v4.0.0

Update ESLint and configuration to version 4.0.0.

Backport-PR-URL: https://github.com/nodejs/node/pull/14340
PR-URL: https://github.com/nodejs/node/pull/13645
Reviewed-By: Teddy Katz <teddy.katz@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
v6.x
Rich Trott 8 years ago
committed by Myles Borins
parent
commit
4ffe804c81
No known key found for this signature in database GPG Key ID: 933B01F40B5CA946
  1. 29
      .eslintrc.yaml
  2. 2
      test/message/throw_in_line_with_tabs.js
  3. 3939
      tools/eslint/CHANGELOG.md
  4. 1
      tools/eslint/LICENSE
  5. 11
      tools/eslint/README.md
  6. 14
      tools/eslint/bin/eslint.js
  7. 15
      tools/eslint/conf/config-schema.json
  8. 2
      tools/eslint/conf/default-cli-options.js
  9. 33
      tools/eslint/conf/default-config-options.js
  10. 3
      tools/eslint/conf/eslint-all.js
  11. 17
      tools/eslint/conf/eslint-recommended.js
  12. 5
      tools/eslint/lib/api.js
  13. 58
      tools/eslint/lib/ast-utils.js
  14. 327
      tools/eslint/lib/cli-engine.js
  15. 3
      tools/eslint/lib/cli.js
  16. 2
      tools/eslint/lib/code-path-analysis/code-path-state.js
  17. 90
      tools/eslint/lib/config.js
  18. 17
      tools/eslint/lib/config/autoconfig.js
  19. 47
      tools/eslint/lib/config/config-file.js
  20. 68
      tools/eslint/lib/config/config-initializer.js
  21. 14
      tools/eslint/lib/config/config-ops.js
  22. 23
      tools/eslint/lib/config/config-rule.js
  23. 114
      tools/eslint/lib/config/config-validator.js
  24. 72
      tools/eslint/lib/config/environments.js
  25. 123
      tools/eslint/lib/config/plugins.js
  26. 16
      tools/eslint/lib/file-finder.js
  27. 28
      tools/eslint/lib/formatters/codeframe.js
  28. 26
      tools/eslint/lib/formatters/stylish.js
  29. 2
      tools/eslint/lib/formatters/table.js
  30. 15
      tools/eslint/lib/ignored-paths.js
  31. 557
      tools/eslint/lib/linter.js
  32. 9
      tools/eslint/lib/load-rules.js
  33. 156
      tools/eslint/lib/rules.js
  34. 235
      tools/eslint/lib/rules/array-bracket-newline.js
  35. 230
      tools/eslint/lib/rules/array-element-newline.js
  36. 55
      tools/eslint/lib/rules/arrow-parens.js
  37. 2
      tools/eslint/lib/rules/block-scoped-var.js
  38. 6
      tools/eslint/lib/rules/brace-style.js
  39. 6
      tools/eslint/lib/rules/capitalized-comments.js
  40. 4
      tools/eslint/lib/rules/comma-spacing.js
  41. 2
      tools/eslint/lib/rules/comma-style.js
  42. 4
      tools/eslint/lib/rules/complexity.js
  43. 5
      tools/eslint/lib/rules/curly.js
  44. 2
      tools/eslint/lib/rules/default-case.js
  45. 8
      tools/eslint/lib/rules/dot-notation.js
  46. 105
      tools/eslint/lib/rules/for-direction.js
  47. 2
      tools/eslint/lib/rules/func-names.js
  48. 2
      tools/eslint/lib/rules/global-require.js
  49. 2
      tools/eslint/lib/rules/id-length.js
  50. 1125
      tools/eslint/lib/rules/indent-legacy.js
  51. 1769
      tools/eslint/lib/rules/indent.js
  52. 71
      tools/eslint/lib/rules/key-spacing.js
  53. 54
      tools/eslint/lib/rules/line-comment-position.js
  54. 190
      tools/eslint/lib/rules/lines-around-comment.js
  55. 18
      tools/eslint/lib/rules/lines-around-directive.js
  56. 31
      tools/eslint/lib/rules/max-len.js
  57. 2
      tools/eslint/lib/rules/new-parens.js
  58. 8
      tools/eslint/lib/rules/newline-after-var.js
  59. 17
      tools/eslint/lib/rules/newline-before-return.js
  60. 2
      tools/eslint/lib/rules/no-alert.js
  61. 37
      tools/eslint/lib/rules/no-buffer-constructor.js
  62. 2
      tools/eslint/lib/rules/no-compare-neg-zero.js
  63. 12
      tools/eslint/lib/rules/no-confusing-arrow.js
  64. 6
      tools/eslint/lib/rules/no-console.js
  65. 10
      tools/eslint/lib/rules/no-debugger.js
  66. 2
      tools/eslint/lib/rules/no-dupe-args.js
  67. 6
      tools/eslint/lib/rules/no-empty-function.js
  68. 2
      tools/eslint/lib/rules/no-empty.js
  69. 4
      tools/eslint/lib/rules/no-eval.js
  70. 169
      tools/eslint/lib/rules/no-extend-native.js
  71. 50
      tools/eslint/lib/rules/no-extra-parens.js
  72. 2
      tools/eslint/lib/rules/no-fallthrough.js
  73. 18
      tools/eslint/lib/rules/no-floating-decimal.js
  74. 4
      tools/eslint/lib/rules/no-implicit-coercion.js
  75. 7
      tools/eslint/lib/rules/no-inline-comments.js
  76. 8
      tools/eslint/lib/rules/no-inner-declarations.js
  77. 24
      tools/eslint/lib/rules/no-irregular-whitespace.js
  78. 6
      tools/eslint/lib/rules/no-lone-blocks.js
  79. 4
      tools/eslint/lib/rules/no-loop-func.js
  80. 93
      tools/eslint/lib/rules/no-multi-spaces.js
  81. 17
      tools/eslint/lib/rules/no-multiple-empty-lines.js
  82. 2
      tools/eslint/lib/rules/no-redeclare.js
  83. 21
      tools/eslint/lib/rules/no-self-compare.js
  84. 6
      tools/eslint/lib/rules/no-this-before-super.js
  85. 2
      tools/eslint/lib/rules/no-undefined.js
  86. 16
      tools/eslint/lib/rules/no-unexpected-multiline.js
  87. 18
      tools/eslint/lib/rules/no-unmodified-loop-condition.js
  88. 12
      tools/eslint/lib/rules/no-unneeded-ternary.js
  89. 20
      tools/eslint/lib/rules/no-unused-vars.js
  90. 16
      tools/eslint/lib/rules/no-use-before-define.js
  91. 4
      tools/eslint/lib/rules/no-useless-computed-key.js
  92. 2
      tools/eslint/lib/rules/no-useless-escape.js
  93. 10
      tools/eslint/lib/rules/no-var.js
  94. 10
      tools/eslint/lib/rules/no-warning-comments.js
  95. 23
      tools/eslint/lib/rules/object-curly-newline.js
  96. 2
      tools/eslint/lib/rules/object-shorthand.js
  97. 6
      tools/eslint/lib/rules/padded-blocks.js
  98. 587
      tools/eslint/lib/rules/padding-line-between-statements.js
  99. 34
      tools/eslint/lib/rules/prefer-arrow-callback.js
  100. 19
      tools/eslint/lib/rules/prefer-const.js

29
.eslintrc.yaml

@ -38,7 +38,7 @@ rules:
dot-location: [2, property] dot-location: [2, property]
no-fallthrough: 2 no-fallthrough: 2
no-global-assign: 2 no-global-assign: 2
no-multi-spaces: 2 no-multi-spaces: [2, {ignoreEOLComments: true}]
no-octal: 2 no-octal: 2
no-redeclare: 2 no-redeclare: 2
no-self-assign: 2 no-self-assign: 2
@ -93,11 +93,18 @@ rules:
func-call-spacing: 2 func-call-spacing: 2
func-name-matching: 2 func-name-matching: 2
func-style: [2, declaration, {allowArrowFunctions: true}] func-style: [2, declaration, {allowArrowFunctions: true}]
indent: [2, 2, {ArrayExpression: first, # indent: [2, 2, {ArrayExpression: first,
CallExpression: {arguments: first}, # CallExpression: {arguments: first},
MemberExpression: 1, # FunctionDeclaration: {parameters: first},
ObjectExpression: first, # FunctionExpression: {parameters: first},
SwitchCase: 1}] # MemberExpression: off,
# ObjectExpression: first,
# SwitchCase: 1}]
indent-legacy: [2, 2, {ArrayExpression: first,
CallExpression: {arguments: first},
MemberExpression: 1,
ObjectExpression: first,
SwitchCase: 1}]
key-spacing: [2, {mode: minimum}] key-spacing: [2, {mode: minimum}]
keyword-spacing: 2 keyword-spacing: 2
linebreak-style: [2, unix] linebreak-style: [2, unix]
@ -117,10 +124,10 @@ rules:
}, { }, {
selector: "ThrowStatement > CallExpression[callee.name=/Error$/]", selector: "ThrowStatement > CallExpression[callee.name=/Error$/]",
message: "Use new keyword when throwing an Error." message: "Use new keyword when throwing an Error."
}, { }, {
selector: "CallExpression[callee.object.name='assert'][callee.property.name='fail'][arguments.length=1]", selector: "CallExpression[callee.object.name='assert'][callee.property.name='fail'][arguments.length=1]",
message: "assert.fail() message should be third argument" message: "assert.fail() message should be third argument"
}] }]
no-tabs: 2 no-tabs: 2
no-trailing-spaces: 2 no-trailing-spaces: 2
operator-linebreak: [2, after, {overrides: {'?': ignore, ':': ignore}}] operator-linebreak: [2, after, {overrides: {'?': ignore, ':': ignore}}]
@ -128,7 +135,11 @@ rules:
semi: 2 semi: 2
semi-spacing: 2 semi-spacing: 2
space-before-blocks: [2, always] space-before-blocks: [2, always]
space-before-function-paren: [2, never] space-before-function-paren: [2, {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}]
space-in-parens: [2, never] space-in-parens: [2, never]
space-infix-ops: 2 space-infix-ops: 2
space-unary-ops: 2 space-unary-ops: 2

2
test/message/throw_in_line_with_tabs.js

@ -1,4 +1,4 @@
/* eslint-disable indent, no-tabs */ /* eslint-disable indent-legacy, no-tabs */
'use strict'; 'use strict';
require('../common'); require('../common');

3939
tools/eslint/CHANGELOG.md

File diff suppressed because it is too large

1
tools/eslint/LICENSE

@ -1,4 +1,3 @@
ESLint
Copyright JS Foundation and other contributors, https://js.foundation Copyright JS Foundation and other contributors, https://js.foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

11
tools/eslint/README.md

@ -5,6 +5,7 @@
[![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)
[![Join the chat at https://gitter.im/eslint/eslint](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/eslint/eslint?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/eslint/eslint](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/eslint/eslint?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint?ref=badge_shield)
# ESLint # ESLint
@ -13,7 +14,7 @@
[Rules](http://eslint.org/docs/rules/) | [Rules](http://eslint.org/docs/rules/) |
[Contributing](http://eslint.org/docs/developer-guide/contributing) | [Contributing](http://eslint.org/docs/developer-guide/contributing) |
[Reporting Bugs](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) | [Reporting Bugs](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) |
[Code of Conduct](https://js.foundation/conduct/) | [Code of Conduct](https://js.foundation/community/code-of-conduct) |
[Twitter](https://twitter.com/geteslint) | [Twitter](https://twitter.com/geteslint) |
[Mailing List](https://groups.google.com/group/eslint) | [Mailing List](https://groups.google.com/group/eslint) |
[Chat Room](https://gitter.im/eslint/eslint) [Chat Room](https://gitter.im/eslint/eslint)
@ -123,7 +124,7 @@ These folks keep the project moving and are resources for help.
* Michael Ficarra ([@michaelficarra](https://github.com/michaelficarra)) * Michael Ficarra ([@michaelficarra](https://github.com/michaelficarra))
* Mark Pedrotti ([@pedrottimark](https://github.com/pedrottimark)) * Mark Pedrotti ([@pedrottimark](https://github.com/pedrottimark))
* Oleg Gaidarenko ([@markelog](https://github.com/markelog)) * Oleg Gaidarenko ([@markelog](https://github.com/markelog))
* Mike Sherov [@mikesherov](https://github.com/mikesherov)) * Mike Sherov ([@mikesherov](https://github.com/mikesherov))
* Henry Zhu ([@hzoo](https://github.com/hzoo)) * Henry Zhu ([@hzoo](https://github.com/hzoo))
* Marat Dulin ([@mdevils](https://github.com/mdevils)) * Marat Dulin ([@mdevils](https://github.com/mdevils))
* Alexej Yaroshevich ([@zxqfox](https://github.com/zxqfox)) * Alexej Yaroshevich ([@zxqfox](https://github.com/zxqfox))
@ -172,6 +173,10 @@ ESLint follows [semantic versioning](http://semver.org). However, due to the nat
According to our policy, any minor update may report more errors than the previous release (ex: from a bug fix). As such, we recommend using the tilde (`~`) in `package.json` e.g. `"eslint": "~3.1.0"` to guarantee the results of your builds. According to our policy, any minor update may report more errors than the previous release (ex: from a bug fix). As such, we recommend using the tilde (`~`) in `package.json` e.g. `"eslint": "~3.1.0"` to guarantee the results of your builds.
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint?ref=badge_large)
## Frequently Asked Questions ## Frequently Asked Questions
### How is ESLint different from JSHint? ### How is ESLint different from JSHint?
@ -205,7 +210,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi
### What about ECMAScript 6 support? ### What about ECMAScript 6 support?
ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 support through [configuration](http://eslint.org/docs/user-guide/configuring). ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 syntax and global variables through [configuration](http://eslint.org/docs/user-guide/configuring).
### What about experimental features? ### What about experimental features?

14
tools/eslint/bin/eslint.js

@ -61,14 +61,12 @@ if (useStdIn) {
} else if (init) { } else if (init) {
const configInit = require("../lib/config/config-initializer"); const configInit = require("../lib/config/config-initializer");
configInit.initializeConfig(err => { configInit.initializeConfig().then(() => {
if (err) { process.exitCode = 0;
process.exitCode = 1; }).catch(err => {
console.error(err.message); process.exitCode = 1;
console.error(err.stack); console.error(err.message);
} else { console.error(err.stack);
process.exitCode = 0;
}
}); });
} else { } else {
process.exitCode = cli.execute(process.argv); process.exitCode = cli.execute(process.argv);

15
tools/eslint/conf/config-schema.json

@ -0,0 +1,15 @@
{
"type": "object",
"properties": {
"root": { "type": "boolean" },
"globals": { "type": ["object"] },
"parser": { "type": ["string", "null"] },
"env": { "type": "object" },
"plugins": { "type": ["array"] },
"settings": { "type": "object" },
"extends": { "type": ["string", "array"] },
"rules": { "type": "object" },
"parserOptions": { "type": "object" }
},
"additionalProperties": false
}

2
tools/eslint/conf/cli-options.js → tools/eslint/conf/default-cli-options.js

@ -12,11 +12,9 @@ module.exports = {
useEslintrc: true, useEslintrc: true,
envs: [], envs: [],
globals: [], globals: [],
rules: {},
extensions: [".js"], extensions: [".js"],
ignore: true, ignore: true,
ignorePath: null, ignorePath: null,
parser: "", // must be empty
cache: false, cache: false,
// in order to honor the cacheFile option if specified // in order to honor the cacheFile option if specified

33
tools/eslint/conf/default-config-options.js

@ -0,0 +1,33 @@
/**
* @fileoverview Default config options
* @author Teddy Katz
*/
"use strict";
/**
* Freezes an object and all its nested properties
* @param {Object} obj The object to deeply freeze
* @returns {Object} `obj` after freezing it
*/
function deepFreeze(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
Object.keys(obj).map(key => obj[key]).forEach(deepFreeze);
return Object.freeze(obj);
}
module.exports = deepFreeze({
env: {},
globals: {},
rules: {},
settings: {},
parser: "espree",
parserOptions: {
ecmaVersion: 5,
sourceType: "script",
ecmaFeatures: {}
}
});

3
tools/eslint/conf/eslint-all.js

@ -10,7 +10,8 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const load = require("../lib/load-rules"), const load = require("../lib/load-rules"),
rules = require("../lib/rules"); Rules = require("../lib/rules");
const rules = new Rules();
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers

17
tools/eslint/conf/eslint-recommended.js

@ -10,15 +10,14 @@
/* eslint-disable sort-keys */ /* eslint-disable sort-keys */
module.exports = { module.exports = {
parser: "espree",
ecmaFeatures: {},
rules: { rules: {
/* eslint-enable sort-keys */ /* eslint-enable sort-keys */
"accessor-pairs": "off", "accessor-pairs": "off",
"array-bracket-newline": "off",
"array-bracket-spacing": "off", "array-bracket-spacing": "off",
"array-callback-return": "off", "array-callback-return": "off",
"array-element-newline": "off",
"arrow-body-style": "off", "arrow-body-style": "off",
"arrow-parens": "off", "arrow-parens": "off",
"arrow-spacing": "off", "arrow-spacing": "off",
@ -43,6 +42,7 @@ module.exports = {
"dot-notation": "off", "dot-notation": "off",
"eol-last": "off", "eol-last": "off",
"eqeqeq": "off", "eqeqeq": "off",
"for-direction": "off",
"func-call-spacing": "off", "func-call-spacing": "off",
"func-name-matching": "off", "func-name-matching": "off",
"func-names": "off", "func-names": "off",
@ -55,6 +55,7 @@ module.exports = {
"id-length": "off", "id-length": "off",
"id-match": "off", "id-match": "off",
"indent": "off", "indent": "off",
"indent-legacy": "off",
"init-declarations": "off", "init-declarations": "off",
"jsx-quotes": "off", "jsx-quotes": "off",
"key-spacing": "off", "key-spacing": "off",
@ -80,11 +81,12 @@ module.exports = {
"no-array-constructor": "off", "no-array-constructor": "off",
"no-await-in-loop": "off", "no-await-in-loop": "off",
"no-bitwise": "off", "no-bitwise": "off",
"no-buffer-constructor": "off",
"no-caller": "off", "no-caller": "off",
"no-case-declarations": "error", "no-case-declarations": "error",
"no-catch-shadow": "off", "no-catch-shadow": "off",
"no-class-assign": "error", "no-class-assign": "error",
"no-compare-neg-zero": "off", "no-compare-neg-zero": "error",
"no-cond-assign": "error", "no-cond-assign": "error",
"no-confusing-arrow": "off", "no-confusing-arrow": "off",
"no-console": "error", "no-console": "error",
@ -202,7 +204,7 @@ module.exports = {
"no-useless-computed-key": "off", "no-useless-computed-key": "off",
"no-useless-concat": "off", "no-useless-concat": "off",
"no-useless-constructor": "off", "no-useless-constructor": "off",
"no-useless-escape": "off", "no-useless-escape": "error",
"no-useless-rename": "off", "no-useless-rename": "off",
"no-useless-return": "off", "no-useless-return": "off",
"no-var": "off", "no-var": "off",
@ -212,7 +214,7 @@ module.exports = {
"no-with": "off", "no-with": "off",
"nonblock-statement-body-position": "off", "nonblock-statement-body-position": "off",
"object-curly-newline": "off", "object-curly-newline": "off",
"object-curly-spacing": ["off", "never"], "object-curly-spacing": "off",
"object-property-newline": "off", "object-property-newline": "off",
"object-shorthand": "off", "object-shorthand": "off",
"one-var": "off", "one-var": "off",
@ -220,6 +222,7 @@ module.exports = {
"operator-assignment": "off", "operator-assignment": "off",
"operator-linebreak": "off", "operator-linebreak": "off",
"padded-blocks": "off", "padded-blocks": "off",
"padding-line-between-statements": "off",
"prefer-arrow-callback": "off", "prefer-arrow-callback": "off",
"prefer-const": "off", "prefer-const": "off",
"prefer-destructuring": "off", "prefer-destructuring": "off",
@ -238,6 +241,7 @@ module.exports = {
"rest-spread-spacing": "off", "rest-spread-spacing": "off",
"semi": "off", "semi": "off",
"semi-spacing": "off", "semi-spacing": "off",
"semi-style": "off",
"sort-imports": "off", "sort-imports": "off",
"sort-keys": "off", "sort-keys": "off",
"sort-vars": "off", "sort-vars": "off",
@ -248,6 +252,7 @@ module.exports = {
"space-unary-ops": "off", "space-unary-ops": "off",
"spaced-comment": "off", "spaced-comment": "off",
"strict": "off", "strict": "off",
"switch-colon-spacing": "off",
"symbol-description": "off", "symbol-description": "off",
"template-curly-spacing": "off", "template-curly-spacing": "off",
"template-tag-spacing": "off", "template-tag-spacing": "off",

5
tools/eslint/lib/api.js

@ -5,8 +5,11 @@
"use strict"; "use strict";
const Linter = require("./linter");
module.exports = { module.exports = {
linter: require("./eslint"), linter: new Linter(),
Linter,
CLIEngine: require("./cli-engine"), CLIEngine: require("./cli-engine"),
RuleTester: require("./testers/rule-tester"), RuleTester: require("./testers/rule-tester"),
SourceCode: require("./util/source-code") SourceCode: require("./util/source-code")

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

@ -10,6 +10,7 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const esutils = require("esutils"); const esutils = require("esutils");
const espree = require("espree");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -27,6 +28,7 @@ const thisTagPattern = /^[\s*]*@this/m;
const COMMENTS_IGNORE_PATTERN = /^\s*(?:eslint|jshint\s+|jslint\s+|istanbul\s+|globals?\s+|exported\s+|jscs)/; const COMMENTS_IGNORE_PATTERN = /^\s*(?:eslint|jshint\s+|jslint\s+|istanbul\s+|globals?\s+|exported\s+|jscs)/;
const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/; const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/;
const SHEBANG_MATCHER = /^#!([^\r\n]+)/;
// A set of node types that can contain a list of statements // A set of node types that can contain a list of statements
const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]); const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]);
@ -243,7 +245,7 @@ function hasJSDocThisTag(node, sourceCode) {
// because callbacks don't have its JSDoc comment. // because callbacks don't have its JSDoc comment.
// e.g. // e.g.
// sinon.test(/* @this sinon.Sandbox */function() { this.spy(); }); // sinon.test(/* @this sinon.Sandbox */function() { this.spy(); });
return sourceCode.getComments(node).leading.some(comment => thisTagPattern.test(comment.value)); return sourceCode.getCommentsBefore(node).some(comment => thisTagPattern.test(comment.value));
} }
/** /**
@ -412,6 +414,7 @@ module.exports = {
COMMENTS_IGNORE_PATTERN, COMMENTS_IGNORE_PATTERN,
LINEBREAKS, LINEBREAKS,
LINEBREAK_MATCHER, LINEBREAK_MATCHER,
SHEBANG_MATCHER,
STATEMENT_LIST_PARENTS, STATEMENT_LIST_PARENTS,
/** /**
@ -524,7 +527,7 @@ module.exports = {
/** /**
* Returns whether the provided node is an ESLint directive comment or not * Returns whether the provided node is an ESLint directive comment or not
* @param {LineComment|BlockComment} node The node to be checked * @param {Line|Block} node The comment token to be checked
* @returns {boolean} `true` if the node is an ESLint directive comment * @returns {boolean} `true` if the node is an ESLint directive comment
*/ */
isDirectiveComment(node) { isDirectiveComment(node) {
@ -556,9 +559,9 @@ module.exports = {
/** /**
* Finds the variable by a given name in a given scope and its upper scopes. * Finds the variable by a given name in a given scope and its upper scopes.
* *
* @param {escope.Scope} initScope - A scope to start find. * @param {eslint-scope.Scope} initScope - A scope to start find.
* @param {string} name - A variable name to find. * @param {string} name - A variable name to find.
* @returns {escope.Variable|null} A found variable or `null`. * @returns {eslint-scope.Variable|null} A found variable or `null`.
*/ */
getVariableByName(initScope, name) { getVariableByName(initScope, name) {
let scope = initScope; let scope = initScope;
@ -1252,5 +1255,52 @@ module.exports = {
* `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020
*/ */
return node.type === "Literal" && node.value === null && !node.regex; return node.type === "Literal" && node.value === null && !node.regex;
},
/**
* Determines whether two tokens can safely be placed next to each other without merging into a single token
* @param {Token|string} leftValue The left token. If this is a string, it will be tokenized and the last token will be used.
* @param {Token|string} rightValue The right token. If this is a string, it will be tokenized and the first token will be used.
* @returns {boolean} If the tokens cannot be safely placed next to each other, returns `false`. If the tokens can be placed
* next to each other, behavior is undefined (although it should return `true` in most cases).
*/
canTokensBeAdjacent(leftValue, rightValue) {
let leftToken;
if (typeof leftValue === "string") {
const leftTokens = espree.tokenize(leftValue, { ecmaVersion: 2015 });
leftToken = leftTokens[leftTokens.length - 1];
} else {
leftToken = leftValue;
}
const rightToken = typeof rightValue === "string" ? espree.tokenize(rightValue, { ecmaVersion: 2015 })[0] : rightValue;
if (leftToken.type === "Punctuator" || rightToken.type === "Punctuator") {
if (leftToken.type === "Punctuator" && rightToken.type === "Punctuator") {
const PLUS_TOKENS = new Set(["+", "++"]);
const MINUS_TOKENS = new Set(["-", "--"]);
return !(
PLUS_TOKENS.has(leftToken.value) && PLUS_TOKENS.has(rightToken.value) ||
MINUS_TOKENS.has(leftToken.value) && MINUS_TOKENS.has(rightToken.value)
);
}
return true;
}
if (
leftToken.type === "String" || rightToken.type === "String" ||
leftToken.type === "Template" || rightToken.type === "Template"
) {
return true;
}
if (leftToken.type !== "Numeric" && rightToken.type === "Numeric" && rightToken.value.startsWith(".")) {
return true;
}
return false;
} }
}; };

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

@ -17,12 +17,10 @@
const fs = require("fs"), const fs = require("fs"),
path = require("path"), path = require("path"),
rules = require("./rules"), defaultOptions = require("../conf/default-cli-options"),
eslint = require("./eslint"), Linter = require("./linter"),
defaultOptions = require("../conf/cli-options"),
IgnoredPaths = require("./ignored-paths"), IgnoredPaths = require("./ignored-paths"),
Config = require("./config"), Config = require("./config"),
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"),
@ -73,8 +71,10 @@ const debug = require("debug")("eslint:cli-engine");
* @typedef {Object} LintResult * @typedef {Object} LintResult
* @property {string} filePath The path to the file that was linted. * @property {string} filePath The path to the file that was linted.
* @property {LintMessage[]} messages All of the messages for the result. * @property {LintMessage[]} messages All of the messages for the result.
* @property {number} errorCount Number or errors for the result. * @property {number} errorCount Number of errors for the result.
* @property {number} warningCount Number or warnings for the result. * @property {number} warningCount Number of warnings for the result.
* @property {number} fixableErrorCount Number of fixable errors for the result.
* @property {number} fixableWarningCount Number of fixable warnings for the result.
* @property {string=} [source] The source code of the file that was linted. * @property {string=} [source] The source code of the file that was linted.
* @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible. * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
*/ */
@ -93,13 +93,21 @@ function calculateStatsPerFile(messages) {
return messages.reduce((stat, message) => { return messages.reduce((stat, message) => {
if (message.fatal || message.severity === 2) { if (message.fatal || message.severity === 2) {
stat.errorCount++; stat.errorCount++;
if (message.fix) {
stat.fixableErrorCount++;
}
} else { } else {
stat.warningCount++; stat.warningCount++;
if (message.fix) {
stat.fixableWarningCount++;
}
} }
return stat; return stat;
}, { }, {
errorCount: 0, errorCount: 0,
warningCount: 0 warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
}); });
} }
@ -113,10 +121,14 @@ function calculateStatsPerRun(results) {
return results.reduce((stat, result) => { return results.reduce((stat, result) => {
stat.errorCount += result.errorCount; stat.errorCount += result.errorCount;
stat.warningCount += result.warningCount; stat.warningCount += result.warningCount;
stat.fixableErrorCount += result.fixableErrorCount;
stat.fixableWarningCount += result.fixableWarningCount;
return stat; return stat;
}, { }, {
errorCount: 0, errorCount: 0,
warningCount: 0 warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
}); });
} }
@ -129,11 +141,12 @@ function calculateStatsPerRun(results) {
* @param {string} options.filename The filename from which the text was read. * @param {string} options.filename The filename from which the text was read.
* @param {boolean} options.allowInlineConfig Flag indicating if inline comments * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
* should be allowed. * should be allowed.
* @param {Linter} linter Linter context
* @returns {Object} The result of the fix operation as returned from the * @returns {Object} The result of the fix operation as returned from the
* SourceCodeFixer. * SourceCodeFixer.
* @private * @private
*/ */
function multipassFix(text, config, options) { function multipassFix(text, config, options, linter) {
const MAX_PASSES = 10; const MAX_PASSES = 10;
let messages = [], let messages = [],
fixedResult, fixedResult,
@ -153,10 +166,10 @@ function multipassFix(text, config, options) {
passNumber++; passNumber++;
debug(`Linting code for ${options.filename} (pass ${passNumber})`); debug(`Linting code for ${options.filename} (pass ${passNumber})`);
messages = eslint.verify(text, config, options); messages = linter.verify(text, config, options);
debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`); debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
fixedResult = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages); fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
// stop if there are any syntax errors. // stop if there are any syntax errors.
// 'fixedResult.output' is a empty string. // 'fixedResult.output' is a empty string.
@ -181,7 +194,7 @@ function multipassFix(text, config, options) {
* the most up-to-date information. * the most up-to-date information.
*/ */
if (fixedResult.fixed) { if (fixedResult.fixed) {
fixedResult.messages = eslint.verify(text, config, options); fixedResult.messages = linter.verify(text, config, options);
} }
@ -200,13 +213,14 @@ function multipassFix(text, config, options) {
* @param {string} filename An optional string representing the texts filename. * @param {string} filename An optional string representing the texts filename.
* @param {boolean} fix Indicates if fixes should be processed. * @param {boolean} fix Indicates if fixes should be processed.
* @param {boolean} allowInlineConfig Allow/ignore comments that change config. * @param {boolean} allowInlineConfig Allow/ignore comments that change config.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this text. * @returns {LintResult} The results for linting on this text.
* @private * @private
*/ */
function processText(text, configHelper, filename, fix, allowInlineConfig) { function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
// clear all existing settings for a new file // clear all existing settings for a new file
eslint.reset(); linter.reset();
let filePath, let filePath,
messages, messages,
@ -224,10 +238,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
const config = configHelper.getConfig(filePath); const config = configHelper.getConfig(filePath);
if (config.plugins) { if (config.plugins) {
Plugins.loadAll(config.plugins); configHelper.plugins.loadAll(config.plugins);
} }
const loadedPlugins = Plugins.getAll(); const loadedPlugins = configHelper.plugins.getAll();
for (const plugin in loadedPlugins) { for (const 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) {
@ -242,7 +256,7 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
const unprocessedMessages = []; const unprocessedMessages = [];
parsedBlocks.forEach(block => { parsedBlocks.forEach(block => {
unprocessedMessages.push(eslint.verify(block, config, { unprocessedMessages.push(linter.verify(block, config, {
filename, filename,
allowInlineConfig allowInlineConfig
})); }));
@ -258,10 +272,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
fixedResult = multipassFix(text, config, { fixedResult = multipassFix(text, config, {
filename, filename,
allowInlineConfig allowInlineConfig
}); }, linter);
messages = fixedResult.messages; messages = fixedResult.messages;
} else { } else {
messages = eslint.verify(text, config, { messages = linter.verify(text, config, {
filename, filename,
allowInlineConfig allowInlineConfig
}); });
@ -274,7 +288,9 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
filePath: filename, filePath: filename,
messages, messages,
errorCount: stats.errorCount, errorCount: stats.errorCount,
warningCount: stats.warningCount warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
}; };
if (fixedResult && fixedResult.fixed) { if (fixedResult && fixedResult.fixed) {
@ -294,13 +310,14 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
* @param {string} filename The filename of the file being checked. * @param {string} filename The filename of the file being checked.
* @param {Object} configHelper The configuration options for ESLint. * @param {Object} configHelper The configuration options for ESLint.
* @param {Object} options The CLIEngine options object. * @param {Object} options The CLIEngine options object.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this file. * @returns {LintResult} The results for linting on this file.
* @private * @private
*/ */
function processFile(filename, configHelper, options) { function processFile(filename, configHelper, options, linter) {
const text = fs.readFileSync(path.resolve(filename), "utf8"), const text = fs.readFileSync(path.resolve(filename), "utf8"),
result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig); result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig, linter);
return result; return result;
@ -339,7 +356,9 @@ function createIgnoreResult(filePath, baseDir) {
} }
], ],
errorCount: 0, errorCount: 0,
warningCount: 1 warningCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0
}; };
} }
@ -432,141 +451,104 @@ function getCacheFile(cacheFile, cwd) {
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** class CLIEngine {
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} options The options for this instance.
* @constructor
*/
function CLIEngine(options) {
options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
options
);
/**
* Stored options for this instance
* @type {Object}
*/
this.options = options;
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
/** /**
* Cache used to avoid operating on files that haven't changed since the * Creates a new instance of the core CLI engine.
* last successful execution (e.g., file passed linting with no errors and * @param {CLIEngineOptions} options The options for this instance.
* no warnings). * @constructor
* @type {Object}
*/ */
this._fileCache = fileEntryCache.create(cacheFile); constructor(options) {
// load in additional rules
if (this.options.rulePaths) {
const cwd = this.options.cwd;
this.options.rulePaths.forEach(rulesdir => {
debug(`Loading rules from ${rulesdir}`);
rules.load(rulesdir, cwd);
});
}
Object.keys(this.options.rules || {}).forEach(name => {
validator.validateRuleOptions(name, this.options.rules[name], "CLI");
});
}
/** options = Object.assign(
* Returns the formatter representing the given format or null if no formatter Object.create(null),
* with the given name can be found. defaultOptions,
* @param {string} [format] The name of the format to load or the path to a { cwd: process.cwd() },
* custom formatter. options
* @returns {Function} The formatter function or null if not found. );
*/
CLIEngine.getFormatter = function(format) {
let formatterPath;
// default is stylish /**
format = format || "stylish"; * Stored options for this instance
* @type {Object}
*/
this.options = options;
this.linter = new Linter();
// only strings are valid formatters const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
if (typeof format === "string") {
// replace \ with / for Windows compatibility /**
format = format.replace(/\\/g, "/"); * Cache used to avoid operating on files that haven't changed since the
* last successful execution (e.g., file passed linting with no errors and
* no warnings).
* @type {Object}
*/
this._fileCache = fileEntryCache.create(cacheFile);
// if there's a slash, then it's a file // load in additional rules
if (format.indexOf("/") > -1) { if (this.options.rulePaths) {
const cwd = this.options ? this.options.cwd : process.cwd(); const cwd = this.options.cwd;
formatterPath = path.resolve(cwd, format); this.options.rulePaths.forEach(rulesdir => {
} else { debug(`Loading rules from ${rulesdir}`);
formatterPath = `./formatters/${format}`; this.linter.rules.load(rulesdir, cwd);
});
} }
try { Object.keys(this.options.rules || {}).forEach(name => {
return require(formatterPath); validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
} catch (ex) { });
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
throw ex;
}
} else { this.config = new Config(this.options, this.linter);
return null;
} }
};
/** /**
* Returns results that only contains errors. * Returns results that only contains errors.
* @param {LintResult[]} results The results to filter. * @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results. * @returns {LintResult[]} The filtered results.
*/ */
CLIEngine.getErrorResults = function(results) { static getErrorResults(results) {
const filtered = []; const filtered = [];
results.forEach(result => { results.forEach(result => {
const filteredMessages = result.messages.filter(isErrorMessage); const filteredMessages = result.messages.filter(isErrorMessage);
if (filteredMessages.length > 0) { if (filteredMessages.length > 0) {
filtered.push( filtered.push(
Object.assign(result, { Object.assign(result, {
messages: filteredMessages, messages: filteredMessages,
errorCount: filteredMessages.length, errorCount: filteredMessages.length,
warningCount: 0 warningCount: 0,
}) fixableErrorCount: result.fixableErrorCount,
); fixableWarningCount: 0
} })
}); );
}
return filtered; });
};
/** return filtered;
* Outputs fixes from the given results to files. }
* @param {Object} report The report object created by CLIEngine.
* @returns {void}
*/
CLIEngine.outputFixes = function(report) {
report.results.filter(result => result.hasOwnProperty("output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
};
CLIEngine.prototype = { /**
* Outputs fixes from the given results to files.
* @param {Object} report The report object created by CLIEngine.
* @returns {void}
*/
static outputFixes(report) {
report.results.filter(result => result.hasOwnProperty("output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
}
constructor: CLIEngine,
/** /**
* Add a plugin by passing it's configuration * Add a plugin by passing its configuration
* @param {string} name Name of the plugin. * @param {string} name Name of the plugin.
* @param {Object} pluginobject Plugin configuration object. * @param {Object} pluginobject Plugin configuration object.
* @returns {void} * @returns {void}
*/ */
addPlugin(name, pluginobject) { addPlugin(name, pluginobject) {
Plugins.define(name, pluginobject); this.config.plugins.define(name, pluginobject);
}, }
/** /**
* Resolves the patterns passed into executeOnFiles() into glob-based patterns * Resolves the patterns passed into executeOnFiles() into glob-based patterns
@ -576,7 +558,7 @@ CLIEngine.prototype = {
*/ */
resolveFileGlobPatterns(patterns) { resolveFileGlobPatterns(patterns) {
return globUtil.resolveFileGlobPatterns(patterns, this.options); return globUtil.resolveFileGlobPatterns(patterns, this.options);
}, }
/** /**
* Executes the current configuration on an array of file and directory names. * Executes the current configuration on an array of file and directory names.
@ -587,7 +569,7 @@ CLIEngine.prototype = {
const results = [], const results = [],
options = this.options, options = this.options,
fileCache = this._fileCache, fileCache = this._fileCache,
configHelper = new Config(options); configHelper = this.config;
let prevConfig; // the previous configuration used let prevConfig; // the previous configuration used
/** /**
@ -624,9 +606,10 @@ CLIEngine.prototype = {
* 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 * @param {boolean} warnIgnored always warn when a file is ignored
* @param {Linter} linter Linter context
* @returns {void} * @returns {void}
*/ */
function executeOnFile(filename, warnIgnored) { function executeOnFile(filename, warnIgnored, linter) {
let hashOfConfig, let hashOfConfig,
descriptor; descriptor;
@ -669,7 +652,7 @@ CLIEngine.prototype = {
debug(`Processing ${filename}`); debug(`Processing ${filename}`);
const res = processFile(filename, configHelper, options); const res = processFile(filename, configHelper, options, linter);
if (options.cache) { if (options.cache) {
@ -702,12 +685,11 @@ CLIEngine.prototype = {
const startTime = Date.now(); const startTime = Date.now();
patterns = this.resolveFileGlobPatterns(patterns); patterns = this.resolveFileGlobPatterns(patterns);
const fileList = globUtil.listFilesToProcess(patterns, options); const fileList = globUtil.listFilesToProcess(patterns, options);
fileList.forEach(fileInfo => { fileList.forEach(fileInfo => {
executeOnFile(fileInfo.filename, fileInfo.ignored); executeOnFile(fileInfo.filename, fileInfo.ignored, this.linter);
}); });
const stats = calculateStatsPerRun(results); const stats = calculateStatsPerRun(results);
@ -723,9 +705,11 @@ CLIEngine.prototype = {
return { return {
results, results,
errorCount: stats.errorCount, errorCount: stats.errorCount,
warningCount: stats.warningCount warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
}; };
}, }
/** /**
* Executes the current configuration on text. * Executes the current configuration on text.
@ -738,7 +722,7 @@ CLIEngine.prototype = {
const results = [], const results = [],
options = this.options, options = this.options,
configHelper = new Config(options), configHelper = this.config,
ignoredPaths = new IgnoredPaths(options); ignoredPaths = new IgnoredPaths(options);
// resolve filename based on options.cwd (for reporting, ignoredPaths also resolves) // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
@ -751,7 +735,7 @@ CLIEngine.prototype = {
results.push(createIgnoreResult(filename, options.cwd)); results.push(createIgnoreResult(filename, options.cwd));
} }
} else { } else {
results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig)); results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig, this.linter));
} }
const stats = calculateStatsPerRun(results); const stats = calculateStatsPerRun(results);
@ -759,9 +743,11 @@ CLIEngine.prototype = {
return { return {
results, results,
errorCount: stats.errorCount, errorCount: stats.errorCount,
warningCount: stats.warningCount warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
}; };
}, }
/** /**
* Returns a configuration object for the given file based on the CLI options. * Returns a configuration object for the given file based on the CLI options.
@ -771,10 +757,10 @@ CLIEngine.prototype = {
* @returns {Object} A configuration object for the file. * @returns {Object} A configuration object for the file.
*/ */
getConfigForFile(filePath) { getConfigForFile(filePath) {
const configHelper = new Config(this.options); const configHelper = this.config;
return configHelper.getConfig(filePath); return configHelper.getConfig(filePath);
}, }
/** /**
* Checks if a given path is ignored by ESLint. * Checks if a given path is ignored by ESLint.
@ -786,12 +772,51 @@ CLIEngine.prototype = {
const ignoredPaths = new IgnoredPaths(this.options); const ignoredPaths = new IgnoredPaths(this.options);
return ignoredPaths.contains(resolvedPath); return ignoredPaths.contains(resolvedPath);
}, }
/**
* Returns the formatter representing the given format or null if no formatter
* with the given name can be found.
* @param {string} [format] The name of the format to load or the path to a
* custom formatter.
* @returns {Function} The formatter function or null if not found.
*/
getFormatter(format) {
let formatterPath;
getFormatter: CLIEngine.getFormatter // default is stylish
format = format || "stylish";
}; // only strings are valid formatters
if (typeof format === "string") {
// replace \ with / for Windows compatibility
format = format.replace(/\\/g, "/");
// if there's a slash, then it's a file
if (format.indexOf("/") > -1) {
const cwd = this.options ? this.options.cwd : process.cwd();
formatterPath = path.resolve(cwd, format);
} else {
formatterPath = `./formatters/${format}`;
}
try {
return require(formatterPath);
} catch (ex) {
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
throw ex;
}
} else {
return null;
}
}
}
CLIEngine.version = pkg.version; CLIEngine.version = pkg.version;
CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
module.exports = CLIEngine; module.exports = CLIEngine;

3
tools/eslint/lib/cli.js

@ -17,7 +17,6 @@
const fs = require("fs"), const fs = require("fs"),
path = require("path"), path = require("path"),
shell = require("shelljs"),
options = require("./options"), options = require("./options"),
CLIEngine = require("./cli-engine"), CLIEngine = require("./cli-engine"),
mkdirp = require("mkdirp"), mkdirp = require("mkdirp"),
@ -83,7 +82,7 @@ function printResults(engine, results, format, outputFile) {
if (outputFile) { if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile); const filePath = path.resolve(process.cwd(), outputFile);
if (shell.test("-d", filePath)) { if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile); log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false; return false;
} }

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

@ -988,7 +988,7 @@ class CodePathState {
switch (context.type) { switch (context.type) {
case "WhileStatement": case "WhileStatement":
case "ForStatement": case "ForStatement":
choiceContext = this.popChoiceContext(); this.popChoiceContext();
makeLooped( makeLooped(
this, this,
forkContext.head, forkContext.head,

90
tools/eslint/lib/config.js

@ -10,13 +10,12 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const path = require("path"), const path = require("path"),
os = require("os"),
ConfigOps = require("./config/config-ops"), ConfigOps = require("./config/config-ops"),
ConfigFile = require("./config/config-file"), ConfigFile = require("./config/config-file"),
Plugins = require("./config/plugins"), Plugins = require("./config/plugins"),
FileFinder = require("./file-finder"), FileFinder = require("./file-finder"),
userHome = require("user-home"), isResolvable = require("is-resolvable");
isResolvable = require("is-resolvable"),
pathIsInside = require("path-is-inside");
const debug = require("debug")("eslint:config"); const debug = require("debug")("eslint:config");
@ -24,7 +23,7 @@ const debug = require("debug")("eslint:config");
// Constants // Constants
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const PERSONAL_CONFIG_DIR = userHome || null; const PERSONAL_CONFIG_DIR = os.homedir() || null;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -43,10 +42,11 @@ function isObject(item) {
/** /**
* Load and parse a JSON config object from a file. * Load and parse a JSON config object from a file.
* @param {string|Object} configToLoad the path to the JSON config file or the config object itself. * @param {string|Object} configToLoad the path to the JSON config file or the config object itself.
* @param {Config} configContext config instance object
* @returns {Object} the parsed config object (empty object if there was a parse error) * @returns {Object} the parsed config object (empty object if there was a parse error)
* @private * @private
*/ */
function loadConfig(configToLoad) { function loadConfig(configToLoad, configContext) {
let config = {}, let config = {},
filePath = ""; filePath = "";
@ -56,32 +56,33 @@ function loadConfig(configToLoad) {
config = configToLoad; config = configToLoad;
if (config.extends) { if (config.extends) {
config = ConfigFile.applyExtends(config, filePath); config = ConfigFile.applyExtends(config, configContext, filePath);
} }
} else { } else {
filePath = configToLoad; filePath = configToLoad;
config = ConfigFile.load(filePath); config = ConfigFile.load(filePath, configContext);
} }
} }
return config; return config;
} }
/** /**
* Get personal config object from ~/.eslintrc. * Get personal config object from ~/.eslintrc.
* @param {Config} configContext Plugin context for the config instance
* @returns {Object} the personal config object (null if there is no personal config) * @returns {Object} the personal config object (null if there is no personal config)
* @private * @private
*/ */
function getPersonalConfig() { function getPersonalConfig(configContext) {
let config; let config;
if (PERSONAL_CONFIG_DIR) { if (PERSONAL_CONFIG_DIR) {
const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
if (filename) { if (filename) {
debug("Using personal config"); debug("Using personal config");
config = loadConfig(filename); config = loadConfig(filename, configContext);
} }
} }
@ -99,21 +100,18 @@ function hasRules(options) {
/** /**
* Get a local config object. * Get a local config object.
* @param {Object} thisConfig A Config object. * @param {Config} thisConfig A Config object.
* @param {string} directory The directory to start looking in for a local config file. * @param {string} directory The directory to start looking in for a local config file.
* @returns {Object} The local config object, or an empty object if there is no local config. * @returns {Object} The local config object, or an empty object if there is no local config.
*/ */
function getLocalConfig(thisConfig, directory) { function getLocalConfig(thisConfig, directory) {
const localConfigFiles = thisConfig.findLocalConfigFiles(directory),
numFiles = localConfigFiles.length,
projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd);
let found,
config = {},
rootPath;
for (let i = 0; i < numFiles; i++) { const projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd);
const localConfigFiles = thisConfig.findLocalConfigFiles(directory);
let found,
config = {};
const localConfigFile = localConfigFiles[i]; for (const localConfigFile of localConfigFiles) {
// Don't consider the personal config file in the home directory, // Don't consider the personal config file in the home directory,
// except if the home directory is the same as the current working directory // except if the home directory is the same as the current working directory
@ -121,27 +119,22 @@ function getLocalConfig(thisConfig, directory) {
continue; continue;
} }
// If root flag is set, don't consider file if it is above root
if (rootPath && !pathIsInside(path.dirname(localConfigFile), rootPath)) {
continue;
}
debug(`Loading ${localConfigFile}`); debug(`Loading ${localConfigFile}`);
const localConfig = loadConfig(localConfigFile); const localConfig = loadConfig(localConfigFile, thisConfig);
// Don't consider a local config file found if the config is null // Don't consider a local config file found if the config is null
if (!localConfig) { if (!localConfig) {
continue; continue;
} }
// Check for root flag
if (localConfig.root === true) {
rootPath = path.dirname(localConfigFile);
}
found = true; found = true;
debug(`Using ${localConfigFile}`); debug(`Using ${localConfigFile}`);
config = ConfigOps.merge(localConfig, config); config = ConfigOps.merge(localConfig, config);
// Check for root flag
if (localConfig.root === true) {
break;
}
} }
if (!found && !thisConfig.useSpecificConfig) { if (!found && !thisConfig.useSpecificConfig) {
@ -152,7 +145,7 @@ function getLocalConfig(thisConfig, directory) {
* - Otherwise, if no rules were manually passed in, throw and error. * - Otherwise, if no rules were manually passed in, throw and error.
* - Note: This function is not called if useEslintrc is false. * - Note: This function is not called if useEslintrc is false.
*/ */
const personalConfig = getPersonalConfig(); const personalConfig = getPersonalConfig(thisConfig);
if (personalConfig) { if (personalConfig) {
config = ConfigOps.merge(config, personalConfig); config = ConfigOps.merge(config, personalConfig);
@ -186,17 +179,21 @@ class Config {
/** /**
* Config options * Config options
* @param {Object} options Options to be passed in * @param {Object} options Options to be passed in
* @param {Linter} linterContext Linter instance object
*/ */
constructor(options) { constructor(options, linterContext) {
options = options || {}; options = options || {};
this.linterContext = linterContext;
this.plugins = new Plugins(linterContext.environments, linterContext.rules);
this.ignore = options.ignore; this.ignore = options.ignore;
this.ignorePath = options.ignorePath; this.ignorePath = options.ignorePath;
this.cache = {}; this.cache = {};
this.parser = options.parser; this.parser = options.parser;
this.parserOptions = options.parserOptions || {}; this.parserOptions = options.parserOptions || {};
this.baseConfig = options.baseConfig ? loadConfig(options.baseConfig) : { rules: {} }; this.baseConfig = options.baseConfig ? loadConfig(options.baseConfig, this) : { rules: {} };
this.useEslintrc = (options.useEslintrc !== false); this.useEslintrc = (options.useEslintrc !== false);
@ -219,16 +216,22 @@ class Config {
return globals; return globals;
}, {}); }, {});
const useConfig = options.configFile;
this.options = options; this.options = options;
this.loadConfigFile(options.configFile);
}
if (useConfig) { /**
debug(`Using command line config ${useConfig}`); * Loads the config from the configuration file
if (isResolvable(useConfig) || isResolvable(`eslint-config-${useConfig}`) || useConfig.charAt(0) === "@") { * @param {string} configFile - patch to the config file
this.useSpecificConfig = loadConfig(useConfig); * @returns {undefined}
*/
loadConfigFile(configFile) {
if (configFile) {
debug(`Using command line config ${configFile}`);
if (isResolvable(configFile) || isResolvable(`eslint-config-${configFile}`) || configFile.charAt(0) === "@") {
this.useSpecificConfig = loadConfig(configFile, this);
} else { } else {
this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, useConfig)); this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, configFile), this);
} }
} }
} }
@ -248,7 +251,6 @@ class Config {
debug(`Constructing config for ${filePath ? filePath : "text"}`); debug(`Constructing config for ${filePath ? filePath : "text"}`);
config = this.cache[directory]; config = this.cache[directory];
if (config) { if (config) {
debug("Using config from cache"); debug("Using config from cache");
return config; return config;
@ -306,13 +308,13 @@ class Config {
// 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");
Plugins.loadAll(this.options.plugins); this.plugins.loadAll(this.options.plugins);
config = ConfigOps.merge(config, { plugins: this.options.plugins }); config = ConfigOps.merge(config, { plugins: this.options.plugins });
} }
// Step 9: Apply environments to the config if present // Step 9: Apply environments to the config if present
if (config.env) { if (config.env) {
config = ConfigOps.applyEnvironments(config); config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
} }
this.cache[directory] = config; this.cache[directory] = config;
@ -323,7 +325,7 @@ class Config {
/** /**
* Find local config files from directory and parent directories. * Find local config files from directory and parent directories.
* @param {string} directory The directory to start searching from. * @param {string} directory The directory to start searching from.
* @returns {string[]} The paths of local config files found. * @returns {GeneratorFunction} The paths of local config files found.
*/ */
findLocalConfigFiles(directory) { findLocalConfigFiles(directory) {

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

@ -10,12 +10,13 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const lodash = require("lodash"), const lodash = require("lodash"),
eslint = require("../eslint"), Linter = require("../linter"),
configRule = require("./config-rule"), configRule = require("./config-rule"),
ConfigOps = require("./config-ops"), ConfigOps = require("./config-ops"),
recConfig = require("../../conf/eslint-recommended"); recConfig = require("../../conf/eslint-recommended");
const debug = require("debug")("eslint:autoconfig"); const debug = require("debug")("eslint:autoconfig");
const linter = new Linter();
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Data // Data
@ -37,11 +38,11 @@ const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only
* @param {number} errorCount The number of errors encountered when linting with the config * @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 * This callback is used to measure execution status in a progress bar
* @callback progressCallback * @callback progressCallback
* @param {number} The total number of times the callback will be called. * @param {number} The total number of times the callback will be called.
*/ */
/** /**
* Create registryItems for rules * Create registryItems for rules
@ -290,7 +291,7 @@ class Registry {
ruleSets.forEach(ruleSet => { ruleSets.forEach(ruleSet => {
const lintConfig = Object.assign({}, config, { rules: ruleSet }); const lintConfig = Object.assign({}, config, { rules: ruleSet });
const lintResults = eslint.verify(sourceCodes[filename], lintConfig); const lintResults = linter.verify(sourceCodes[filename], lintConfig);
lintResults.forEach(result => { lintResults.forEach(result => {
@ -310,7 +311,7 @@ class Registry {
ruleSetIdx += 1; ruleSetIdx += 1;
if (cb) { if (cb) {
cb(totalFilesLinting); // eslint-disable-line callback-return cb(totalFilesLinting); // eslint-disable-line callback-return
} }
}); });

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

@ -13,17 +13,13 @@
const fs = require("fs"), const fs = require("fs"),
path = require("path"), path = require("path"),
shell = require("shelljs"),
ConfigOps = require("./config-ops"), ConfigOps = require("./config-ops"),
validator = require("./config-validator"), validator = require("./config-validator"),
Plugins = require("./plugins"),
pathUtil = require("../util/path-util"), pathUtil = require("../util/path-util"),
ModuleResolver = require("../util/module-resolver"), ModuleResolver = require("../util/module-resolver"),
pathIsInside = require("path-is-inside"), pathIsInside = require("path-is-inside"),
stripBom = require("strip-bom"),
stripComments = require("strip-json-comments"), stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify"), stringify = require("json-stable-stringify"),
defaultOptions = require("../../conf/eslint-recommended"),
requireUncached = require("require-uncached"); requireUncached = require("require-uncached");
const debug = require("debug")("eslint:config-file"); const debug = require("debug")("eslint:config-file");
@ -63,11 +59,11 @@ const resolver = new ModuleResolver();
/** /**
* Convenience wrapper for synchronously reading file contents. * Convenience wrapper for synchronously reading file contents.
* @param {string} filePath The filename to read. * @param {string} filePath The filename to read.
* @returns {string} The file contents. * @returns {string} The file contents, with the BOM removed.
* @private * @private
*/ */
function readFile(filePath) { function readFile(filePath) {
return stripBom(fs.readFileSync(filePath, "utf8")); return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");
} }
/** /**
@ -368,18 +364,18 @@ function getLookupPath(configFilePath) {
function getEslintCoreConfigPath(name) { function getEslintCoreConfigPath(name) {
if (name === "eslint:recommended") { if (name === "eslint:recommended") {
/* /*
* Add an explicit substitution for eslint:recommended to * Add an explicit substitution for eslint:recommended to
* conf/eslint-recommended.js. * conf/eslint-recommended.js.
*/ */
return path.resolve(__dirname, "../../conf/eslint-recommended.js"); return path.resolve(__dirname, "../../conf/eslint-recommended.js");
} }
if (name === "eslint:all") { if (name === "eslint:all") {
/* /*
* Add an explicit substitution for eslint:all to conf/eslint-all.js * Add an explicit substitution for eslint:all to conf/eslint-all.js
*/ */
return path.resolve(__dirname, "../../conf/eslint-all.js"); return path.resolve(__dirname, "../../conf/eslint-all.js");
} }
@ -389,6 +385,7 @@ function getEslintCoreConfigPath(name) {
/** /**
* 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.
* @param {Config} configContext Plugin context for the config instance
* @param {string} filePath The file path from which the configuration information * @param {string} filePath The file path from which the configuration information
* was loaded. * was loaded.
* @param {string} [relativeTo] The path to resolve relative to. * @param {string} [relativeTo] The path to resolve relative to.
@ -396,7 +393,7 @@ function getEslintCoreConfigPath(name) {
* loaded and merged. * loaded and merged.
* @private * @private
*/ */
function applyExtends(config, filePath, relativeTo) { function applyExtends(config, configContext, filePath, relativeTo) {
let configExtends = config.extends; let configExtends = config.extends;
// normalize into an array for easier handling // normalize into an array for easier handling
@ -421,7 +418,7 @@ function applyExtends(config, filePath, relativeTo) {
); );
} }
debug(`Loading ${parentPath}`); debug(`Loading ${parentPath}`);
return ConfigOps.merge(load(parentPath, false, relativeTo), previousValue); return ConfigOps.merge(load(parentPath, configContext, false, relativeTo), previousValue);
} catch (e) { } catch (e) {
/* /*
@ -516,19 +513,18 @@ function resolve(filePath, relativeTo) {
return { filePath }; return { 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 {Config} configContext Plugins context
* @param {boolean} [applyEnvironments=false] Set to true to merge in environment settings. * @param {boolean} [applyEnvironments=false] Set to true to merge in environment settings.
* @param {string} [relativeTo] The path to resolve relative to. * @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} The configuration information. * @returns {Object} The configuration information.
* @private
*/ */
function load(filePath, applyEnvironments, relativeTo) { function load(filePath, configContext, applyEnvironments, relativeTo) {
const resolvedPath = resolve(filePath, relativeTo), const resolvedPath = resolve(filePath, relativeTo),
dirname = path.dirname(resolvedPath.filePath), dirname = path.dirname(resolvedPath.filePath),
lookupPath = getLookupPath(dirname); lookupPath = getLookupPath(dirname);
@ -538,12 +534,7 @@ function load(filePath, applyEnvironments, relativeTo) {
// ensure plugins are properly loaded first // ensure plugins are properly loaded first
if (config.plugins) { if (config.plugins) {
Plugins.loadAll(config.plugins); configContext.plugins.loadAll(config.plugins);
}
// remove parser from config if it is the default parser
if (config.parser === defaultOptions.parser) {
config.parser = null;
} }
// include full path of parser if present // include full path of parser if present
@ -556,20 +547,20 @@ function load(filePath, applyEnvironments, relativeTo) {
} }
// validate the configuration before continuing // validate the configuration before continuing
validator.validate(config, filePath); validator.validate(config, filePath, configContext.linterContext.rules, configContext.linterContext.environments);
/* /*
* If an `extends` property is defined, it represents a configuration file to use as * If an `extends` property is defined, it represents a configuration file to use as
* a "parent". Load the referenced file and merge the configuration recursively. * a "parent". Load the referenced file and merge the configuration recursively.
*/ */
if (config.extends) { if (config.extends) {
config = applyExtends(config, filePath, dirname); config = applyExtends(config, configContext, filePath, dirname);
} }
if (config.env && applyEnvironments) { if (config.env && applyEnvironments) {
// Merge in environment-specific globals and parserOptions. // Merge in environment-specific globals and parserOptions.
config = ConfigOps.applyEnvironments(config); config = ConfigOps.applyEnvironments(config, configContext.linterContext.environments);
} }
} }
@ -603,7 +594,7 @@ module.exports = {
for (let i = 0, len = CONFIG_FILES.length; i < len; i++) { for (let i = 0, len = CONFIG_FILES.length; i < len; i++) {
const filename = path.join(directory, CONFIG_FILES[i]); const filename = path.join(directory, CONFIG_FILES[i]);
if (shell.test("-f", filename)) { if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {
return filename; return filename;
} }
} }

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

@ -282,13 +282,12 @@ function getConfigForStyleGuide(guide) {
/* istanbul ignore next: no need to test inquirer*/ /* istanbul ignore next: no need to test inquirer*/
/** /**
* Ask use a few questions on command prompt * Ask use a few questions on command prompt
* @param {Function} callback callback function when file has been written * @returns {Promise} The promise with the result of the prompt
* @returns {void}
*/ */
function promptUser(callback) { function promptUser() {
let config; let config;
inquirer.prompt([ return inquirer.prompt([
{ {
type: "list", type: "list",
name: "source", name: "source",
@ -343,29 +342,26 @@ function promptUser(callback) {
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto"); return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
} }
} }
], earlyAnswers => { ]).then(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") {
if (!earlyAnswers.packageJsonExists) { if (!earlyAnswers.packageJsonExists) {
log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again."); log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
return; return void 0;
} }
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) { if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
earlyAnswers.styleguide = "airbnb-base"; earlyAnswers.styleguide = "airbnb-base";
} }
try {
config = getConfigForStyleGuide(earlyAnswers.styleguide); config = getConfigForStyleGuide(earlyAnswers.styleguide);
writeFile(config, earlyAnswers.format); writeFile(config, earlyAnswers.format);
} catch (err) {
callback(err); return void 0;
return;
}
return;
} }
// continue with the questions otherwise... // continue with the questions otherwise...
inquirer.prompt([ return inquirer.prompt([
{ {
type: "confirm", type: "confirm",
name: "es6", name: "es6",
@ -412,25 +408,21 @@ function promptUser(callback) {
return answers.jsx; return answers.jsx;
} }
} }
], secondAnswers => { ]).then(secondAnswers => {
// early exit if you are using automatic style generation // early exit if you are using automatic style generation
if (earlyAnswers.source === "auto") { if (earlyAnswers.source === "auto") {
try { const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
config = processAnswers(combinedAnswers);
config = processAnswers(combinedAnswers); installModules(config);
installModules(config); writeFile(config, earlyAnswers.format);
writeFile(config, earlyAnswers.format);
} catch (err) { return void 0;
callback(err);
return;
}
return;
} }
// continue with the style questions otherwise... // continue with the style questions otherwise...
inquirer.prompt([ return inquirer.prompt([
{ {
type: "list", type: "list",
name: "indent", name: "indent",
@ -465,16 +457,12 @@ function promptUser(callback) {
default: "JavaScript", default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"] choices: ["JavaScript", "YAML", "JSON"]
} }
], answers => { ]).then(answers => {
try { const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
config = processAnswers(totalAnswers);
config = processAnswers(totalAnswers); installModules(config);
installModules(config); writeFile(config, answers.format);
writeFile(config, answers.format);
} catch (err) {
callback(err); // eslint-disable-line callback-return
}
}); });
}); });
}); });
@ -487,8 +475,8 @@ function promptUser(callback) {
const init = { const init = {
getConfigForStyleGuide, getConfigForStyleGuide,
processAnswers, processAnswers,
/* istanbul ignore next */initializeConfig(callback) { /* istanbul ignore next */initializeConfig() {
promptUser(callback); return promptUser();
} }
}; };

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

@ -9,8 +9,6 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const Environments = require("./environments");
const debug = require("debug")("eslint:config-ops"); const debug = require("debug")("eslint:config-ops");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -46,10 +44,11 @@ module.exports = {
/** /**
* Creates an environment config based on the specified environments. * Creates an environment config based on the specified environments.
* @param {Object<string,boolean>} env The environment settings. * @param {Object<string,boolean>} env The environment settings.
* @param {Environments} envContext The environment context.
* @returns {Object} A configuration object with the appropriate rules and globals * @returns {Object} A configuration object with the appropriate rules and globals
* set. * set.
*/ */
createEnvironmentConfig(env) { createEnvironmentConfig(env, envContext) {
const envConfig = this.createEmptyConfig(); const envConfig = this.createEmptyConfig();
@ -58,7 +57,7 @@ module.exports = {
envConfig.env = env; envConfig.env = env;
Object.keys(env).filter(name => env[name]).forEach(name => { Object.keys(env).filter(name => env[name]).forEach(name => {
const environment = Environments.get(name); const environment = envContext.get(name);
if (environment) { if (environment) {
debug(`Creating config for environment ${name}`); debug(`Creating config for environment ${name}`);
@ -80,12 +79,13 @@ module.exports = {
* Given a config with environment settings, applies the globals and * Given a config with environment settings, applies the globals and
* ecmaFeatures to the configuration and returns the result. * ecmaFeatures to the configuration and returns the result.
* @param {Object} config The configuration information. * @param {Object} config The configuration information.
* @param {Environments} envContent env context.
* @returns {Object} The updated configuration information. * @returns {Object} The updated configuration information.
*/ */
applyEnvironments(config) { applyEnvironments(config, envContent) {
if (config.env && typeof config.env === "object") { if (config.env && typeof config.env === "object") {
debug("Apply environment settings to config"); debug("Apply environment settings to config");
return this.merge(this.createEnvironmentConfig(config.env), config); return this.merge(this.createEnvironmentConfig(config.env, envContent), config);
} }
return config; return config;
@ -175,7 +175,7 @@ module.exports = {
} }
Object.keys(src).forEach(key => { Object.keys(src).forEach(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" || key === "extends", isRule);
} else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") { } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
dst[key] = src[key]; dst[key] = src[key];
} else { } else {

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

@ -9,9 +9,10 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const rules = require("../rules"), const Rules = require("../rules"),
loadRules = require("../load-rules"); loadRules = require("../load-rules");
const rules = new Rules();
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -168,16 +169,16 @@ function combinePropertyObjects(objArr1, objArr2) {
return res; return res;
} }
/** /**
* Creates a new instance of a rule configuration set * Creates a new instance of a rule configuration set
* *
* A rule configuration set is an array of configurations that are valid for a * 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: * given rule. For example, the configuration set for the "semi" rule could be:
* *
* ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]] * ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
* *
* Rule configuration set class * Rule configuration set class
*/ */
class RuleConfigSet { class RuleConfigSet {
/** /**

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

@ -9,9 +9,8 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const rules = require("../rules"), const schemaValidator = require("is-my-json-valid"),
Environments = require("./environments"), configSchema = require("../../conf/config-schema.json"),
schemaValidator = require("is-my-json-valid"),
util = require("util"); util = require("util");
const validators = { const validators = {
@ -25,10 +24,11 @@ const validators = {
/** /**
* Gets a complete options schema for a rule. * Gets a complete options schema for a rule.
* @param {string} id The rule's unique name. * @param {string} id The rule's unique name.
* @param {Rules} rulesContext Rule context
* @returns {Object} JSON Schema for the rule's options. * @returns {Object} JSON Schema for the rule's options.
*/ */
function getRuleOptionsSchema(id) { function getRuleOptionsSchema(id, rulesContext) {
const rule = rules.get(id), const rule = rulesContext.get(id),
schema = rule && rule.schema || rule && rule.meta && rule.meta.schema; schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
// Given a tuple of schemas, insert warning level at the beginning // Given a tuple of schemas, insert warning level at the beginning
@ -72,10 +72,11 @@ function validateRuleSeverity(options) {
* Validates the non-severity options passed to a rule, based on its schema. * Validates the non-severity options passed to a rule, based on its schema.
* @param {string} id The rule's unique name * @param {string} id The rule's unique name
* @param {array} localOptions The options for the rule, excluding severity * @param {array} localOptions The options for the rule, excluding severity
* @param {Rules} rulesContext Rule context
* @returns {void} * @returns {void}
*/ */
function validateRuleSchema(id, localOptions) { function validateRuleSchema(id, localOptions, rulesContext) {
const schema = getRuleOptionsSchema(id); const schema = getRuleOptionsSchema(id, rulesContext);
if (!validators.rules[id] && schema) { if (!validators.rules[id] && schema) {
validators.rules[id] = schemaValidator(schema, { verbose: true }); validators.rules[id] = schemaValidator(schema, { verbose: true });
@ -95,15 +96,16 @@ function validateRuleSchema(id, localOptions) {
* Validates a rule's options against its schema. * Validates a rule's options against its schema.
* @param {string} id The rule's unique name. * @param {string} id The rule's unique name.
* @param {array|number} options The given options for the rule. * @param {array|number} options The given options for the rule.
* @param {string} source The name of the configuration source. * @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext Rule context
* @returns {void} * @returns {void}
*/ */
function validateRuleOptions(id, options, source) { function validateRuleOptions(id, options, source, rulesContext) {
try { try {
const severity = validateRuleSeverity(options); const severity = validateRuleSeverity(options);
if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) { if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) {
validateRuleSchema(id, Array.isArray(options) ? options.slice(1) : []); validateRuleSchema(id, Array.isArray(options) ? options.slice(1) : [], rulesContext);
} }
} catch (err) { } catch (err) {
throw new Error(`${source}:\n\tConfiguration for rule "${id}" is invalid:\n${err.message}`); throw new Error(`${source}:\n\tConfiguration for rule "${id}" is invalid:\n${err.message}`);
@ -113,51 +115,91 @@ function validateRuleOptions(id, options, source) {
/** /**
* Validates an environment object * Validates an environment object
* @param {Object} environment The environment config object to validate. * @param {Object} environment The environment config object to validate.
* @param {string} source The location to report with any errors. * @param {string} source The name of the configuration source to report in any errors.
* @param {Environments} envContext Env context
* @returns {void} * @returns {void}
*/ */
function validateEnvironment(environment, source) { function validateEnvironment(environment, source, envContext) {
// not having an environment is ok // not having an environment is ok
if (!environment) { if (!environment) {
return; return;
} }
if (Array.isArray(environment)) { Object.keys(environment).forEach(env => {
throw new Error("Environment must not be an array"); if (!envContext.get(env)) {
} const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`;
throw new Error(message);
}
});
}
if (typeof environment === "object") { /**
Object.keys(environment).forEach(env => { * Validates a rules config object
if (!Environments.get(env)) { * @param {Object} rulesConfig The rules config object to validate.
const message = [ * @param {string} source The name of the configuration source to report in any errors.
source, ":\n", * @param {Rules} rulesContext Rule context
"\tEnvironment key \"", env, "\" is unknown\n" * @returns {void}
]; */
function validateRules(rulesConfig, source, rulesContext) {
throw new Error(message.join("")); if (!rulesConfig) {
} return;
});
} else {
throw new Error("Environment must be an object");
} }
Object.keys(rulesConfig).forEach(id => {
validateRuleOptions(id, rulesConfig[id], source, rulesContext);
});
} }
/** /**
* Validates an entire config object. * Formats an array of schema validation errors.
* @param {Array} errors An array of error messages to format.
* @returns {string} Formatted error message
*/
function formatErrors(errors) {
return errors.map(error => {
if (error.message === "has additional properties") {
return `Unexpected top-level property "${error.value.replace(/^data\./, "")}"`;
}
if (error.message === "is the wrong type") {
const formattedField = error.field.replace(/^data\./, "");
const formattedExpectedType = typeof error.type === "string" ? error.type : error.type.join("/");
const formattedValue = JSON.stringify(error.value);
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
}
return `"${error.field.replace(/^(data\.)/, "")}" ${error.message}. Value: ${error.value}`;
}).map(message => `\t- ${message}.\n`).join("");
}
/**
* Validates the top level properties of the config object.
* @param {Object} config The config object to validate. * @param {Object} config The config object to validate.
* @param {string} source The location to report with any errors. * @param {string} source The name of the configuration source to report in any errors.
* @returns {void} * @returns {void}
*/ */
function validate(config, source) { function validateConfigSchema(config, source) {
const validator = schemaValidator(configSchema, { verbose: true });
if (typeof config.rules === "object") { if (!validator(config)) {
Object.keys(config.rules).forEach(id => { throw new Error(`${source}:\n\tESLint configuration is invalid:\n${formatErrors(validator.errors)}`);
validateRuleOptions(id, config.rules[id], source);
});
} }
}
validateEnvironment(config.env, source); /**
* Validates an entire config object.
* @param {Object} config The config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext The rules context
* @param {Environments} envContext The env context
* @returns {void}
*/
function validate(config, source, rulesContext, envContext) {
validateConfigSchema(config, source);
validateRules(config.rules, source, rulesContext);
validateEnvironment(config.env, source, envContext);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

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

@ -11,32 +11,30 @@
const envs = require("../../conf/environments"); const envs = require("../../conf/environments");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Private // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let environments = new Map(); class Environments {
/** /**
* Loads the default environments. * create env context
* @returns {void} */
* @private constructor() {
*/ this._environments = new Map();
function load() {
Object.keys(envs).forEach(envName => {
environments.set(envName, envs[envName]);
});
}
// always load default environments upfront
load();
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { this.load();
}
load, /**
* Loads the default environments.
* @returns {void}
* @private
*/
load() {
Object.keys(envs).forEach(envName => {
this._environments.set(envName, envs[envName]);
});
}
/** /**
* Gets the environment with the given name. * Gets the environment with the given name.
@ -44,8 +42,19 @@ module.exports = {
* @returns {Object?} The environment object or null if not found. * @returns {Object?} The environment object or null if not found.
*/ */
get(name) { get(name) {
return environments.get(name) || null; return this._environments.get(name) || null;
}, }
/**
* Gets all the environment present
* @returns {Object} The environment object for each env name
*/
getAll() {
return Array.from(this._environments).reduce((coll, env) => {
coll[env[0]] = env[1];
return coll;
}, {});
}
/** /**
* Defines an environment. * Defines an environment.
@ -54,8 +63,8 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
define(name, env) { define(name, env) {
environments.set(name, env); this._environments.set(name, env);
}, }
/** /**
* Imports all environments from a plugin. * Imports all environments from a plugin.
@ -69,14 +78,7 @@ module.exports = {
this.define(`${pluginName}/${envName}`, plugin.environments[envName]); this.define(`${pluginName}/${envName}`, plugin.environments[envName]);
}); });
} }
},
/**
* Resets all environments. Only use for tests!
* @returns {void}
*/
testReset() {
environments = new Map();
load();
} }
}; }
module.exports = Environments;

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

@ -8,56 +8,61 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const Environments = require("./environments"),
Rules = require("../rules");
const debug = require("debug")("eslint:plugins"); const debug = require("debug")("eslint:plugins");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Private // Private
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let plugins = Object.create(null);
const PLUGIN_NAME_PREFIX = "eslint-plugin-", const PLUGIN_NAME_PREFIX = "eslint-plugin-",
NAMESPACE_REGEX = /^@.*\//i; NAMESPACE_REGEX = /^@.*\//i;
/** //------------------------------------------------------------------------------
* Removes the prefix `eslint-plugin-` from a plugin name. // Public Interface
* @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. * Plugin class
* @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) { class Plugins {
return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
}
/** /**
* Removes the namespace from a plugin name. * Creates the plugins context
* @param {string} pluginName The name of the plugin which may have the prefix. * @param {Environments} envContext - env context
* @returns {string} The name of the plugin without the namespace. * @param {Rules} rulesContext - rules context
*/ */
function removeNamespace(pluginName) { constructor(envContext, rulesContext) {
return pluginName.replace(NAMESPACE_REGEX, ""); this._plugins = Object.create(null);
} this._environments = envContext;
this._rules = rulesContext;
}
//------------------------------------------------------------------------------ /**
// Public Interface * 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.
*/
static removePrefix(pluginName) {
return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.substring(PLUGIN_NAME_PREFIX.length) : pluginName;
}
module.exports = { /**
* 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.
*/
static getNamespace(pluginName) {
return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
}
removePrefix, /**
getNamespace, * Removes the namespace from a plugin name.
removeNamespace, * @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without the namespace.
*/
static removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}
/** /**
* Defines a plugin with a given name rather than loading from disk. * Defines a plugin with a given name rather than loading from disk.
@ -66,22 +71,16 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
define(pluginName, plugin) { define(pluginName, plugin) {
const pluginNamespace = getNamespace(pluginName), const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = removeNamespace(pluginName), pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace), pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix; shortName = pluginNamespace + pluginNameWithoutPrefix;
// load up environments and rules // load up environments and rules
plugins[shortName] = plugin; this._plugins[shortName] = plugin;
Environments.importPlugin(plugin, shortName); this._environments.importPlugin(plugin, shortName);
Rules.importPlugin(plugin, shortName); this._rules.importPlugin(plugin, shortName);
}
// load up environments and rules for the name that '@scope/' was omitted
// 3 lines below will be removed by 4.0.0
plugins[pluginNameWithoutPrefix] = plugin;
Environments.importPlugin(plugin, pluginNameWithoutPrefix);
Rules.importPlugin(plugin, pluginNameWithoutPrefix);
},
/** /**
* Gets a plugin with the given name. * Gets a plugin with the given name.
@ -89,16 +88,16 @@ module.exports = {
* @returns {Object} The plugin or null if not loaded. * @returns {Object} The plugin or null if not loaded.
*/ */
get(pluginName) { get(pluginName) {
return plugins[pluginName] || null; return this._plugins[pluginName] || null;
}, }
/** /**
* Returns all plugins that are loaded. * Returns all plugins that are loaded.
* @returns {Object} The plugins cache. * @returns {Object} The plugins cache.
*/ */
getAll() { getAll() {
return plugins; return this._plugins;
}, }
/** /**
* Loads a plugin with the given name. * Loads a plugin with the given name.
@ -107,9 +106,9 @@ module.exports = {
* @throws {Error} If the plugin cannot be loaded. * @throws {Error} If the plugin cannot be loaded.
*/ */
load(pluginName) { load(pluginName) {
const pluginNamespace = getNamespace(pluginName), const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = removeNamespace(pluginName), pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace), pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix, shortName = pluginNamespace + pluginNameWithoutPrefix,
longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix; longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
let plugin = null; let plugin = null;
@ -124,7 +123,7 @@ module.exports = {
throw whitespaceError; throw whitespaceError;
} }
if (!plugins[shortName]) { if (!this._plugins[shortName]) {
try { try {
plugin = require(longName); plugin = require(longName);
} catch (pluginLoadErr) { } catch (pluginLoadErr) {
@ -150,7 +149,7 @@ module.exports = {
this.define(pluginName, plugin); this.define(pluginName, plugin);
} }
}, }
/** /**
* Loads all plugins from an array. * Loads all plugins from an array.
@ -160,13 +159,7 @@ module.exports = {
*/ */
loadAll(pluginNames) { loadAll(pluginNames) {
pluginNames.forEach(this.load, this); pluginNames.forEach(this.load, this);
},
/**
* Resets plugin information. Use for tests only.
* @returns {void}
*/
testReset() {
plugins = Object.create(null);
} }
}; }
module.exports = Plugins;

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

@ -25,6 +25,7 @@ const fs = require("fs"),
*/ */
function getDirectoryEntries(directory) { function getDirectoryEntries(directory) {
try { try {
return fs.readdirSync(directory); return fs.readdirSync(directory);
} catch (ex) { } catch (ex) {
return []; return [];
@ -79,9 +80,9 @@ class FileFinder {
* Searches for all the file names in this.fileNames. * Searches for all the file names in this.fileNames.
* Is currently used by lib/config.js to find .eslintrc and package.json files. * Is currently used by lib/config.js to find .eslintrc and package.json files.
* @param {string} directory The directory to start the search from. * @param {string} directory The directory to start the search from.
* @returns {string[]} The file paths found. * @returns {GeneratorFunction} to iterate the file paths found
*/ */
findAllInDirectoryAndParents(directory) { *findAllInDirectoryAndParents(directory) {
const cache = this.cache; const cache = this.cache;
if (directory) { if (directory) {
@ -91,7 +92,8 @@ class FileFinder {
} }
if (cache.hasOwnProperty(directory)) { if (cache.hasOwnProperty(directory)) {
return cache[directory]; yield* cache[directory];
return; // to avoid doing the normal loop afterwards
} }
const dirs = []; const dirs = [];
@ -114,19 +116,21 @@ class FileFinder {
for (let j = 0; j < searched; j++) { for (let j = 0; j < searched; j++) {
cache[dirs[j]].push(filePath); cache[dirs[j]].push(filePath);
} }
yield filePath;
break; break;
} }
} }
} }
const child = directory; const child = directory;
// Assign parent directory to directory. // Assign parent directory to directory.
directory = path.dirname(directory); directory = path.dirname(directory);
if (directory === child) { if (directory === child) {
return cache[dirs[0]]; return;
} }
} while (!cache.hasOwnProperty(directory)); } while (!cache.hasOwnProperty(directory));
// Add what has been cached previously to the cache of each directory searched. // Add what has been cached previously to the cache of each directory searched.
@ -134,7 +138,7 @@ class FileFinder {
dirs.push.apply(cache[dirs[i]], cache[directory]); dirs.push.apply(cache[dirs[i]], cache[directory]);
} }
return cache[dirs[0]]; yield* cache[dirs[0]];
} }
} }

28
tools/eslint/lib/formatters/codeframe.js

@ -74,11 +74,14 @@ function formatMessage(message, parentResult) {
* Gets the formatted output summary for a given number of errors and warnings. * Gets the formatted output summary for a given number of errors and warnings.
* @param {number} errors The number of errors. * @param {number} errors The number of errors.
* @param {number} warnings The number of warnings. * @param {number} warnings The number of warnings.
* @param {number} fixableErrors The number of fixable errors.
* @param {number} fixableWarnings The number of fixable warnings.
* @returns {string} The formatted output summary. * @returns {string} The formatted output summary.
*/ */
function formatSummary(errors, warnings) { function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
const summaryColor = errors > 0 ? "red" : "yellow"; const summaryColor = errors > 0 ? "red" : "yellow";
const summary = []; const summary = [];
const fixablesSummary = [];
if (errors > 0) { if (errors > 0) {
summary.push(`${errors} ${pluralize("error", errors)}`); summary.push(`${errors} ${pluralize("error", errors)}`);
@ -88,7 +91,21 @@ function formatSummary(errors, warnings) {
summary.push(`${warnings} ${pluralize("warning", warnings)}`); summary.push(`${warnings} ${pluralize("warning", warnings)}`);
} }
return chalk[summaryColor].bold(`${summary.join(" and ")} found.`); if (fixableErrors > 0) {
fixablesSummary.push(`${fixableErrors} ${pluralize("error", fixableErrors)}`);
}
if (fixableWarnings > 0) {
fixablesSummary.push(`${fixableWarnings} ${pluralize("warning", fixableWarnings)}`);
}
let output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
if (fixableErrors || fixableWarnings) {
output += chalk[summaryColor].bold(`\n${fixablesSummary.join(" and ")} potentially fixable with the \`--fix\` option.`);
}
return output;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -98,6 +115,9 @@ function formatSummary(errors, warnings) {
module.exports = function(results) { module.exports = function(results) {
let errors = 0; let errors = 0;
let warnings = 0; let warnings = 0;
let fixableErrors = 0;
let fixableWarnings = 0;
const resultsWithMessages = results.filter(result => result.messages.length > 0); const resultsWithMessages = results.filter(result => result.messages.length > 0);
let output = resultsWithMessages.reduce((resultsOutput, result) => { let output = resultsWithMessages.reduce((resultsOutput, result) => {
@ -105,12 +125,14 @@ module.exports = function(results) {
errors += result.errorCount; errors += result.errorCount;
warnings += result.warningCount; warnings += result.warningCount;
fixableErrors += result.fixableErrorCount;
fixableWarnings += result.fixableWarningCount;
return resultsOutput.concat(messages); return resultsOutput.concat(messages);
}, []).join("\n"); }, []).join("\n");
output += "\n"; output += "\n";
output += formatSummary(errors, warnings); output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
return (errors + warnings) > 0 ? output : ""; return (errors + warnings) > 0 ? output : "";
}; };

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

@ -28,8 +28,10 @@ function pluralize(word, count) {
module.exports = function(results) { module.exports = function(results) {
let output = "\n", let output = "\n",
errors = 0, errorCount = 0,
warnings = 0, warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow"; summaryColor = "yellow";
results.forEach(result => { results.forEach(result => {
@ -39,8 +41,10 @@ module.exports = function(results) {
return; return;
} }
errors += result.errorCount; errorCount += result.errorCount;
warnings += result.warningCount; warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
output += `${chalk.underline(result.filePath)}\n`; output += `${chalk.underline(result.filePath)}\n`;
@ -73,14 +77,22 @@ module.exports = function(results) {
).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`; ).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
}); });
const total = errors + warnings; const total = errorCount + warningCount;
if (total > 0) { if (total > 0) {
output += chalk[summaryColor].bold([ output += chalk[summaryColor].bold([
"\u2716 ", total, pluralize(" problem", total), "\u2716 ", total, pluralize(" problem", total),
" (", errors, pluralize(" error", errors), ", ", " (", errorCount, pluralize(" error", errorCount), ", ",
warnings, pluralize(" warning", warnings), ")\n" warningCount, pluralize(" warning", warningCount), ")\n"
].join("")); ].join(""));
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
output += chalk[summaryColor].bold([
" ", fixableErrorCount, pluralize(" error", fixableErrorCount), ", ",
fixableWarningCount, pluralize(" warning", fixableWarningCount),
" potentially fixable with the `--fix` option.\n"
].join(""));
}
} }
return total > 0 ? output : ""; return total > 0 ? output : "";

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

@ -9,7 +9,7 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const chalk = require("chalk"), const chalk = require("chalk"),
table = require("table").default, table = require("table").table,
pluralize = require("pluralize"); pluralize = require("pluralize");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

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

@ -12,7 +12,6 @@
const fs = require("fs"), const fs = require("fs"),
path = require("path"), path = require("path"),
ignore = require("ignore"), ignore = require("ignore"),
shell = require("shelljs"),
pathUtil = require("./util/path-util"); pathUtil = require("./util/path-util");
const debug = require("debug")("eslint:ignored-paths"); const debug = require("debug")("eslint:ignored-paths");
@ -54,7 +53,7 @@ function findIgnoreFile(cwd) {
const ignoreFilePath = path.resolve(cwd, ESLINT_IGNORE_FILENAME); const ignoreFilePath = path.resolve(cwd, ESLINT_IGNORE_FILENAME);
return shell.test("-f", ignoreFilePath) ? ignoreFilePath : ""; return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
} }
/** /**
@ -179,7 +178,7 @@ class IgnoredPaths {
let result = false; let result = false;
const absolutePath = path.resolve(this.options.cwd, filepath); const absolutePath = path.resolve(this.options.cwd, filepath);
const relativePath = pathUtil.getRelativePath(absolutePath, this.options.cwd); const relativePath = pathUtil.getRelativePath(absolutePath, this.baseDir);
if ((typeof category === "undefined") || (category === "default")) { if ((typeof category === "undefined") || (category === "default")) {
result = result || (this.ig.default.filter([relativePath]).length === 0); result = result || (this.ig.default.filter([relativePath]).length === 0);
@ -213,15 +212,7 @@ class IgnoredPaths {
const filter = ig.createFilter(); const filter = ig.createFilter();
/** const base = this.baseDir;
* TODO
* 1.
* Actually, it should be `this.options.baseDir`, which is the base dir of `ignore-path`,
* as well as Line 177.
* But doing this leads to a breaking change and fails tests.
* Related to #6759
*/
const base = this.options.cwd;
return function(absolutePath) { return function(absolutePath) {
const relative = pathUtil.getRelativePath(absolutePath, base); const relative = pathUtil.getRelativePath(absolutePath, base);

557
tools/eslint/lib/eslint.js → tools/eslint/lib/linter.js

@ -1,6 +1,6 @@
/** /**
* @fileoverview Main ESLint object. * @fileoverview Main Linter Class
* @author Nicholas C. Zakas * @author Gyandeep Singh
*/ */
"use strict"; "use strict";
@ -11,22 +11,22 @@
const assert = require("assert"), const assert = require("assert"),
EventEmitter = require("events").EventEmitter, EventEmitter = require("events").EventEmitter,
escope = require("escope"), eslintScope = require("eslint-scope"),
levn = require("levn"), levn = require("levn"),
blankScriptAST = require("../conf/blank-script.json"), blankScriptAST = require("../conf/blank-script.json"),
DEFAULT_PARSER = require("../conf/eslint-recommended").parser, defaultConfig = require("../conf/default-config-options.js"),
replacements = require("../conf/replacements.json"), replacements = require("../conf/replacements.json"),
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
ConfigOps = require("./config/config-ops"), ConfigOps = require("./config/config-ops"),
validator = require("./config/config-validator"), validator = require("./config/config-validator"),
Environments = require("./config/environments"), Environments = require("./config/environments"),
CommentEventGenerator = require("./util/comment-event-generator"),
NodeEventGenerator = require("./util/node-event-generator"), NodeEventGenerator = require("./util/node-event-generator"),
SourceCode = require("./util/source-code"), SourceCode = require("./util/source-code"),
Traverser = require("./util/traverser"), Traverser = require("./util/traverser"),
RuleContext = require("./rule-context"), RuleContext = require("./rule-context"),
rules = require("./rules"), Rules = require("./rules"),
timing = require("./timing"), timing = require("./timing"),
astUtils = require("./ast-utils"),
pkg = require("../package.json"); pkg = require("../package.json");
@ -158,19 +158,20 @@ function parseListConfig(string) {
* @param {ASTNode} program The top node of the AST. * @param {ASTNode} program The top node of the AST.
* @param {Scope} globalScope The global scope. * @param {Scope} globalScope The global scope.
* @param {Object} config The existing configuration data. * @param {Object} config The existing configuration data.
* @param {Environments} envContext Env context
* @returns {void} * @returns {void}
*/ */
function addDeclaredGlobals(program, globalScope, config) { function addDeclaredGlobals(program, globalScope, config, envContext) {
const declaredGlobals = {}, const declaredGlobals = {},
exportedGlobals = {}, exportedGlobals = {},
explicitGlobals = {}, explicitGlobals = {},
builtin = Environments.get("builtin"); builtin = envContext.get("builtin");
Object.assign(declaredGlobals, builtin); Object.assign(declaredGlobals, builtin);
Object.keys(config.env).forEach(name => { Object.keys(config.env).forEach(name => {
if (config.env[name]) { if (config.env[name]) {
const env = Environments.get(name), const env = envContext.get(name),
environmentGlobals = env && env.globals; environmentGlobals = env && env.globals;
if (environmentGlobals) { if (environmentGlobals) {
@ -187,7 +188,7 @@ function addDeclaredGlobals(program, globalScope, config) {
let variable = globalScope.set.get(name); let variable = globalScope.set.get(name);
if (!variable) { if (!variable) {
variable = new escope.Variable(name, globalScope); variable = new eslintScope.Variable(name, globalScope);
variable.eslintExplicitGlobal = false; variable.eslintExplicitGlobal = false;
globalScope.variables.push(variable); globalScope.variables.push(variable);
globalScope.set.set(name, variable); globalScope.set.set(name, variable);
@ -199,7 +200,7 @@ function addDeclaredGlobals(program, globalScope, config) {
let variable = globalScope.set.get(name); let variable = globalScope.set.get(name);
if (!variable) { if (!variable) {
variable = new escope.Variable(name, globalScope); variable = new eslintScope.Variable(name, globalScope);
variable.eslintExplicitGlobal = true; variable.eslintExplicitGlobal = true;
variable.eslintExplicitGlobalComment = explicitGlobals[name].comment; variable.eslintExplicitGlobalComment = explicitGlobals[name].comment;
globalScope.variables.push(variable); globalScope.variables.push(variable);
@ -314,11 +315,10 @@ function enableReporting(reportingConfig, start, rulesToEnable) {
* @param {string} filename The file being checked. * @param {string} filename The file being checked.
* @param {ASTNode} ast The top node of the AST. * @param {ASTNode} ast The top node of the AST.
* @param {Object} config The existing configuration data. * @param {Object} config The existing configuration data.
* @param {Object[]} reportingConfig The existing reporting configuration data. * @param {Linter} linterContext Linter context object
* @param {Object[]} messages The messages queue.
* @returns {Object} Modified config object * @returns {Object} Modified config object
*/ */
function modifyConfigsFromComments(filename, ast, config, reportingConfig, messages) { function modifyConfigsFromComments(filename, ast, config, linterContext) {
let commentConfig = { let commentConfig = {
exported: {}, exported: {},
@ -327,6 +327,8 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
env: {} env: {}
}; };
const commentRules = {}; const commentRules = {};
const messages = linterContext.messages;
const reportingConfig = linterContext.reportingConfig;
ast.comments.forEach(comment => { ast.comments.forEach(comment => {
@ -365,7 +367,7 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
Object.keys(items).forEach(name => { Object.keys(items).forEach(name => {
const ruleValue = items[name]; const ruleValue = items[name];
validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`); validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
commentRules[name] = ruleValue; commentRules[name] = ruleValue;
}); });
break; break;
@ -373,7 +375,7 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
// no default // no default
} }
} else { // comment.type === "Line" } else { // comment.type === "Line"
if (match[1] === "eslint-disable-line") { if (match[1] === "eslint-disable-line") {
disableReporting(reportingConfig, { line: comment.loc.start.line, column: 0 }, Object.keys(parseListConfig(value))); disableReporting(reportingConfig, { line: comment.loc.start.line, column: 0 }, Object.keys(parseListConfig(value)));
enableReporting(reportingConfig, comment.loc.end, Object.keys(parseListConfig(value))); enableReporting(reportingConfig, comment.loc.end, Object.keys(parseListConfig(value)));
@ -387,7 +389,7 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
// apply environment configs // apply environment configs
Object.keys(commentConfig.env).forEach(name => { Object.keys(commentConfig.env).forEach(name => {
const env = Environments.get(name); const env = linterContext.environments.get(name);
if (env) { if (env) {
commentConfig = ConfigOps.merge(commentConfig, env); commentConfig = ConfigOps.merge(commentConfig, env);
@ -446,15 +448,13 @@ function normalizeEcmaVersion(ecmaVersion, isModule) {
/** /**
* Process initial config to make it safe to extend by file comment config * Process initial config to make it safe to extend by file comment config
* @param {Object} config Initial config * @param {Object} config Initial config
* @param {Environments} envContext Env context
* @returns {Object} Processed config * @returns {Object} Processed config
*/ */
function prepareConfig(config) { function prepareConfig(config, envContext) {
config.globals = config.globals || {};
config.globals = config.globals || config.global || {}; const copiedRules = Object.assign({}, defaultConfig.rules);
delete config.global; let parserOptions = Object.assign({}, defaultConfig.parserOptions);
const copiedRules = {};
let parserOptions = {};
if (typeof config.rules === "object") { if (typeof config.rules === "object") {
Object.keys(config.rules).forEach(k => { Object.keys(config.rules).forEach(k => {
@ -474,7 +474,7 @@ function prepareConfig(config) {
// merge in environment parserOptions // merge in environment parserOptions
if (typeof config.env === "object") { if (typeof config.env === "object") {
Object.keys(config.env).forEach(envName => { Object.keys(config.env).forEach(envName => {
const env = Environments.get(envName); const env = envContext.get(envName);
if (config.env[envName] && env && env.parserOptions) { if (config.env[envName] && env && env.parserOptions) {
parserOptions = ConfigOps.merge(parserOptions, env.parserOptions); parserOptions = ConfigOps.merge(parserOptions, env.parserOptions);
@ -484,21 +484,18 @@ function prepareConfig(config) {
const preparedConfig = { const preparedConfig = {
rules: copiedRules, rules: copiedRules,
parser: config.parser || DEFAULT_PARSER, parser: config.parser || defaultConfig.parser,
globals: ConfigOps.merge({}, config.globals), globals: ConfigOps.merge(defaultConfig.globals, config.globals),
env: ConfigOps.merge({}, config.env || {}), env: ConfigOps.merge(defaultConfig.env, config.env || {}),
settings: ConfigOps.merge({}, config.settings || {}), settings: ConfigOps.merge(defaultConfig.settings, config.settings || {}),
parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {}) parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {})
}; };
const isModule = preparedConfig.parserOptions.sourceType === "module"; const isModule = preparedConfig.parserOptions.sourceType === "module";
if (isModule) { if (isModule) {
if (!preparedConfig.parserOptions.ecmaFeatures) {
preparedConfig.parserOptions.ecmaFeatures = {};
}
// can't have global return inside of modules // can't have global return inside of modules
preparedConfig.parserOptions.ecmaFeatures.globalReturn = false; preparedConfig.parserOptions.ecmaFeatures = Object.assign({}, preparedConfig.parserOptions.ecmaFeatures, { globalReturn: false });
} }
preparedConfig.parserOptions.ecmaVersion = normalizeEcmaVersion(preparedConfig.parserOptions.ecmaVersion, isModule); preparedConfig.parserOptions.ecmaVersion = normalizeEcmaVersion(preparedConfig.parserOptions.ecmaVersion, isModule);
@ -586,150 +583,157 @@ function stripUnicodeBOM(text) {
return text; return text;
} }
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/** /**
* Object that is responsible for verifying JavaScript text * Get the severity level of a rule (0 - none, 1 - warning, 2 - error)
* @name eslint * Returns 0 if the rule config is not valid (an Array or a number)
* @param {Array|number} ruleConfig rule configuration
* @returns {number} 0, 1, or 2, indicating rule severity
*/ */
module.exports = (function() { function getRuleSeverity(ruleConfig) {
if (typeof ruleConfig === "number") {
const api = Object.create(new EventEmitter()); return ruleConfig;
let messages = [], } else if (Array.isArray(ruleConfig)) {
currentConfig = null, return ruleConfig[0];
currentScopes = null, }
scopeManager = null, return 0;
currentFilename = null,
traverser = null,
reportingConfig = [],
sourceCode = null;
/**
* Parses text into an AST. Moved out here because the try-catch prevents
* optimization of functions, so it's best to keep the try-catch as isolated
* as possible
* @param {string} text The text to parse.
* @param {Object} config The ESLint configuration object.
* @param {string} filePath The path to the file being parsed.
* @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
* or null if not.
* @private
*/
function parse(text, config, filePath) {
let parser,
parserOptions = {
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
attachComment: true,
filePath
};
try {
parser = require(config.parser);
} catch (ex) {
messages.push({
ruleId: null,
fatal: true,
severity: 2,
source: null,
message: ex.message,
line: 0,
column: 0
});
return null;
}
// merge in any additional parser options }
if (config.parserOptions) {
parserOptions = Object.assign({}, config.parserOptions, parserOptions);
}
/* /**
* Check for parsing errors first. If there's a parsing error, nothing * Get the options for a rule (not including severity), if any
* else can happen. However, a parsing error does not throw an error * @param {Array|number} ruleConfig rule configuration
* from this method - it's just considered a fatal error message, a * @returns {Array} of rule options, empty Array if none
* problem that ESLint identified just like any other. */
*/ function getRuleOptions(ruleConfig) {
try { if (Array.isArray(ruleConfig)) {
if (typeof parser.parseForESLint === "function") { return ruleConfig.slice(1);
return parser.parseForESLint(text, parserOptions); }
} return [];
return parser.parse(text, parserOptions);
} catch (ex) { }
// If the message includes a leading line number, strip it: /**
const message = ex.message.replace(/^line \d+:/i, "").trim(); * Parses text into an AST. Moved out here because the try-catch prevents
const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null; * optimization of functions, so it's best to keep the try-catch as isolated
* as possible
* @param {string} text The text to parse.
* @param {Object} config The ESLint configuration object.
* @param {string} filePath The path to the file being parsed.
* @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
* or null if not.
* @param {Array<Object>} messages Messages array for the linter object
* @returns {*} parsed text if successful otherwise null
* @private
*/
function parse(text, config, filePath, messages) {
let parser,
parserOptions = {
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
filePath
};
messages.push({ try {
ruleId: null, parser = require(config.parser);
fatal: true, } catch (ex) {
severity: 2, messages.push({
source, ruleId: null,
message: `Parsing error: ${message}`, fatal: true,
severity: 2,
source: null,
message: ex.message,
line: 0,
column: 0
});
line: ex.lineNumber, return null;
column: ex.column }
});
return null; // merge in any additional parser options
} if (config.parserOptions) {
parserOptions = Object.assign({}, config.parserOptions, parserOptions);
} }
/** /*
* Get the severity level of a rule (0 - none, 1 - warning, 2 - error) * Check for parsing errors first. If there's a parsing error, nothing
* Returns 0 if the rule config is not valid (an Array or a number) * else can happen. However, a parsing error does not throw an error
* @param {Array|number} ruleConfig rule configuration * from this method - it's just considered a fatal error message, a
* @returns {number} 0, 1, or 2, indicating rule severity * problem that ESLint identified just like any other.
*/ */
function getRuleSeverity(ruleConfig) { try {
if (typeof ruleConfig === "number") { if (typeof parser.parseForESLint === "function") {
return ruleConfig; return parser.parseForESLint(text, parserOptions);
} else if (Array.isArray(ruleConfig)) {
return ruleConfig[0];
} }
return 0; return parser.parse(text, parserOptions);
} } catch (ex) {
/** // If the message includes a leading line number, strip it:
* Get the options for a rule (not including severity), if any const message = ex.message.replace(/^line \d+:/i, "").trim();
* @param {Array|number} ruleConfig rule configuration const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
* @returns {Array} of rule options, empty Array if none
*/
function getRuleOptions(ruleConfig) {
if (Array.isArray(ruleConfig)) {
return ruleConfig.slice(1);
}
return [];
messages.push({
ruleId: null,
fatal: true,
severity: 2,
source,
message: `Parsing error: ${message}`,
line: ex.lineNumber,
column: ex.column
});
return null;
} }
}
// set unlimited listeners (see https://github.com/eslint/eslint/issues/524) //------------------------------------------------------------------------------
api.setMaxListeners(0); // Public Interface
//------------------------------------------------------------------------------
/**
* Object that is responsible for verifying JavaScript text
* @name eslint
*/
class Linter extends EventEmitter {
constructor() {
super();
this.messages = [];
this.currentConfig = null;
this.currentScopes = null;
this.scopeManager = null;
this.currentFilename = null;
this.traverser = null;
this.reportingConfig = [];
this.sourceCode = null;
this.version = pkg.version;
this.rules = new Rules();
this.environments = new Environments();
// set unlimited listeners (see https://github.com/eslint/eslint/issues/524)
this.setMaxListeners(0);
}
/** /**
* Resets the internal state of the object. * Resets the internal state of the object.
* @returns {void} * @returns {void}
*/ */
api.reset = function() { reset() {
this.removeAllListeners(); this.removeAllListeners();
messages = []; this.messages = [];
currentConfig = null; this.currentConfig = null;
currentScopes = null; this.currentScopes = null;
scopeManager = null; this.scopeManager = null;
traverser = null; this.traverser = null;
reportingConfig = []; this.reportingConfig = [];
sourceCode = null; this.sourceCode = null;
}; }
/** /**
* Configuration object for the `verify` API. A JS representation of the eslintrc files. * Configuration object for the `verify` API. A JS representation of the eslintrc files.
@ -739,7 +743,7 @@ module.exports = (function() {
* @property {Object} [parserOptions] Options for the parsed used. * @property {Object} [parserOptions] Options for the parsed used.
* @property {Object} [settings] Global settings passed to each rule. * @property {Object} [settings] Global settings passed to each rule.
* @property {Object} [env] The environment to verify in. * @property {Object} [env] The environment to verify in.
* @property {Object} [globals] Available globalsto the code. * @property {Object} [globals] Available globals to the code.
*/ */
/** /**
@ -755,20 +759,19 @@ module.exports = (function() {
* Useful if you want to validate JS without comments overriding rules. * Useful if you want to validate JS without comments overriding rules.
* @returns {Object[]} The results as an array of messages or null if no messages. * @returns {Object[]} The results as an array of messages or null if no messages.
*/ */
api.verify = function(textOrSourceCode, config, filenameOrOptions, saveState) { verify(textOrSourceCode, config, filenameOrOptions, saveState) {
const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null; const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null;
let ast, let ast,
parseResult, parseResult,
shebang,
allowInlineConfig; allowInlineConfig;
// evaluate arguments // evaluate arguments
if (typeof filenameOrOptions === "object") { if (typeof filenameOrOptions === "object") {
currentFilename = filenameOrOptions.filename; this.currentFilename = filenameOrOptions.filename;
allowInlineConfig = filenameOrOptions.allowInlineConfig; allowInlineConfig = filenameOrOptions.allowInlineConfig;
saveState = filenameOrOptions.saveState; saveState = filenameOrOptions.saveState;
} else { } else {
currentFilename = filenameOrOptions; this.currentFilename = filenameOrOptions;
} }
if (!saveState) { if (!saveState) {
@ -789,24 +792,22 @@ module.exports = (function() {
} }
// process initial config to make it safe to extend // process initial config to make it safe to extend
config = prepareConfig(config); config = prepareConfig(config, this.environments);
// 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); this.sourceCode = new SourceCode(text, blankScriptAST);
return messages; return this.messages;
} }
parseResult = parse( parseResult = parse(
stripUnicodeBOM(text).replace(/^#!([^\r\n]+)/, (match, captured) => { stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`),
shebang = captured;
return `//${captured}`;
}),
config, config,
currentFilename this.currentFilename,
this.messages
); );
// if this result is from a parseForESLint() method, normalize // if this result is from a parseForESLint() method, normalize
@ -818,12 +819,12 @@ module.exports = (function() {
} }
if (ast) { if (ast) {
sourceCode = new SourceCode(text, ast); this.sourceCode = new SourceCode(text, ast);
} }
} else { } else {
sourceCode = textOrSourceCode; this.sourceCode = textOrSourceCode;
ast = sourceCode.ast; ast = this.sourceCode.ast;
} }
// if espree failed to parse the file, there's no sense in setting up rules // if espree failed to parse the file, there's no sense in setting up rules
@ -831,7 +832,7 @@ module.exports = (function() {
// parse global comments and modify config // parse global comments and modify config
if (allowInlineConfig !== false) { if (allowInlineConfig !== false) {
config = modifyConfigsFromComments(currentFilename, ast, config, reportingConfig, messages); config = modifyConfigsFromComments(this.currentFilename, ast, config, this);
} }
// ensure that severities are normalized in the config // ensure that severities are normalized in the config
@ -841,7 +842,7 @@ module.exports = (function() {
Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => { Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
let ruleCreator; let ruleCreator;
ruleCreator = rules.get(key); ruleCreator = this.rules.get(key);
if (!ruleCreator) { if (!ruleCreator) {
const replacementMsg = getRuleReplacementMessage(key); const replacementMsg = getRuleReplacementMessage(key);
@ -851,7 +852,7 @@ module.exports = (function() {
} else { } else {
ruleCreator = createStubRule(`Definition for rule '${key}' was not found`); ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
} }
rules.define(key, ruleCreator); this.rules.define(key, ruleCreator);
} }
const severity = getRuleSeverity(config.rules[key]); const severity = getRuleSeverity(config.rules[key]);
@ -859,7 +860,7 @@ module.exports = (function() {
try { try {
const ruleContext = new RuleContext( const ruleContext = new RuleContext(
key, api, severity, options, key, this, severity, options,
config.settings, config.parserOptions, config.parser, config.settings, config.parserOptions, config.parser,
ruleCreator.meta, ruleCreator.meta,
(parseResult && parseResult.services ? parseResult.services : {}) (parseResult && parseResult.services ? parseResult.services : {})
@ -870,7 +871,7 @@ module.exports = (function() {
// add all the selectors from the rule as listeners // add all the selectors from the rule as listeners
Object.keys(rule).forEach(selector => { Object.keys(rule).forEach(selector => {
api.on(selector, timing.enabled this.on(selector, timing.enabled
? timing.time(key, rule[selector]) ? timing.time(key, rule[selector])
: rule[selector] : rule[selector]
); );
@ -882,40 +883,30 @@ module.exports = (function() {
}); });
// save config so rules can access as necessary // save config so rules can access as necessary
currentConfig = config; this.currentConfig = config;
traverser = new Traverser(); this.traverser = new Traverser();
const ecmaFeatures = currentConfig.parserOptions.ecmaFeatures || {}; const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
const ecmaVersion = currentConfig.parserOptions.ecmaVersion || 5; const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
// gather scope data that may be needed by the rules // gather scope data that may be needed by the rules
scopeManager = escope.analyze(ast, { this.scopeManager = eslintScope.analyze(ast, {
ignoreEval: true, ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn, nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict, impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion, ecmaVersion,
sourceType: currentConfig.parserOptions.sourceType || "script", sourceType: this.currentConfig.parserOptions.sourceType || "script",
fallback: Traverser.getKeys fallback: Traverser.getKeys
}); });
currentScopes = scopeManager.scopes; this.currentScopes = this.scopeManager.scopes;
// augment global scope with declared global variables // augment global scope with declared global variables
addDeclaredGlobals(ast, currentScopes[0], currentConfig); addDeclaredGlobals(ast, this.currentScopes[0], this.currentConfig, this.environments);
// remove shebang comments let eventGenerator = new NodeEventGenerator(this);
if (shebang && ast.comments.length && ast.comments[0].value === shebang) {
ast.comments.splice(0, 1);
if (ast.body.length && ast.body[0].leadingComments && ast.body[0].leadingComments[0].value === shebang) {
ast.body[0].leadingComments.splice(0, 1);
}
}
let eventGenerator = new NodeEventGenerator(api);
eventGenerator = new CodePathAnalyzer(eventGenerator); eventGenerator = new CodePathAnalyzer(eventGenerator);
eventGenerator = new CommentEventGenerator(eventGenerator, sourceCode);
/* /*
* Each node has a type property. Whenever a particular type of * Each node has a type property. Whenever a particular type of
@ -923,7 +914,7 @@ module.exports = (function() {
* automatically be informed that this type of node has been found * automatically be informed that this type of node has been found
* and react accordingly. * and react accordingly.
*/ */
traverser.traverse(ast, { this.traverser.traverse(ast, {
enter(node, parent) { enter(node, parent) {
node.parent = parent; node.parent = parent;
eventGenerator.enterNode(node); eventGenerator.enterNode(node);
@ -935,7 +926,7 @@ module.exports = (function() {
} }
// sort by line and column // sort by line and column
messages.sort((a, b) => { this.messages.sort((a, b) => {
const lineDiff = a.line - b.line; const lineDiff = a.line - b.line;
if (lineDiff === 0) { if (lineDiff === 0) {
@ -945,8 +936,8 @@ module.exports = (function() {
}); });
return messages; return this.messages;
}; }
/** /**
* Reports a message from one of the rules. * Reports a message from one of the rules.
@ -963,11 +954,13 @@ module.exports = (function() {
* @param {Object} meta Metadata of the rule * @param {Object} meta Metadata of the rule
* @returns {void} * @returns {void}
*/ */
api.report = function(ruleId, severity, node, location, message, opts, fix, meta) { report(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");
} }
let endLocation;
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");
@ -976,14 +969,14 @@ module.exports = (function() {
opts = message; opts = message;
message = location; message = location;
location = node.loc.start; location = node.loc.start;
endLocation = node.loc.end;
} else {
endLocation = location.end;
} }
// Store end location.
const endLocation = location.end;
location = location.start || location; location = location.start || location;
if (isDisabledByReportingConfig(reportingConfig, ruleId, location)) { if (isDisabledByReportingConfig(this.reportingConfig, ruleId, location)) {
return; return;
} }
@ -1003,15 +996,15 @@ module.exports = (function() {
severity, severity,
message, message,
line: location.line, line: location.line,
column: location.column + 1, // switch to 1-base instead of 0-base column: location.column + 1, // switch to 1-base instead of 0-base
nodeType: node && node.type, nodeType: node && node.type,
source: sourceCode.lines[location.line - 1] || "" source: this.sourceCode.lines[location.line - 1] || ""
}; };
// Define endLine and endColumn if exists. // Define endLine and endColumn if exists.
if (endLocation) { if (endLocation) {
problem.endLine = endLocation.line; problem.endLine = endLocation.line;
problem.endColumn = endLocation.column + 1; // switch to 1-base instead of 0-base problem.endColumn = endLocation.column + 1; // switch to 1-base instead of 0-base
} }
// ensure there's range and text properties, otherwise it's not a valid fix // ensure there's range and text properties, otherwise it's not a valid fix
@ -1025,73 +1018,39 @@ module.exports = (function() {
problem.fix = fix; problem.fix = fix;
} }
messages.push(problem); this.messages.push(problem);
}; }
/** /**
* Gets the SourceCode object representing the parsed source. * Gets the SourceCode object representing the parsed source.
* @returns {SourceCode} The SourceCode object. * @returns {SourceCode} The SourceCode object.
*/ */
api.getSourceCode = function() { getSourceCode() {
return sourceCode; return this.sourceCode;
}; }
// methods that exist on SourceCode object
const externalMethods = {
getSource: "getText",
getSourceLines: "getLines",
getAllComments: "getAllComments",
getNodeByRangeIndex: "getNodeByRangeIndex",
getComments: "getComments",
getJSDocComment: "getJSDocComment",
getFirstToken: "getFirstToken",
getFirstTokens: "getFirstTokens",
getLastToken: "getLastToken",
getLastTokens: "getLastTokens",
getTokenAfter: "getTokenAfter",
getTokenBefore: "getTokenBefore",
getTokenByRangeStart: "getTokenByRangeStart",
getTokens: "getTokens",
getTokensAfter: "getTokensAfter",
getTokensBefore: "getTokensBefore",
getTokensBetween: "getTokensBetween"
};
// copy over methods
Object.keys(externalMethods).forEach(methodName => {
const exMethodName = externalMethods[methodName];
// All functions expected to have less arguments than 5.
api[methodName] = function(a, b, c, d, e) {
if (sourceCode) {
return sourceCode[exMethodName](a, b, c, d, e);
}
return null;
};
});
/** /**
* Gets nodes that are ancestors of current node. * Gets nodes that are ancestors of current node.
* @returns {ASTNode[]} Array of objects representing ancestors. * @returns {ASTNode[]} Array of objects representing ancestors.
*/ */
api.getAncestors = function() { getAncestors() {
return traverser.parents(); return this.traverser.parents();
}; }
/** /**
* Gets the scope for the current node. * Gets the scope for the current node.
* @returns {Object} An object representing the current node's scope. * @returns {Object} An object representing the current node's scope.
*/ */
api.getScope = function() { getScope() {
const parents = traverser.parents(); const parents = this.traverser.parents();
// Don't do this for Program nodes - they have no parents // Don't do this for Program nodes - they have no parents
if (parents.length) { if (parents.length) {
// if current node introduces a scope, add it to the list // if current node introduces a scope, add it to the list
const current = traverser.current(); const current = this.traverser.current();
if (currentConfig.parserOptions.ecmaVersion >= 6) { if (this.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);
} }
@ -1105,7 +1064,7 @@ module.exports = (function() {
for (let i = parents.length - 1; i >= 0; --i) { for (let i = parents.length - 1; i >= 0; --i) {
// Get the innermost scope // Get the innermost scope
const scope = scopeManager.acquire(parents[i], true); const scope = this.scopeManager.acquire(parents[i], true);
if (scope) { if (scope) {
if (scope.type === "function-expression-name") { if (scope.type === "function-expression-name") {
@ -1119,8 +1078,8 @@ module.exports = (function() {
} }
return currentScopes[0]; return this.currentScopes[0];
}; }
/** /**
* Record that a particular variable has been used in code * Record that a particular variable has been used in code
@ -1128,9 +1087,9 @@ module.exports = (function() {
* @returns {boolean} True if the variable was found and marked as used, * @returns {boolean} True if the variable was found and marked as used,
* false if not. * false if not.
*/ */
api.markVariableAsUsed = function(name) { markVariableAsUsed(name) {
const hasGlobalReturn = currentConfig.parserOptions.ecmaFeatures && currentConfig.parserOptions.ecmaFeatures.globalReturn, const hasGlobalReturn = this.currentConfig.parserOptions.ecmaFeatures && this.currentConfig.parserOptions.ecmaFeatures.globalReturn,
specialScope = hasGlobalReturn || currentConfig.parserOptions.sourceType === "module"; specialScope = hasGlobalReturn || this.currentConfig.parserOptions.sourceType === "module";
let scope = this.getScope(), let scope = this.getScope(),
i, i,
len; len;
@ -1152,20 +1111,20 @@ module.exports = (function() {
} while ((scope = scope.upper)); } while ((scope = scope.upper));
return false; return false;
}; }
/** /**
* Gets the filename for the currently parsed source. * Gets the filename for the currently parsed source.
* @returns {string} The filename associated with the source being parsed. * @returns {string} The filename associated with the source being parsed.
* Defaults to "<input>" if no filename info is present. * Defaults to "<input>" if no filename info is present.
*/ */
api.getFilename = function() { getFilename() {
if (typeof currentFilename === "string") { if (typeof this.currentFilename === "string") {
return currentFilename; return this.currentFilename;
} }
return "<input>"; return "<input>";
}; }
/** /**
* Defines a new linting rule. * Defines a new linting rule.
@ -1173,38 +1132,36 @@ module.exports = (function() {
* @param {Function} ruleModule Function from context to object mapping AST node types to event handlers * @param {Function} ruleModule Function from context to object mapping AST node types to event handlers
* @returns {void} * @returns {void}
*/ */
const defineRule = api.defineRule = function(ruleId, ruleModule) { defineRule(ruleId, ruleModule) {
rules.define(ruleId, ruleModule); this.rules.define(ruleId, ruleModule);
}; }
/** /**
* Defines many new linting rules. * Defines many new linting rules.
* @param {Object} rulesToDefine map from unique rule identifier to rule * @param {Object} rulesToDefine map from unique rule identifier to rule
* @returns {void} * @returns {void}
*/ */
api.defineRules = function(rulesToDefine) { defineRules(rulesToDefine) {
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => { Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
defineRule(ruleId, rulesToDefine[ruleId]); this.defineRule(ruleId, rulesToDefine[ruleId]);
}); });
}; }
/** /**
* Gets the default eslint configuration. * Gets the default eslint configuration.
* @returns {Object} Object mapping rule IDs to their default configurations * @returns {Object} Object mapping rule IDs to their default configurations
*/ */
api.defaults = function() { defaults() { // eslint-disable-line class-methods-use-this
return require("../conf/eslint-recommended"); return defaultConfig;
}; }
/** /**
* Gets an object with all loaded rules. * Gets an object with all loaded rules.
* @returns {Map} All loaded rules * @returns {Map} All loaded rules
*/ */
api.getRules = function() { getRules() {
return rules.getAllLoadedRules(); return this.rules.getAllLoadedRules();
}; }
api.version = pkg.version;
/** /**
* Gets variables that are declared by a specified node. * Gets variables that are declared by a specified node.
@ -1223,12 +1180,48 @@ module.exports = (function() {
* - others - always an empty array. * - others - always an empty array.
* *
* @param {ASTNode} node A node to get. * @param {ASTNode} node A node to get.
* @returns {escope.Variable[]} Variables that are declared by the node. * @returns {eslint-scope.Variable[]} Variables that are declared by the node.
*/ */
api.getDeclaredVariables = function(node) { getDeclaredVariables(node) {
return (scopeManager && scopeManager.getDeclaredVariables(node)) || []; return (this.scopeManager && this.scopeManager.getDeclaredVariables(node)) || [];
}; }
}
return api; // methods that exist on SourceCode object
const externalMethods = {
getSource: "getText",
getSourceLines: "getLines",
getAllComments: "getAllComments",
getNodeByRangeIndex: "getNodeByRangeIndex",
getComments: "getComments",
getCommentsBefore: "getCommentsBefore",
getCommentsAfter: "getCommentsAfter",
getCommentsInside: "getCommentsInside",
getJSDocComment: "getJSDocComment",
getFirstToken: "getFirstToken",
getFirstTokens: "getFirstTokens",
getLastToken: "getLastToken",
getLastTokens: "getLastTokens",
getTokenAfter: "getTokenAfter",
getTokenBefore: "getTokenBefore",
getTokenByRangeStart: "getTokenByRangeStart",
getTokens: "getTokens",
getTokensAfter: "getTokensAfter",
getTokensBefore: "getTokensBefore",
getTokensBetween: "getTokensBetween"
};
// copy over methods
Object.keys(externalMethods).forEach(methodName => {
const exMethodName = externalMethods[methodName];
// All functions expected to have less arguments than 5.
Linter.prototype[methodName] = function(a, b, c, d, e) {
if (this.sourceCode) {
return this.sourceCode[exMethodName](a, b, c, d, e);
}
return null;
};
});
}()); module.exports = Linter;

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

@ -12,6 +12,8 @@
const fs = require("fs"), const fs = require("fs"),
path = require("path"); path = require("path");
const rulesDirCache = {};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -29,6 +31,11 @@ module.exports = function(rulesDir, cwd) {
rulesDir = path.resolve(cwd, rulesDir); rulesDir = path.resolve(cwd, rulesDir);
} }
// cache will help performance as IO operation are expensive
if (rulesDirCache[rulesDir]) {
return rulesDirCache[rulesDir];
}
const rules = Object.create(null); const rules = Object.create(null);
fs.readdirSync(rulesDir).forEach(file => { fs.readdirSync(rulesDir).forEach(file => {
@ -37,5 +44,7 @@ module.exports = function(rulesDir, cwd) {
} }
rules[file.slice(0, -3)] = path.join(rulesDir, file); rules[file.slice(0, -3)] = path.join(rulesDir, file);
}); });
rulesDirCache[rulesDir] = rules;
return rules; return rules;
}; };

156
tools/eslint/lib/rules.js

@ -11,115 +11,85 @@
const loadRules = require("./load-rules"); const loadRules = require("./load-rules");
//------------------------------------------------------------------------------
// Privates
//------------------------------------------------------------------------------
let rules = Object.create(null);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Public Interface // Public Interface
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** class Rules {
* Registers a rule module for rule id in storage. constructor() {
* @param {string} ruleId Rule id (file name). this._rules = Object.create(null);
* @param {Function} ruleModule Rule handler.
* @returns {void}
*/
function define(ruleId, ruleModule) {
rules[ruleId] = ruleModule;
}
/** this.load();
* 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} cwd Current working directory
* @returns {void}
*/
function load(rulesDir, cwd) {
const newRules = loadRules(rulesDir, cwd);
Object.keys(newRules).forEach(ruleId => { /**
define(ruleId, newRules[ruleId]); * Registers a rule module for rule id in storage.
}); * @param {string} ruleId Rule id (file name).
} * @param {Function} ruleModule Rule handler.
* @returns {void}
*/
define(ruleId, ruleModule) {
this._rules[ruleId] = ruleModule;
}
/** /**
* Registers all given rules of a plugin. * Loads and registers all rules from passed rules directory.
* @param {Object} plugin The plugin object to import. * @param {string} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`.
* @param {string} pluginName The name of the plugin without prefix (`eslint-plugin-`). * @param {string} cwd Current working directory
* @returns {void} * @returns {void}
*/ */
function importPlugin(plugin, pluginName) { load(rulesDir, cwd) {
if (plugin.rules) { const newRules = loadRules(rulesDir, cwd);
Object.keys(plugin.rules).forEach(ruleId => {
const qualifiedRuleId = `${pluginName}/${ruleId}`,
rule = plugin.rules[ruleId];
define(qualifiedRuleId, rule); Object.keys(newRules).forEach(ruleId => {
this.define(ruleId, newRules[ruleId]);
}); });
} }
}
/** /**
* Access rule handler by id (file name). * Registers all given rules of a plugin.
* @param {string} ruleId Rule id (file name). * @param {Object} plugin The plugin object to import.
* @returns {Function} Rule handler. * @param {string} pluginName The name of the plugin without prefix (`eslint-plugin-`).
*/ * @returns {void}
function getHandler(ruleId) { */
if (typeof rules[ruleId] === "string") { importPlugin(plugin, pluginName) {
return require(rules[ruleId]); if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleId => {
const qualifiedRuleId = `${pluginName}/${ruleId}`,
rule = plugin.rules[ruleId];
this.define(qualifiedRuleId, rule);
});
}
} }
return rules[ruleId];
}
/**
* Get an object with all currently loaded rules
* @returns {Map} All loaded rules
*/
function getAllLoadedRules() {
const allRules = new Map();
Object.keys(rules).forEach(name => {
const rule = getHandler(name);
allRules.set(name, rule);
});
return allRules;
}
/** /**
* Reset rules storage. * Access rule handler by id (file name).
* Should be used only in tests. * @param {string} ruleId Rule id (file name).
* @returns {void} * @returns {Function} Rule handler.
*/ */
function testClear() { get(ruleId) {
rules = Object.create(null); if (typeof this._rules[ruleId] === "string") {
} return require(this._rules[ruleId]);
}
return this._rules[ruleId];
module.exports = { }
define,
load,
importPlugin,
get: getHandler,
getAllLoadedRules,
testClear,
/** /**
* Resets rules to its starting state. Use for tests only. * Get an object with all currently loaded rules
* @returns {void} * @returns {Map} All loaded rules
*/ */
testReset() { getAllLoadedRules() {
testClear(); const allRules = new Map();
load();
}
};
//------------------------------------------------------------------------------ Object.keys(this._rules).forEach(name => {
// Initialization const rule = this.get(name);
//------------------------------------------------------------------------------
allRules.set(name, rule);
});
return allRules;
}
}
// loads built-in rules module.exports = Rules;
load();

235
tools/eslint/lib/rules/array-bracket-newline.js

@ -0,0 +1,235 @@
/**
* @fileoverview Rule to enforce linebreaks after open and before close array brackets
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce linebreaks after opening and before closing array brackets",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
multiline: {
type: "boolean"
},
minItems: {
type: ["integer", "null"],
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Normalizes a given option value.
*
* @param {string|Object|undefined} option - An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(option) {
let multiline = false;
let minItems = 0;
if (option) {
if (option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
} else {
multiline = true;
minItems = Number.POSITIVE_INFINITY;
}
return { multiline, minItems };
}
/**
* Normalizes a given option value.
*
* @param {string|Object|undefined} options - An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
const value = normalizeOptionValue(options);
return { ArrayExpression: value, ArrayPattern: value };
}
/**
* Reports that there shouldn't be a linebreak 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 reportNoBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
message: "There should be no linebreak after '['.",
fix(fixer) {
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
if (astUtils.isCommentToken(nextToken)) {
return null;
}
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/**
* Reports that there shouldn't be a linebreak before the last 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 reportNoEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
message: "There should be no linebreak before ']'.",
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
if (astUtils.isCommentToken(previousToken)) {
return null;
}
return fixer.removeRange([previousToken.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a linebreak 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 reportRequiredBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
message: "A linebreak is required after '['.",
fix(fixer) {
return fixer.insertTextAfter(token, "\n");
}
});
}
/**
* Reports that there should be a linebreak before the last 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 reportRequiredEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
message: "A linebreak is required before ']'.",
fix(fixer) {
return fixer.insertTextBefore(token, "\n");
}
});
}
/**
* Reports a given node if it violated this rule.
*
* @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node.
* @param {{multiline: boolean, minItems: number}} options - An option object.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
const openBracket = sourceCode.getFirstToken(node);
const closeBracket = sourceCode.getLastToken(node);
const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
const first = sourceCode.getTokenAfter(openBracket);
const last = sourceCode.getTokenBefore(closeBracket);
const needsLinebreaks = (
elements.length >= options.minItems ||
(
options.multiline &&
elements.length > 0 &&
firstIncComment.loc.start.line !== lastIncComment.loc.end.line
)
);
/*
* Use tokens or comments to check multiline or not.
* But use only tokens to check whether linebreaks are needed.
* This allows:
* var arr = [ // eslint-disable-line foo
* 'a'
* ]
*/
if (needsLinebreaks) {
if (astUtils.isTokenOnSameLine(openBracket, first)) {
reportRequiredBeginningLinebreak(node, openBracket);
}
if (astUtils.isTokenOnSameLine(last, closeBracket)) {
reportRequiredEndingLinebreak(node, closeBracket);
}
} else {
if (!astUtils.isTokenOnSameLine(openBracket, first)) {
reportNoBeginningLinebreak(node, openBracket);
}
if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
reportNoEndingLinebreak(node, closeBracket);
}
}
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
ArrayPattern: check,
ArrayExpression: check
};
}
};

230
tools/eslint/lib/rules/array-element-newline.js

@ -0,0 +1,230 @@
/**
* @fileoverview Rule to enforce line breaks after each array element
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce line breaks after each array element",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
multiline: {
type: "boolean"
},
minItems: {
type: ["integer", "null"],
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Normalizes a given option value.
*
* @param {string|Object|undefined} option - An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(option) {
let multiline = false;
let minItems;
option = option || "always";
if (option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
return { multiline, minItems };
}
/**
* Normalizes a given option value.
*
* @param {string|Object|undefined} options - An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
const value = normalizeOptionValue(options);
return { ArrayExpression: value, ArrayPattern: value };
}
/**
* Reports that there shouldn't be a line break after the first token
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportNoLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start
},
message: "There should be no linebreak here.",
fix(fixer) {
if (astUtils.isCommentToken(tokenBefore)) {
return null;
}
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ");
}
/*
* This will check if the comma is on the same line as the next element
* Following array:
* [
* 1
* , 2
* , 3
* ]
*
* will be fixed to:
* [
* 1, 2, 3
* ]
*/
const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true });
if (astUtils.isCommentToken(twoTokensBefore)) {
return null;
}
return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], "");
}
});
}
/**
* Reports that there should be a line break after the first token
* @param {Token} token - The token to use for the report.
* @returns {void}
*/
function reportRequiredLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start
},
message: "There should be a linebreak after this element.",
fix(fixer) {
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n");
}
});
}
/**
* Reports a given node if it violated this rule.
*
* @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node.
* @param {{multiline: boolean, minItems: number}} options - An option object.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
let elementBreak = false;
/*
* MULTILINE: true
* loop through every element and check
* if at least one element has linebreaks inside
* this ensures that following is not valid (due to elements are on the same line):
*
* [
* 1,
* 2,
* 3
* ]
*/
if (options.multiline) {
elementBreak = elements
.filter(element => element !== null)
.some(element => element.loc.start.line !== element.loc.end.line);
}
const needsLinebreaks = (
elements.length >= options.minItems ||
(
options.multiline &&
elementBreak
)
);
elements.forEach((element, i) => {
const previousElement = elements[i - 1];
if (i === 0 || element === null || previousElement === null) {
return;
}
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
if (needsLinebreaks) {
if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
reportRequiredLineBreak(firstTokenOfCurrentElement);
}
} else {
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
reportNoLineBreak(firstTokenOfCurrentElement);
}
}
});
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
ArrayPattern: check,
ArrayExpression: check
};
}
};

55
tools/eslint/lib/rules/arrow-parens.js

@ -50,14 +50,31 @@ module.exports = {
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
/** /**
* Determines whether a arrow function argument end with `)` * Determines whether a arrow function argument end with `)`
* @param {ASTNode} node The arrow function node. * @param {ASTNode} node The arrow function node.
* @returns {void} * @returns {void}
*/ */
function parens(node) { function parens(node) {
const token = sourceCode.getFirstToken(node, node.async ? 1 : 0); const isAsync = node.async;
const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0);
/**
* Remove the parenthesis around a parameter
* @param {Fixer} fixer Fixer
* @returns {string} fixed parameter
*/
function fixParamsWithParenthesis(fixer) {
const paramToken = sourceCode.getTokenAfter(firstTokenOfParam);
const closingParenToken = sourceCode.getTokenAfter(paramToken);
const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null;
const shouldAddSpaceForAsync = asyncToken && (asyncToken.end === firstTokenOfParam.start);
return fixer.replaceTextRange([
firstTokenOfParam.range[0],
closingParenToken.range[1]
], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`);
}
// "as-needed", { "requireForBlockBody": true }: x => x // "as-needed", { "requireForBlockBody": true }: x => x
if ( if (
@ -68,19 +85,11 @@ module.exports = {
node.body.type !== "BlockStatement" && node.body.type !== "BlockStatement" &&
!node.returnType !node.returnType
) { ) {
if (astUtils.isOpeningParenToken(token)) { if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({ context.report({
node, node,
message: requireForBlockBodyMessage, message: requireForBlockBodyMessage,
fix(fixer) { fix: fixParamsWithParenthesis
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
}); });
} }
return; return;
@ -90,12 +99,12 @@ module.exports = {
requireForBlockBody && requireForBlockBody &&
node.body.type === "BlockStatement" node.body.type === "BlockStatement"
) { ) {
if (!astUtils.isOpeningParenToken(token)) { if (!astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({ context.report({
node, node,
message: requireForBlockBodyNoParensMessage, message: requireForBlockBodyNoParensMessage,
fix(fixer) { fix(fixer) {
return fixer.replaceText(token, `(${token.value})`); return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
} }
}); });
} }
@ -109,26 +118,18 @@ module.exports = {
!node.params[0].typeAnnotation && !node.params[0].typeAnnotation &&
!node.returnType !node.returnType
) { ) {
if (astUtils.isOpeningParenToken(token)) { if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({ context.report({
node, node,
message: asNeededMessage, message: asNeededMessage,
fix(fixer) { fix: fixParamsWithParenthesis
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
}); });
} }
return; return;
} }
if (token.type === "Identifier") { if (firstTokenOfParam.type === "Identifier") {
const after = sourceCode.getTokenAfter(token); const after = sourceCode.getTokenAfter(firstTokenOfParam);
// (x) => x // (x) => x
if (after.value !== ")") { if (after.value !== ")") {
@ -136,7 +137,7 @@ module.exports = {
node, node,
message, message,
fix(fixer) { fix(fixer) {
return fixer.replaceText(token, `(${token.value})`); return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
} }
}); });
} }

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

@ -41,7 +41,7 @@ module.exports = {
/** /**
* Reports a given reference. * Reports a given reference.
* @param {escope.Reference} reference - A reference to report. * @param {eslint-scope.Reference} reference - A reference to report.
* @returns {void} * @returns {void}
*/ */
function report(reference) { function report(reference) {

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

@ -62,10 +62,12 @@ module.exports = {
function removeNewlineBetween(firstToken, secondToken) { function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]]; const textRange = [firstToken.range[1], secondToken.range[0]];
const textBetween = sourceCode.text.slice(textRange[0], textRange[1]); const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
const NEWLINE_REGEX = astUtils.createGlobalLinebreakMatcher();
// Don't do a fix if there is a comment between the tokens // Don't do a fix if there is a comment between the tokens
return fixer => fixer.replaceTextRange(textRange, textBetween.trim() ? null : textBetween.replace(NEWLINE_REGEX, "")); if (textBetween.trim()) {
return null;
}
return fixer => fixer.replaceTextRange(textRange, " ");
} }
/** /**

6
tools/eslint/lib/rules/capitalized-comments.js

@ -19,7 +19,7 @@ const ALWAYS_MESSAGE = "Comments should not begin with a lowercase character",
NEVER_MESSAGE = "Comments should not begin with an uppercase character", NEVER_MESSAGE = "Comments should not begin with an uppercase character",
DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN, DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/g, WHITESPACE = /\s/g,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern? MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
DEFAULTS = { DEFAULTS = {
ignorePattern: null, ignorePattern: null,
ignoreInlineComments: false, ignoreInlineComments: false,
@ -270,7 +270,7 @@ module.exports = {
: NEVER_MESSAGE; : NEVER_MESSAGE;
context.report({ context.report({
node: null, // Intentionally using loc instead node: null, // Intentionally using loc instead
loc: comment.loc, loc: comment.loc,
message, message,
fix(fixer) { fix(fixer) {
@ -295,7 +295,7 @@ module.exports = {
Program() { Program() {
const comments = sourceCode.getAllComments(); const comments = sourceCode.getAllComments();
comments.forEach(processComment); comments.filter(token => token.type !== "Shebang").forEach(processComment);
} }
}; };
} }

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

@ -87,8 +87,8 @@ module.exports = {
}, },
message: options[dir] message: options[dir]
? "A space is required {{dir}} ','." ? "A space is required {{dir}} ','."
: "There should be no space {{dir}} ','.", : "There should be no space {{dir}} ','.",
data: { data: {
dir dir
} }

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

@ -202,7 +202,7 @@ module.exports = {
*/ */
if (astUtils.isCommaToken(commaToken)) { if (astUtils.isCommaToken(commaToken)) {
validateCommaItemSpacing(previousItemToken, commaToken, validateCommaItemSpacing(previousItemToken, commaToken,
currentItemToken, reportItem); currentItemToken, reportItem);
} }
if (item) { if (item) {

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

@ -122,7 +122,7 @@ module.exports = {
// Avoiding `default` // Avoiding `default`
if (node.test) { if (node.test) {
increaseComplexity(node); increaseComplexity();
} }
} }
@ -136,7 +136,7 @@ module.exports = {
// Avoiding && // Avoiding &&
if (node.operator === "||") { if (node.operator === "||") {
increaseComplexity(node); increaseComplexity();
} }
} }

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

@ -9,7 +9,6 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const astUtils = require("../ast-utils"); const astUtils = require("../ast-utils");
const esUtils = require("esutils");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
@ -240,7 +239,7 @@ module.exports = {
// e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)` // e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
const needsPrecedingSpace = node.type === "DoWhileStatement" && const needsPrecedingSpace = node.type === "DoWhileStatement" &&
sourceCode.getTokenBefore(bodyNode).end === bodyNode.start && sourceCode.getTokenBefore(bodyNode).end === bodyNode.start &&
esUtils.code.isIdentifierPartES6(sourceCode.getText(bodyNode).charCodeAt(1)); !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(bodyNode, { skip: 1 }));
const openingBracket = sourceCode.getFirstToken(bodyNode); const openingBracket = sourceCode.getFirstToken(bodyNode);
const closingBracket = sourceCode.getLastToken(bodyNode); const closingBracket = sourceCode.getLastToken(bodyNode);
@ -294,7 +293,7 @@ module.exports = {
} }
} else if (multiOrNest) { } else if (multiOrNest) {
if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) { if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
const leadingComments = sourceCode.getComments(body.body[0]).leading; const leadingComments = sourceCode.getCommentsBefore(body.body[0]);
expected = leadingComments.length > 0; expected = leadingComments.length > 0;
} else if (!isOneLiner(body)) { } else if (!isOneLiner(body)) {

2
tools/eslint/lib/rules/default-case.js

@ -74,7 +74,7 @@ module.exports = {
let comment; let comment;
const lastCase = last(node.cases); const lastCase = last(node.cases);
const comments = sourceCode.getComments(lastCase).trailing; const comments = sourceCode.getCommentsAfter(lastCase);
if (comments.length) { if (comments.length) {
comment = last(comments); comment = last(comments);

8
tools/eslint/lib/rules/dot-notation.js

@ -79,11 +79,17 @@ module.exports = {
return null; return null;
} }
const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket);
const needsSpaceAfterProperty = tokenAfterProperty &&
rightBracket.range[1] === tokenAfterProperty.range[0] &&
!astUtils.canTokensBeAdjacent(String(node.property.value), tokenAfterProperty);
const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : ""; const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
const textAfterProperty = needsSpaceAfterProperty ? " " : "";
return fixer.replaceTextRange( return fixer.replaceTextRange(
[leftBracket.range[0], rightBracket.range[1]], [leftBracket.range[0], rightBracket.range[1]],
`${textBeforeDot}.${node.property.value}` `${textBeforeDot}.${node.property.value}${textAfterProperty}`
); );
} }
}); });

105
tools/eslint/lib/rules/for-direction.js

@ -0,0 +1,105 @@
/**
* @fileoverview enforce "for" loop update clause moving the counter in the right direction.(for-direction)
* @author Aladdin-ADD<hh_2013@foxmail.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce \"for\" loop update clause moving the counter in the right direction.",
category: "Possible Errors",
recommended: false
},
fixable: null,
schema: []
},
create(context) {
/**
* report an error.
* @param {ASTNode} node the node to report.
* @returns {void}
*/
function report(node) {
context.report({
node,
message: "The update clause in this loop moves the variable in the wrong direction."
});
}
/**
* check UpdateExpression add/sub the counter
* @param {ASTNode} update UpdateExpression to check
* @param {string} counter variable name to check
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
*/
function getUpdateDirection(update, counter) {
if (update.argument.type === "Identifier" && update.argument.name === counter) {
if (update.operator === "++") {
return 1;
}
if (update.operator === "--") {
return -1;
}
}
return 0;
}
/**
* check AssignmentExpression add/sub the counter
* @param {ASTNode} update AssignmentExpression to check
* @param {string} counter variable name to check
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
*/
function getAssignmentDirection(update, counter) {
if (update.left.name === counter) {
if (update.operator === "+=") {
return 1;
}
if (update.operator === "-=") {
return -1;
}
}
return 0;
}
return {
ForStatement(node) {
if (node.test && node.test.type === "BinaryExpression" && node.test.left.type === "Identifier" && node.update) {
const counter = node.test.left.name;
const operator = node.test.operator;
const update = node.update;
if (operator === "<" || operator === "<=") {
// report error if update sub the counter (--, -=)
if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) < 0) {
report(node);
}
if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) < 0) {
report(node);
}
} else if (operator === ">" || operator === ">=") {
// report error if update add the counter (++, +=)
if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) > 0) {
report(node);
}
if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) > 0) {
report(node);
}
}
}
}
};
}
};

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

@ -13,7 +13,7 @@ const astUtils = require("../ast-utils");
/** /**
* Checks whether or not a given variable is a function name. * Checks whether or not a given variable is a function name.
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is a function name. * @returns {boolean} `true` if the variable is a function name.
*/ */
function isFunctionName(variable) { function isFunctionName(variable) {

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

@ -17,7 +17,7 @@ const ACCEPTABLE_PARENTS = [
]; ];
/** /**
* Finds the escope reference in the given scope. * Finds the eslint-scope 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|null} Returns the found reference or null if none were found. * @returns {Reference|null} Returns the found reference or null if none were found.

2
tools/eslint/lib/rules/id-length.js

@ -96,7 +96,7 @@ module.exports = {
const isLong = name.length > maxLength; const isLong = name.length > maxLength;
if (!(isShort || isLong) || exceptions[name]) { if (!(isShort || isLong) || exceptions[name]) {
return; // Nothing to report return; // Nothing to report
} }
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type]; const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];

1125
tools/eslint/lib/rules/indent-legacy.js

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

@ -33,41 +33,6 @@ function last(arr) {
return arr[arr.length - 1]; return arr[arr.length - 1];
} }
/**
* Checks whether a property is a member of the property group it follows.
* @param {ASTNode} lastMember The last Property known to be in the group.
* @param {ASTNode} candidate The next Property that might be in the group.
* @returns {boolean} True if the candidate property is part of the group.
*/
function continuesPropertyGroup(lastMember, candidate) {
const groupEndLine = lastMember.loc.start.line,
candidateStartLine = candidate.loc.start.line;
if (candidateStartLine - groupEndLine <= 1) {
return true;
}
// Check that the first comment is adjacent to the end of the group, the
// last comment is adjacent to the candidate property, and that successive
// comments are adjacent to each other.
const comments = candidate.leadingComments;
if (
comments &&
comments[0].loc.start.line - groupEndLine <= 1 &&
candidateStartLine - last(comments).loc.end.line <= 1
) {
for (let i = 1; i < comments.length; i++) {
if (comments[i].loc.start.line - comments[i - 1].loc.end.line > 1) {
return false;
}
}
return true;
}
return false;
}
/** /**
* Checks whether a node is contained on a single line. * Checks whether a node is contained on a single line.
* @param {ASTNode} node AST Node being evaluated. * @param {ASTNode} node AST Node being evaluated.
@ -350,6 +315,41 @@ module.exports = {
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
/**
* Checks whether a property is a member of the property group it follows.
* @param {ASTNode} lastMember The last Property known to be in the group.
* @param {ASTNode} candidate The next Property that might be in the group.
* @returns {boolean} True if the candidate property is part of the group.
*/
function continuesPropertyGroup(lastMember, candidate) {
const groupEndLine = lastMember.loc.start.line,
candidateStartLine = candidate.loc.start.line;
if (candidateStartLine - groupEndLine <= 1) {
return true;
}
// Check that the first comment is adjacent to the end of the group, the
// last comment is adjacent to the candidate property, and that successive
// comments are adjacent to each other.
const leadingComments = sourceCode.getCommentsBefore(candidate);
if (
leadingComments.length &&
leadingComments[0].loc.start.line - groupEndLine <= 1 &&
candidateStartLine - last(leadingComments).loc.end.line <= 1
) {
for (let i = 1; i < leadingComments.length; i++) {
if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {
return false;
}
}
return true;
}
return false;
}
/** /**
* Determines if the given property is key-value property. * Determines if the given property is key-value property.
* @param {ASTNode} property Property node to check. * @param {ASTNode} property Property node to check.
@ -634,6 +634,5 @@ module.exports = {
}; };
} }
}; };

54
tools/eslint/lib/rules/line-comment-position.js

@ -78,33 +78,37 @@ module.exports = {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
return { return {
LineComment(node) { Program() {
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) { const comments = sourceCode.getAllComments();
return;
} comments.filter(token => token.type === "Line").forEach(node => {
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) {
if (ignorePattern && customIgnoreRegExp.test(node.value)) { return;
return; }
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
const previous = sourceCode.getTokenBefore(node, { includeComments: true }); return;
const isOnSameLine = previous && previous.loc.end.line === node.loc.start.line;
if (above) {
if (isOnSameLine) {
context.report({
node,
message: "Expected comment to be above code."
});
} }
} else {
if (!isOnSameLine) { const previous = sourceCode.getTokenBefore(node, { includeComments: true });
context.report({ const isOnSameLine = previous && previous.loc.end.line === node.loc.start.line;
node,
message: "Expected comment to be beside code." if (above) {
}); if (isOnSameLine) {
context.report({
node,
message: "Expected comment to be above code."
});
}
} else {
if (!isOnSameLine) {
context.report({
node,
message: "Expected comment to be beside code."
});
}
} }
} });
} }
}; };
} }

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

@ -31,7 +31,7 @@ function getEmptyLineNums(lines) {
/** /**
* Return an array with with any line numbers that contain comments. * Return an array with with any line numbers that contain comments.
* @param {Array} comments An array of comment nodes. * @param {Array} comments An array of comment tokens.
* @returns {Array} An array of line numbers. * @returns {Array} An array of line numbers.
*/ */
function getCommentLineNums(comments) { function getCommentLineNums(comments) {
@ -131,38 +131,28 @@ module.exports = {
emptyLines = getEmptyLineNums(lines), emptyLines = getEmptyLineNums(lines),
commentAndEmptyLines = commentLines.concat(emptyLines); commentAndEmptyLines = commentLines.concat(emptyLines);
/**
* Returns whether or not a token is a comment node type
* @param {Token} token The token to check
* @returns {boolean} True if the token is a comment node
*/
function isCommentNodeType(token) {
return token && (token.type === "Block" || token.type === "Line");
}
/** /**
* Returns whether or not comments are on lines starting with or ending with code * Returns whether or not comments are on lines starting with or ending with code
* @param {ASTNode} node The comment node to check. * @param {token} token The comment token to check.
* @returns {boolean} True if the comment is not alone. * @returns {boolean} True if the comment is not alone.
*/ */
function codeAroundComment(node) { function codeAroundComment(token) {
let token; let currentToken = token;
token = node;
do { do {
token = sourceCode.getTokenBefore(token, { includeComments: true }); currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
} while (isCommentNodeType(token)); } while (currentToken && astUtils.isCommentToken(currentToken));
if (token && astUtils.isTokenOnSameLine(token, node)) { if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
return true; return true;
} }
token = node; currentToken = token;
do { do {
token = sourceCode.getTokenAfter(token, { includeComments: true }); currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
} while (isCommentNodeType(token)); } while (currentToken && astUtils.isCommentToken(currentToken));
if (token && astUtils.isTokenOnSameLine(node, token)) { if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
return true; return true;
} }
@ -171,137 +161,135 @@ module.exports = {
/** /**
* Returns whether or not comments are inside a node type or not. * Returns whether or not comments are inside a node type or not.
* @param {ASTNode} node The Comment node.
* @param {ASTNode} parent The Comment parent node. * @param {ASTNode} parent The Comment parent node.
* @param {string} nodeType The parent type to check against. * @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is inside nodeType. * @returns {boolean} True if the comment is inside nodeType.
*/ */
function isCommentInsideNodeType(node, parent, nodeType) { function isParentNodeType(parent, nodeType) {
return parent.type === nodeType || return parent.type === nodeType ||
(parent.body && parent.body.type === nodeType) || (parent.body && parent.body.type === nodeType) ||
(parent.consequent && parent.consequent.type === nodeType); (parent.consequent && parent.consequent.type === nodeType);
} }
/**
* Returns the parent node that contains the given token.
* @param {token} token The token to check.
* @returns {ASTNode} The parent node that contains the given token.
*/
function getParentNodeOfToken(token) {
return sourceCode.getNodeByRangeIndex(token.range[0]);
}
/** /**
* Returns whether or not comments are at the parent start or not. * Returns whether or not comments are at the parent start or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @param {string} nodeType The parent type to check against. * @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is at parent start. * @returns {boolean} True if the comment is at parent start.
*/ */
function isCommentAtParentStart(node, nodeType) { function isCommentAtParentStart(token, nodeType) {
const ancestors = context.getAncestors(); const parent = getParentNodeOfToken(token);
let parent;
if (ancestors.length) { return parent && isParentNodeType(parent, nodeType) &&
parent = ancestors.pop(); token.loc.start.line - parent.loc.start.line === 1;
}
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
node.loc.start.line - parent.loc.start.line === 1;
} }
/** /**
* Returns whether or not comments are at the parent end or not. * Returns whether or not comments are at the parent end or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @param {string} nodeType The parent type to check against. * @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is at parent end. * @returns {boolean} True if the comment is at parent end.
*/ */
function isCommentAtParentEnd(node, nodeType) { function isCommentAtParentEnd(token, nodeType) {
const ancestors = context.getAncestors(); const parent = getParentNodeOfToken(token);
let parent;
if (ancestors.length) {
parent = ancestors.pop();
}
return parent && isCommentInsideNodeType(node, parent, nodeType) && return parent && isParentNodeType(parent, nodeType) &&
parent.loc.end.line - node.loc.end.line === 1; parent.loc.end.line - token.loc.end.line === 1;
} }
/** /**
* Returns whether or not comments are at the block start or not. * Returns whether or not comments are at the block start or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at block start. * @returns {boolean} True if the comment is at block start.
*/ */
function isCommentAtBlockStart(node) { function isCommentAtBlockStart(token) {
return isCommentAtParentStart(node, "ClassBody") || isCommentAtParentStart(node, "BlockStatement") || isCommentAtParentStart(node, "SwitchCase"); return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
} }
/** /**
* Returns whether or not comments are at the block end or not. * Returns whether or not comments are at the block end or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at block end. * @returns {boolean} True if the comment is at block end.
*/ */
function isCommentAtBlockEnd(node) { function isCommentAtBlockEnd(token) {
return isCommentAtParentEnd(node, "ClassBody") || isCommentAtParentEnd(node, "BlockStatement") || isCommentAtParentEnd(node, "SwitchCase") || isCommentAtParentEnd(node, "SwitchStatement"); return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
} }
/** /**
* Returns whether or not comments are at the object start or not. * Returns whether or not comments are at the object start or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at object start. * @returns {boolean} True if the comment is at object start.
*/ */
function isCommentAtObjectStart(node) { function isCommentAtObjectStart(token) {
return isCommentAtParentStart(node, "ObjectExpression") || isCommentAtParentStart(node, "ObjectPattern"); return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
} }
/** /**
* Returns whether or not comments are at the object end or not. * Returns whether or not comments are at the object end or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at object end. * @returns {boolean} True if the comment is at object end.
*/ */
function isCommentAtObjectEnd(node) { function isCommentAtObjectEnd(token) {
return isCommentAtParentEnd(node, "ObjectExpression") || isCommentAtParentEnd(node, "ObjectPattern"); return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
} }
/** /**
* Returns whether or not comments are at the array start or not. * Returns whether or not comments are at the array start or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at array start. * @returns {boolean} True if the comment is at array start.
*/ */
function isCommentAtArrayStart(node) { function isCommentAtArrayStart(token) {
return isCommentAtParentStart(node, "ArrayExpression") || isCommentAtParentStart(node, "ArrayPattern"); return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
} }
/** /**
* Returns whether or not comments are at the array end or not. * Returns whether or not comments are at the array end or not.
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @returns {boolean} True if the comment is at array end. * @returns {boolean} True if the comment is at array end.
*/ */
function isCommentAtArrayEnd(node) { function isCommentAtArrayEnd(token) {
return isCommentAtParentEnd(node, "ArrayExpression") || isCommentAtParentEnd(node, "ArrayPattern"); return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
} }
/** /**
* Checks if a comment node has lines around it (ignores inline comments) * Checks if a comment token has lines around it (ignores inline comments)
* @param {ASTNode} node The Comment node. * @param {token} token The Comment token.
* @param {Object} opts Options to determine the newline. * @param {Object} opts Options to determine the newline.
* @param {boolean} opts.after Should have a newline after this line. * @param {boolean} opts.after Should have a newline after this line.
* @param {boolean} opts.before Should have a newline before this line. * @param {boolean} opts.before Should have a newline before this line.
* @returns {void} * @returns {void}
*/ */
function checkForEmptyLine(node, opts) { function checkForEmptyLine(token, opts) {
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(node.value)) { if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
return; return;
} }
if (ignorePattern && customIgnoreRegExp.test(node.value)) { if (ignorePattern && customIgnoreRegExp.test(token.value)) {
return; return;
} }
let after = opts.after, let after = opts.after,
before = opts.before; before = opts.before;
const prevLineNum = node.loc.start.line - 1, const prevLineNum = token.loc.start.line - 1,
nextLineNum = node.loc.end.line + 1, nextLineNum = token.loc.end.line + 1,
commentIsNotAlone = codeAroundComment(node); commentIsNotAlone = codeAroundComment(token);
const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(node), const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(token),
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(node), blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token),
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(node), objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(node), objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(node), arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(node); arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed; const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed;
const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed; const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed;
@ -319,17 +307,17 @@ module.exports = {
return; return;
} }
const previousTokenOrComment = sourceCode.getTokenBefore(node, { includeComments: true }); const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
const nextTokenOrComment = sourceCode.getTokenAfter(node, { includeComments: true }); const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
// check for newline before // check for newline before
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) && if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
!(isCommentNodeType(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, node))) { !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
const lineStart = node.range[0] - node.loc.start.column; const lineStart = token.range[0] - token.loc.start.column;
const range = [lineStart, lineStart]; const range = [lineStart, lineStart];
context.report({ context.report({
node, node: token,
message: "Expected line before comment.", message: "Expected line before comment.",
fix(fixer) { fix(fixer) {
return fixer.insertTextBeforeRange(range, "\n"); return fixer.insertTextBeforeRange(range, "\n");
@ -339,12 +327,12 @@ module.exports = {
// check for newline after // check for newline after
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) && if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
!(isCommentNodeType(nextTokenOrComment) && astUtils.isTokenOnSameLine(node, nextTokenOrComment))) { !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
context.report({ context.report({
node, node: token,
message: "Expected line after comment.", message: "Expected line after comment.",
fix(fixer) { fix(fixer) {
return fixer.insertTextAfter(node, "\n"); return fixer.insertTextAfter(token, "\n");
} }
}); });
} }
@ -356,25 +344,25 @@ module.exports = {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
return { return {
Program() {
LineComment(node) { comments.forEach(token => {
if (options.beforeLineComment || options.afterLineComment) { if (token.type === "Line") {
checkForEmptyLine(node, { if (options.beforeLineComment || options.afterLineComment) {
after: options.afterLineComment, checkForEmptyLine(token, {
before: options.beforeLineComment after: options.afterLineComment,
}); before: options.beforeLineComment
} });
}, }
} else if (token.type === "Block") {
BlockComment(node) { if (options.beforeBlockComment || options.afterBlockComment) {
if (options.beforeBlockComment || options.afterBlockComment) { checkForEmptyLine(token, {
checkForEmptyLine(node, { after: options.afterBlockComment,
after: options.afterBlockComment, before: options.beforeBlockComment
before: options.beforeBlockComment });
}); }
} }
});
} }
}; };
} }
}; };

18
tools/eslint/lib/rules/lines-around-directive.js

@ -1,6 +1,7 @@
/** /**
* @fileoverview Require or disallow newlines around directives. * @fileoverview Require or disallow newlines around directives.
* @author Kai Cataldo * @author Kai Cataldo
* @deprecated
*/ */
"use strict"; "use strict";
@ -16,7 +17,8 @@ module.exports = {
docs: { docs: {
description: "require or disallow newlines around directives", description: "require or disallow newlines around directives",
category: "Stylistic Issues", category: "Stylistic Issues",
recommended: false recommended: false,
replacedBy: ["padding-line-between-statements"]
}, },
schema: [{ schema: [{
oneOf: [ oneOf: [
@ -38,7 +40,8 @@ module.exports = {
} }
] ]
}], }],
fixable: "whitespace" fixable: "whitespace",
deprecated: true
}, },
create(context) { create(context) {
@ -131,17 +134,12 @@ module.exports = {
} }
const firstDirective = directives[0]; const firstDirective = directives[0];
const hasTokenOrCommentBefore = !!sourceCode.getTokenBefore(firstDirective, { includeComments: true }); const leadingComments = sourceCode.getCommentsBefore(firstDirective);
// Only check before the first directive if it is preceded by a comment or if it is at the top of // Only check before the first directive if it is preceded by a comment or if it is at the top of
// the file and expectLineBefore is set to "never". This is to not force a newline at the top of // the file and expectLineBefore is set to "never". This is to not force a newline at the top of
// the file if there are no comments as well as for compatibility with padded-blocks. // the file if there are no comments as well as for compatibility with padded-blocks.
if ( if (leadingComments.length) {
firstDirective.leadingComments && firstDirective.leadingComments.length ||
// Shebangs are not added to leading comments but are accounted for by the following.
node.type === "Program" && hasTokenOrCommentBefore
) {
if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) { if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) {
reportError(firstDirective, "before", true); reportError(firstDirective, "before", true);
} }
@ -152,7 +150,7 @@ module.exports = {
} else if ( } else if (
node.type === "Program" && node.type === "Program" &&
expectLineBefore === "never" && expectLineBefore === "never" &&
!hasTokenOrCommentBefore && !leadingComments.length &&
hasNewlineBefore(firstDirective) hasNewlineBefore(firstDirective)
) { ) {
reportError(firstDirective, "before", false); reportError(firstDirective, "before", false);

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

@ -108,7 +108,7 @@ module.exports = {
previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0, previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0,
spaceCount = tabWidth - previousTabStopOffset; spaceCount = tabWidth - previousTabStopOffset;
extraCharacterCount += spaceCount - 1; // -1 for the replaced tab extraCharacterCount += spaceCount - 1; // -1 for the replaced tab
}); });
return Array.from(line).length + extraCharacterCount; return Array.from(line).length + extraCharacterCount;
} }
@ -268,13 +268,13 @@ module.exports = {
// we iterate over comments in parallel with the lines // we iterate over comments in parallel with the lines
let commentsIndex = 0; let commentsIndex = 0;
const strings = getAllStrings(sourceCode); const strings = getAllStrings();
const stringsByLine = strings.reduce(groupByLineNumber, {}); const stringsByLine = strings.reduce(groupByLineNumber, {});
const templateLiterals = getAllTemplateLiterals(sourceCode); const templateLiterals = getAllTemplateLiterals();
const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {}); const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {});
const regExpLiterals = getAllRegExpLiterals(sourceCode); const regExpLiterals = getAllRegExpLiterals();
const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {}); const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {});
lines.forEach((line, i) => { lines.forEach((line, i) => {
@ -321,21 +321,24 @@ module.exports = {
} }
const lineLength = computeLineLength(line, tabWidth); const lineLength = computeLineLength(line, tabWidth);
const commentLengthApplies = lineIsComment && maxCommentLength;
if (lineIsComment && ignoreComments) { if (lineIsComment && ignoreComments) {
return; return;
} }
if (lineIsComment && lineLength > maxCommentLength) { if (commentLengthApplies) {
context.report({ if (lineLength > maxCommentLength) {
node, context.report({
loc: { line: lineNumber, column: 0 }, node,
message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.", loc: { line: lineNumber, column: 0 },
data: { message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.",
lineNumber: i + 1, data: {
maxCommentLength lineNumber: i + 1,
} maxCommentLength
}); }
});
}
} else if (lineLength > maxLength) { } else if (lineLength > maxLength) {
context.report({ context.report({
node, node,

2
tools/eslint/lib/rules/new-parens.js

@ -38,7 +38,7 @@ module.exports = {
return { return {
NewExpression(node) { NewExpression(node) {
if (node.arguments.length !== 0) { if (node.arguments.length !== 0) {
return; // shortcut: if there are arguments, there have to be parens return; // shortcut: if there are arguments, there have to be parens
} }
const lastToken = sourceCode.getLastToken(node); const lastToken = sourceCode.getLastToken(node);

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

@ -1,6 +1,7 @@
/** /**
* @fileoverview Rule to check empty newline after "var" statement * @fileoverview Rule to check empty newline after "var" statement
* @author Gopal Venkatesan * @author Gopal Venkatesan
* @deprecated
*/ */
"use strict"; "use strict";
@ -20,7 +21,8 @@ module.exports = {
docs: { docs: {
description: "require or disallow an empty line after variable declarations", description: "require or disallow an empty line after variable declarations",
category: "Stylistic Issues", category: "Stylistic Issues",
recommended: false recommended: false,
replacedBy: ["padding-line-between-statements"]
}, },
schema: [ schema: [
@ -29,7 +31,9 @@ module.exports = {
} }
], ],
fixable: "whitespace" fixable: "whitespace",
deprecated: true
}, },
create(context) { create(context) {

17
tools/eslint/lib/rules/newline-before-return.js

@ -1,6 +1,7 @@
/** /**
* @fileoverview Rule to require newlines before `return` statement * @fileoverview Rule to require newlines before `return` statement
* @author Kai Cataldo * @author Kai Cataldo
* @deprecated
*/ */
"use strict"; "use strict";
@ -13,10 +14,12 @@ module.exports = {
docs: { docs: {
description: "require an empty line before `return` statements", description: "require an empty line before `return` statements",
category: "Stylistic Issues", category: "Stylistic Issues",
recommended: false recommended: false,
replacedBy: ["padding-line-between-statements"]
}, },
fixable: "whitespace", fixable: "whitespace",
schema: [] schema: [],
deprecated: true
}, },
create(context) { create(context) {
@ -50,8 +53,8 @@ module.exports = {
if (node.parent.body) { if (node.parent.body) {
return Array.isArray(node.parent.body) return Array.isArray(node.parent.body)
? node.parent.body[0] === node ? node.parent.body[0] === node
: node.parent.body === node; : node.parent.body === node;
} }
if (parentType === "IfStatement") { if (parentType === "IfStatement") {
@ -73,7 +76,7 @@ module.exports = {
* @private * @private
*/ */
function calcCommentLines(node, lineNumTokenBefore) { function calcCommentLines(node, lineNumTokenBefore) {
const comments = sourceCode.getComments(node).leading; const comments = sourceCode.getCommentsBefore(node);
let numLinesComments = 0; let numLinesComments = 0;
if (!comments.length) { if (!comments.length) {
@ -121,7 +124,7 @@ module.exports = {
if (tokenBefore) { if (tokenBefore) {
lineNumTokenBefore = tokenBefore.loc.end.line; lineNumTokenBefore = tokenBefore.loc.end.line;
} else { } else {
lineNumTokenBefore = 0; // global return at beginning of script lineNumTokenBefore = 0; // global return at beginning of script
} }
return lineNumTokenBefore; return lineNumTokenBefore;
@ -153,7 +156,7 @@ module.exports = {
* @private * @private
*/ */
function canFix(node) { function canFix(node) {
const leadingComments = sourceCode.getComments(node).leading; const leadingComments = sourceCode.getCommentsBefore(node);
const lastLeadingComment = leadingComments[leadingComments.length - 1]; const lastLeadingComment = leadingComments[leadingComments.length - 1];
const tokenBefore = sourceCode.getTokenBefore(node); const tokenBefore = sourceCode.getTokenBefore(node);

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

@ -35,7 +35,7 @@ function report(context, node, identifierName) {
} }
/** /**
* Finds the escope reference in the given scope. * Finds the eslint-scope 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|null} Returns the found reference or null if none were found. * @returns {Reference|null} Returns the found reference or null if none were found.

37
tools/eslint/lib/rules/no-buffer-constructor.js

@ -0,0 +1,37 @@
/**
* @fileoverview disallow use of the Buffer() constructor
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow use of the Buffer() constructor",
category: "Node.js and CommonJS",
recommended: false
},
schema: []
},
create(context) {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
"CallExpression[callee.name='Buffer'], NewExpression[callee.name='Buffer']"(node) {
context.report({
node,
message: "{{example}} is deprecated. Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead.",
data: { example: node.type === "CallExpression" ? "Buffer()" : "new Buffer()" }
});
}
};
}
};

2
tools/eslint/lib/rules/no-compare-neg-zero.js

@ -13,7 +13,7 @@ module.exports = {
docs: { docs: {
description: "disallow comparing against -0", description: "disallow comparing against -0",
category: "Possible Errors", category: "Possible Errors",
recommended: false recommended: true
}, },
fixable: null, fixable: null,
schema: [] schema: []

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

@ -33,6 +33,8 @@ module.exports = {
recommended: false recommended: false
}, },
fixable: "code",
schema: [{ schema: [{
type: "object", type: "object",
properties: { properties: {
@ -55,7 +57,15 @@ module.exports = {
const body = node.body; const body = node.body;
if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(sourceCode, body))) { if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(sourceCode, body))) {
context.report({ node, message: "Arrow function used ambiguously with a conditional expression." }); context.report({
node,
message: "Arrow function used ambiguously with a conditional expression.",
fix(fixer) {
// if `allowParens` is not set to true dont bother wrapping in parens
return config.allowParens && fixer.replaceText(node.body, `(${sourceCode.getText(node.body)})`);
}
});
} }
} }

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

@ -48,7 +48,7 @@ module.exports = {
/** /**
* Checks whether the given reference is 'console' or not. * Checks whether the given reference is 'console' or not.
* *
* @param {escope.Reference} reference - The reference to check. * @param {eslint-scope.Reference} reference - The reference to check.
* @returns {boolean} `true` if the reference is 'console'. * @returns {boolean} `true` if the reference is 'console'.
*/ */
function isConsole(reference) { function isConsole(reference) {
@ -74,7 +74,7 @@ module.exports = {
* Checks whether the given reference is a member access which is not * Checks whether the given reference is a member access which is not
* allowed by options or not. * allowed by options or not.
* *
* @param {escope.Reference} reference - The reference to check. * @param {eslint-scope.Reference} reference - The reference to check.
* @returns {boolean} `true` if the reference is a member access which * @returns {boolean} `true` if the reference is a member access which
* is not allowed by options. * is not allowed by options.
*/ */
@ -92,7 +92,7 @@ module.exports = {
/** /**
* Reports the given reference as a violation. * Reports the given reference as a violation.
* *
* @param {escope.Reference} reference - The reference to report. * @param {eslint-scope.Reference} reference - The reference to report.
* @returns {void} * @returns {void}
*/ */
function report(reference) { function report(reference) {

10
tools/eslint/lib/rules/no-debugger.js

@ -16,7 +16,7 @@ module.exports = {
category: "Possible Errors", category: "Possible Errors",
recommended: true recommended: true
}, },
fixable: "code",
schema: [] schema: []
}, },
@ -24,7 +24,13 @@ module.exports = {
return { return {
DebuggerStatement(node) { DebuggerStatement(node) {
context.report({ node, message: "Unexpected 'debugger' statement." }); context.report({
node,
message: "Unexpected 'debugger' statement.",
fix(fixer) {
return fixer.remove(node);
}
});
} }
}; };

2
tools/eslint/lib/rules/no-dupe-args.js

@ -28,7 +28,7 @@ module.exports = {
/** /**
* Checks whether or not a given definition is a parameter's. * Checks whether or not a given definition is a parameter's.
* @param {escope.DefEntry} def - A definition to check. * @param {eslint-scope.DefEntry} def - A definition to check.
* @returns {boolean} `true` if the definition is a parameter's. * @returns {boolean} `true` if the definition is a parameter's.
*/ */
function isParameter(def) { function isParameter(def) {

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

@ -132,11 +132,15 @@ module.exports = {
function reportIfEmpty(node) { function reportIfEmpty(node) {
const kind = getKind(node); const kind = getKind(node);
const name = astUtils.getFunctionNameWithKind(node); const name = astUtils.getFunctionNameWithKind(node);
const innerComments = sourceCode.getTokens(node.body, {
includeComments: true,
filter: astUtils.isCommentToken
});
if (allowed.indexOf(kind) === -1 && if (allowed.indexOf(kind) === -1 &&
node.body.type === "BlockStatement" && node.body.type === "BlockStatement" &&
node.body.body.length === 0 && node.body.body.length === 0 &&
sourceCode.getComments(node.body).trailing.length === 0 innerComments.length === 0
) { ) {
context.report({ context.report({
node, node,

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

@ -59,7 +59,7 @@ module.exports = {
} }
// any other block is only allowed to be empty, if it contains a comment // any other block is only allowed to be empty, if it contains a comment
if (sourceCode.getComments(node).trailing.length > 0) { if (sourceCode.getCommentsInside(node).length > 0) {
return; return;
} }

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

@ -166,7 +166,7 @@ module.exports = {
/** /**
* Reports accesses of `eval` via the global object. * Reports accesses of `eval` via the global object.
* *
* @param {escope.Scope} globalScope - The global scope. * @param {eslint-scope.Scope} globalScope - The global scope.
* @returns {void} * @returns {void}
*/ */
function reportAccessingEvalViaGlobalObject(globalScope) { function reportAccessingEvalViaGlobalObject(globalScope) {
@ -200,7 +200,7 @@ module.exports = {
/** /**
* Reports all accesses of `eval` (excludes direct calls to eval). * Reports all accesses of `eval` (excludes direct calls to eval).
* *
* @param {escope.Scope} globalScope - The global scope. * @param {eslint-scope.Scope} globalScope - The global scope.
* @returns {void} * @returns {void}
*/ */
function reportAccessingEval(globalScope) { function reportAccessingEval(globalScope) {

169
tools/eslint/lib/rules/no-extend-native.js

@ -9,8 +9,15 @@
// Requirements // Requirements
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const globals = require("globals"); const globals = require("globals");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]);
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -43,73 +50,123 @@ module.exports = {
create(context) { create(context) {
const config = context.options[0] || {}; const config = context.options[0] || {};
const exceptions = config.exceptions || []; const exceptions = new Set(config.exceptions || []);
let modifiedBuiltins = Object.keys(globals.builtin).filter(builtin => builtin[0].toUpperCase() === builtin[0]); const modifiedBuiltins = new Set(
Object.keys(globals.builtin)
.filter(builtin => builtin[0].toUpperCase() === builtin[0])
.filter(builtin => !exceptions.has(builtin))
);
/**
* Reports a lint error for the given node.
* @param {ASTNode} node The node to report.
* @param {string} builtin The name of the native builtin being extended.
* @returns {void}
*/
function reportNode(node, builtin) {
context.report({
node,
message: "{{builtin}} prototype is read only, properties should not be added.",
data: {
builtin
}
});
}
if (exceptions.length) { /**
modifiedBuiltins = modifiedBuiltins.filter(builtIn => exceptions.indexOf(builtIn) === -1); * Check to see if the `prototype` property of the given object
* identifier node is being accessed.
* @param {ASTNode} identifierNode The Identifier representing the object
* to check.
* @returns {boolean} True if the identifier is the object of a
* MemberExpression and its `prototype` property is being accessed,
* false otherwise.
*/
function isPrototypePropertyAccessed(identifierNode) {
return Boolean(
identifierNode &&
identifierNode.parent &&
identifierNode.parent.type === "MemberExpression" &&
identifierNode.parent.object === identifierNode &&
astUtils.getStaticPropertyName(identifierNode.parent) === "prototype"
);
} }
return { /**
* Checks that an identifier is an object of a prototype whose member
* is being assigned in an AssignmentExpression.
* Example: Object.prototype.foo = "bar"
* @param {ASTNode} identifierNode The identifier to check.
* @returns {boolean} True if the identifier's prototype is modified.
*/
function isInPrototypePropertyAssignment(identifierNode) {
return Boolean(
isPrototypePropertyAccessed(identifierNode) &&
identifierNode.parent.parent.type === "MemberExpression" &&
identifierNode.parent.parent.parent.type === "AssignmentExpression" &&
identifierNode.parent.parent.parent.left === identifierNode.parent.parent
);
}
// handle the Array.prototype.extra style case /**
AssignmentExpression(node) { * Checks that an identifier is an object of a prototype whose member
const lhs = node.left; * is being extended via the Object.defineProperty() or
* Object.defineProperties() methods.
* Example: Object.defineProperty(Array.prototype, "foo", ...)
* Example: Object.defineProperties(Array.prototype, ...)
* @param {ASTNode} identifierNode The identifier to check.
* @returns {boolean} True if the identifier's prototype is modified.
*/
function isInDefinePropertyCall(identifierNode) {
return Boolean(
isPrototypePropertyAccessed(identifierNode) &&
identifierNode.parent.parent.type === "CallExpression" &&
identifierNode.parent.parent.arguments[0] === identifierNode.parent &&
identifierNode.parent.parent.callee.type === "MemberExpression" &&
identifierNode.parent.parent.callee.object.type === "Identifier" &&
identifierNode.parent.parent.callee.object.name === "Object" &&
identifierNode.parent.parent.callee.property.type === "Identifier" &&
propertyDefinitionMethods.has(identifierNode.parent.parent.callee.property.name)
);
}
if (lhs.type !== "MemberExpression" || lhs.object.type !== "MemberExpression") { /**
return; * Check to see if object prototype access is part of a prototype
} * extension. There are three ways a prototype can be extended:
* 1. Assignment to prototype property (Object.prototype.foo = 1)
* 2. Object.defineProperty()/Object.defineProperties() on a prototype
* If prototype extension is detected, report the AssignmentExpression
* or CallExpression node.
* @param {ASTNode} identifierNode The Identifier representing the object
* which prototype is being accessed and possibly extended.
* @returns {void}
*/
function checkAndReportPrototypeExtension(identifierNode) {
if (isInPrototypePropertyAssignment(identifierNode)) {
// Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression
reportNode(identifierNode.parent.parent.parent, identifierNode.name);
} else if (isInDefinePropertyCall(identifierNode)) {
// Identifier --> MemberExpression --> CallExpression
reportNode(identifierNode.parent.parent, identifierNode.name);
}
}
const affectsProto = lhs.object.computed return {
? lhs.object.property.type === "Literal" && lhs.object.property.value === "prototype"
: lhs.object.property.name === "prototype";
if (!affectsProto) { "Program:exit"() {
return; const globalScope = context.getScope();
}
modifiedBuiltins.forEach(builtin => { modifiedBuiltins.forEach(builtin => {
if (lhs.object.object.name === builtin) { const builtinVar = globalScope.set.get(builtin);
context.report({
node, if (builtinVar && builtinVar.references) {
message: "{{builtin}} prototype is read only, properties should not be added.", builtinVar.references
data: { .map(ref => ref.identifier)
builtin .forEach(checkAndReportPrototypeExtension);
}
});
} }
}); });
},
// handle the Object.definePropert[y|ies](Array.prototype) case
CallExpression(node) {
const callee = node.callee;
// only worry about Object.definePropert[y|ies]
if (callee.type === "MemberExpression" &&
callee.object.name === "Object" &&
(callee.property.name === "defineProperty" || callee.property.name === "defineProperties")) {
// verify the object being added to is a native prototype
const subject = node.arguments[0];
const object = subject && subject.object;
if (object &&
object.type === "Identifier" &&
(modifiedBuiltins.indexOf(object.name) > -1) &&
subject.property.name === "prototype") {
context.report({
node,
message: "{{objectName}} prototype is read only, properties should not be added.",
data: {
objectName: object.name
}
});
}
}
} }
}; };

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

@ -9,7 +9,6 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const astUtils = require("../ast-utils.js"); const astUtils = require("../ast-utils.js");
const esUtils = require("esutils");
module.exports = { module.exports = {
meta: { meta: {
@ -250,28 +249,27 @@ module.exports = {
const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1); const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1);
const firstToken = sourceCode.getFirstToken(node); const firstToken = sourceCode.getFirstToken(node);
// If there is already whitespace before the previous token, don't add more. return tokenBeforeLeftParen &&
if (!tokenBeforeLeftParen || tokenBeforeLeftParen.end !== leftParenToken.start) { tokenBeforeLeftParen.range[1] === leftParenToken.range[0] &&
return false; leftParenToken.range[1] === firstToken.range[0] &&
} !astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, firstToken);
}
// If the parens are preceded by a keyword (e.g. `typeof(0)`), a space should be inserted (`typeof 0`)
const precededByIdentiferPart = esUtils.code.isIdentifierPartES6(tokenBeforeLeftParen.value.slice(-1).charCodeAt(0));
// However, a space should not be inserted unless the first character of the token is an identifier part
// e.g. `typeof([])` should be fixed to `typeof[]`
const startsWithIdentifierPart = esUtils.code.isIdentifierPartES6(firstToken.value.charCodeAt(0));
// If the parens are preceded by and start with a unary plus/minus (e.g. `+(+foo)`), a space should be inserted (`+ +foo`)
const precededByUnaryPlus = tokenBeforeLeftParen.type === "Punctuator" && tokenBeforeLeftParen.value === "+";
const precededByUnaryMinus = tokenBeforeLeftParen.type === "Punctuator" && tokenBeforeLeftParen.value === "-";
const startsWithUnaryPlus = firstToken.type === "Punctuator" && firstToken.value === "+";
const startsWithUnaryMinus = firstToken.type === "Punctuator" && firstToken.value === "-";
return (precededByIdentiferPart && startsWithIdentifierPart) || /**
(precededByUnaryPlus && startsWithUnaryPlus) || * Determines whether a node should be followed by an additional space when removing parens
(precededByUnaryMinus && startsWithUnaryMinus); * @param {ASTNode} node node to evaluate; must be surrounded by parentheses
* @returns {boolean} `true` if a space should be inserted after the node
* @private
*/
function requiresTrailingSpace(node) {
const nextTwoTokens = sourceCode.getTokensAfter(node, { count: 2 });
const rightParenToken = nextTwoTokens[0];
const tokenAfterRightParen = nextTwoTokens[1];
const tokenBeforeRightParen = sourceCode.getLastToken(node);
return rightParenToken && tokenAfterRightParen &&
!sourceCode.isSpaceBetweenTokens(rightParenToken, tokenAfterRightParen) &&
!astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen);
} }
/** /**
@ -298,7 +296,7 @@ module.exports = {
return fixer.replaceTextRange([ return fixer.replaceTextRange([
leftParenToken.range[0], leftParenToken.range[0],
rightParenToken.range[1] rightParenToken.range[1]
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource); ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
} }
}); });
} }
@ -507,12 +505,18 @@ module.exports = {
if (hasExcessParens(node.right)) { if (hasExcessParens(node.right)) {
report(node.right); report(node.right);
} }
if (hasExcessParens(node.left)) {
report(node.left);
}
}, },
ForOfStatement(node) { ForOfStatement(node) {
if (hasExcessParens(node.right)) { if (hasExcessParens(node.right)) {
report(node.right); report(node.right);
} }
if (hasExcessParens(node.left)) {
report(node.left);
}
}, },
ForStatement(node) { ForStatement(node) {

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

@ -25,7 +25,7 @@ const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/i;
*/ */
function hasFallthroughComment(node, context, fallthroughCommentPattern) { function hasFallthroughComment(node, context, fallthroughCommentPattern) {
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
const comment = lodash.last(sourceCode.getComments(node).leading); const comment = lodash.last(sourceCode.getCommentsBefore(node));
return Boolean(comment && fallthroughCommentPattern.test(comment.value)); return Boolean(comment && fallthroughCommentPattern.test(comment.value));
} }

18
tools/eslint/lib/rules/no-floating-decimal.js

@ -5,6 +5,12 @@
"use strict"; "use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -23,16 +29,24 @@ module.exports = {
}, },
create(context) { create(context) {
const sourceCode = context.getSourceCode();
return { return {
Literal(node) { Literal(node) {
if (typeof node.value === "number") { if (typeof node.value === "number") {
if (node.raw.indexOf(".") === 0) { if (node.raw.startsWith(".")) {
context.report({ context.report({
node, node,
message: "A leading decimal point can be confused with a dot.", message: "A leading decimal point can be confused with a dot.",
fix: fixer => fixer.insertTextBefore(node, "0") fix(fixer) {
const tokenBefore = sourceCode.getTokenBefore(node);
const needsSpaceBefore = tokenBefore &&
tokenBefore.range[1] === node.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBefore, `0${node.raw}`);
return fixer.insertTextBefore(node, needsSpaceBefore ? " 0" : "0");
}
}); });
} }
if (node.raw.indexOf(".") === node.raw.length - 1) { if (node.raw.indexOf(".") === node.raw.length - 1) {

4
tools/eslint/lib/rules/no-implicit-coercion.js

@ -6,7 +6,6 @@
"use strict"; "use strict";
const astUtils = require("../ast-utils"); const astUtils = require("../ast-utils");
const esUtils = require("esutils");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Helpers // Helpers
@ -215,8 +214,7 @@ module.exports = {
if ( if (
tokenBefore && tokenBefore &&
tokenBefore.range[1] === node.range[0] && tokenBefore.range[1] === node.range[0] &&
esUtils.code.isIdentifierPartES6(tokenBefore.value.slice(-1).charCodeAt(0)) && !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
esUtils.code.isIdentifierPartES6(recommendation.charCodeAt(0))
) { ) {
return fixer.replaceText(node, ` ${recommendation}`); return fixer.replaceText(node, ` ${recommendation}`);
} }

7
tools/eslint/lib/rules/no-inline-comments.js

@ -55,10 +55,11 @@ module.exports = {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
return { return {
Program() {
const comments = sourceCode.getAllComments();
LineComment: testCodeAroundComment, comments.filter(token => token.type !== "Shebang").forEach(testCodeAroundComment);
BlockComment: testCodeAroundComment }
}; };
} }
}; };

8
tools/eslint/lib/rules/no-inner-declarations.js

@ -58,16 +58,14 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
function check(node) { function check(node) {
const body = nearestBody(node), const body = nearestBody(),
valid = ((body.type === "Program" && body.distance === 1) || valid = ((body.type === "Program" && body.distance === 1) ||
body.distance === 2); body.distance === 2);
if (!valid) { if (!valid) {
context.report({ node, message: "Move {{type}} declaration to {{body}} root.", data: { context.report({ node, message: "Move {{type}} declaration to {{body}} root.", data: {
type: (node.type === "FunctionDeclaration" type: (node.type === "FunctionDeclaration" ? "function" : "variable"),
? "function" : "variable"), body: (body.type === "Program" ? "program" : "function body")
body: (body.type === "Program"
? "program" : "function body")
} }); } });
} }
} }

24
tools/eslint/lib/rules/no-irregular-whitespace.js

@ -16,8 +16,8 @@ const astUtils = require("../ast-utils");
// Constants // Constants
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const ALL_IRREGULARS = /[\f\v\u0085\u00A0\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/; const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/;
const IRREGULAR_WHITESPACE = /[\f\v\u0085\u00A0\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mg; const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mg;
const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mg; const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mg;
const LINE_BREAK = astUtils.createGlobalLinebreakMatcher(); const LINE_BREAK = astUtils.createGlobalLinebreakMatcher();
@ -60,9 +60,6 @@ module.exports = {
// Module store of errors that we have found // Module store of errors that we have found
let errors = []; let errors = [];
// Comment nodes. We accumulate these as we go, so we can be sure to trigger them after the whole `Program` entity is parsed, even for top-of-file comments.
const commentNodes = [];
// Lookup the `skipComments` option, which defaults to `false`. // Lookup the `skipComments` option, which defaults to `false`.
const options = context.options[0] || {}; const options = context.options[0] || {};
const skipComments = !!options.skipComments; const skipComments = !!options.skipComments;
@ -71,6 +68,7 @@ module.exports = {
const skipTemplates = !!options.skipTemplates; const skipTemplates = !!options.skipTemplates;
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
const commentNodes = sourceCode.getAllComments();
/** /**
* Removes errors that occur inside a string node * Removes errors that occur inside a string node
@ -188,16 +186,6 @@ module.exports = {
} }
} }
/**
* Stores a comment node (`LineComment` or `BlockComment`) for later stripping of errors within; a necessary deferring of processing to deal with top-of-file comments.
* @param {ASTNode} node The comment node
* @returns {void}
* @private
*/
function rememberCommentNode(node) {
commentNodes.push(node);
}
/** /**
* A no-op function to act as placeholder for comment accumulation when the `skipComments` option is `false`. * A no-op function to act as placeholder for comment accumulation when the `skipComments` option is `false`.
* @returns {void} * @returns {void}
@ -220,7 +208,6 @@ module.exports = {
* We can later filter the errors when they are found to be not an * We can later filter the errors when they are found to be not an
* issue in nodes we don't care about. * issue in nodes we don't care about.
*/ */
checkForIrregularWhitespace(node); checkForIrregularWhitespace(node);
checkForIrregularLineTerminators(node); checkForIrregularLineTerminators(node);
}; };
@ -228,13 +215,10 @@ module.exports = {
nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral; nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral;
nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral; nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral;
nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop; nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop;
nodes.LineComment = skipComments ? rememberCommentNode : noop;
nodes.BlockComment = skipComments ? rememberCommentNode : noop;
nodes["Program:exit"] = function() { nodes["Program:exit"] = function() {
if (skipComments) { if (skipComments) {
// First strip errors occurring in comment nodes. We have to do this post-`Program` to deal with top-of-file comments. // First strip errors occurring in comment nodes.
commentNodes.forEach(removeInvalidNodeErrorsInComment); commentNodes.forEach(removeInvalidNodeErrorsInComment);
} }

6
tools/eslint/lib/rules/no-lone-blocks.js

@ -94,13 +94,13 @@ module.exports = {
ruleDef.VariableDeclaration = function(node) { ruleDef.VariableDeclaration = function(node) {
if (node.kind === "let" || node.kind === "const") { if (node.kind === "let" || node.kind === "const") {
markLoneBlock(node); markLoneBlock();
} }
}; };
ruleDef.FunctionDeclaration = function(node) { ruleDef.FunctionDeclaration = function() {
if (context.getScope().isStrict) { if (context.getScope().isStrict) {
markLoneBlock(node); markLoneBlock();
} }
}; };

4
tools/eslint/lib/rules/no-loop-func.js

@ -90,7 +90,7 @@ function getTopLoopNode(node, excludedNode) {
* *
* @param {ASTNode} funcNode - A target function node. * @param {ASTNode} funcNode - A target function node.
* @param {ASTNode} loopNode - A containing loop node. * @param {ASTNode} loopNode - A containing loop node.
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is safe or not. * @returns {boolean} `true` if the reference is safe or not.
*/ */
function isSafe(funcNode, loopNode, reference) { function isSafe(funcNode, loopNode, reference) {
@ -131,7 +131,7 @@ function isSafe(funcNode, loopNode, reference) {
* - is readonly. * - is readonly.
* - doesn't exist inside a local function and after the border. * - doesn't exist inside a local function and after the border.
* *
* @param {escope.Reference} upperRef - A reference to check. * @param {eslint-scope.Reference} upperRef - A reference to check.
* @returns {boolean} `true` if the reference is safe. * @returns {boolean} `true` if the reference is safe.
*/ */
function isSafeReference(upperRef) { function isSafeReference(upperRef) {

93
tools/eslint/lib/rules/no-multi-spaces.js

@ -33,6 +33,9 @@ module.exports = {
} }
}, },
additionalProperties: false additionalProperties: false
},
ignoreEOLComments: {
type: "boolean"
} }
}, },
additionalProperties: false additionalProperties: false
@ -43,8 +46,10 @@ module.exports = {
create(context) { create(context) {
// the index of the last comment that was checked // the index of the last comment that was checked
const exceptions = { Property: true }, const sourceCode = context.getSourceCode(),
options = context.options[0]; exceptions = { Property: true },
options = context.options[0] || {},
ignoreEOLComments = options.ignoreEOLComments;
let hasExceptions = true, let hasExceptions = true,
lastCommentIndex = 0; lastCommentIndex = 0;
@ -59,6 +64,23 @@ module.exports = {
hasExceptions = Object.keys(exceptions).length > 0; hasExceptions = Object.keys(exceptions).length > 0;
} }
/**
* Checks if a given token is the last token of the line or not.
* @param {Token} token The token to check.
* @returns {boolean} Whether or not a token is at the end of the line it occurs in.
* @private
*/
function isLastTokenOfLine(token) {
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
// nextToken is null if the comment is the last token in the program.
if (!nextToken) {
return true;
}
return !astUtils.isTokenOnSameLine(token, nextToken);
}
/** /**
* Determines if a given source index is in a comment or not by checking * Determines if a given source index is in a comment or not by checking
* the index against the comment range. Since the check goes straight * the index against the comment range. Since the check goes straight
@ -73,7 +95,7 @@ module.exports = {
while (lastCommentIndex < comments.length) { while (lastCommentIndex < comments.length) {
const comment = comments[lastCommentIndex]; const comment = comments[lastCommentIndex];
if (comment.range[0] <= index && index < comment.range[1]) { if (comment.range[0] < index && index < comment.range[1]) {
return true; return true;
} else if (index > comment.range[1]) { } else if (index > comment.range[1]) {
lastCommentIndex++; lastCommentIndex++;
@ -85,6 +107,33 @@ module.exports = {
return false; return false;
} }
/**
* Formats value of given comment token for error message by truncating its length.
* @param {Token} token comment token
* @returns {string} formatted value
* @private
*/
function formatReportedCommentValue(token) {
const valueLines = token.value.split("\n");
const value = valueLines[0];
const formattedValue = `${value.substring(0, 12)}...`;
return valueLines.length === 1 && value.length <= 12 ? value : formattedValue;
}
/**
* Creates a fix function that removes the multiple spaces between the two tokens
* @param {Token} leftToken left token
* @param {Token} rightToken right token
* @returns {Function} fix function
* @private
*/
function createFix(leftToken, rightToken) {
return function(fixer) {
return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ");
};
}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Public // Public
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@ -92,47 +141,43 @@ module.exports = {
return { return {
Program() { Program() {
const sourceCode = context.getSourceCode(), const source = sourceCode.getText(),
source = sourceCode.getText(),
allComments = sourceCode.getAllComments(), allComments = sourceCode.getAllComments(),
JOINED_LINEBEAKS = Array.from(astUtils.LINEBREAKS).join(""), pattern = /[^\s].*? {2,}/g;
pattern = new RegExp(String.raw`[^ \t${JOINED_LINEBEAKS}].? {2,}`, "g"); // note: repeating space
let parent; let parent;
/**
* Creates a fix function that removes the multiple spaces between the two tokens
* @param {RuleFixer} leftToken left token
* @param {RuleFixer} rightToken right token
* @returns {Function} fix function
* @private
*/
function createFix(leftToken, rightToken) {
return function(fixer) {
return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ");
};
}
while (pattern.test(source)) { while (pattern.test(source)) {
// do not flag anything inside of comments // do not flag anything inside of comments
if (!isIndexInComment(pattern.lastIndex, allComments)) { if (!isIndexInComment(pattern.lastIndex, allComments)) {
const token = sourceCode.getTokenByRangeStart(pattern.lastIndex); const token = sourceCode.getTokenByRangeStart(pattern.lastIndex, { includeComments: true });
if (token) { if (token) {
const previousToken = sourceCode.getTokenBefore(token); if (ignoreEOLComments && astUtils.isCommentToken(token) && isLastTokenOfLine(token)) {
return;
}
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
if (hasExceptions) { if (hasExceptions) {
parent = sourceCode.getNodeByRangeIndex(pattern.lastIndex - 1); parent = sourceCode.getNodeByRangeIndex(pattern.lastIndex - 1);
} }
if (!parent || !exceptions[parent.type]) { if (!parent || !exceptions[parent.type]) {
let value = token.value;
if (token.type === "Block") {
value = `/*${formatReportedCommentValue(token)}*/`;
} else if (token.type === "Line") {
value = `//${formatReportedCommentValue(token)}`;
}
context.report({ context.report({
node: token, node: token,
loc: token.loc.start, loc: token.loc.start,
message: "Multiple spaces found before '{{value}}'.", message: "Multiple spaces found before '{{value}}'.",
data: { value: token.value }, data: { value },
fix: createFix(previousToken, token) fix: createFix(previousToken, token)
}); });
} }

17
tools/eslint/lib/rules/no-multiple-empty-lines.js

@ -111,10 +111,19 @@ module.exports = {
message, message,
data: { max: maxAllowed, pluralizedLines: maxAllowed === 1 ? "line" : "lines" }, data: { max: maxAllowed, pluralizedLines: maxAllowed === 1 ? "line" : "lines" },
fix(fixer) { fix(fixer) {
return fixer.removeRange([ const rangeStart = sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 });
sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 }),
sourceCode.getIndexFromLoc({ line: lineNumber - maxAllowed, column: 0 }) /*
]); * The end of the removal range is usually the start index of the next line.
* However, at the end of the file there is no next line, so the end of the
* range is just the length of the text.
*/
const lineNumberAfterRemovedLines = lineNumber - maxAllowed;
const rangeEnd = lineNumberAfterRemovedLines <= allLines.length
? sourceCode.getIndexFromLoc({ line: lineNumberAfterRemovedLines, column: 0 })
: sourceCode.text.length;
return fixer.removeRange([rangeStart, rangeEnd]);
} }
}); });
} }

2
tools/eslint/lib/rules/no-redeclare.js

@ -35,7 +35,7 @@ module.exports = {
/** /**
* Find variables in a given scope and flag redeclared ones. * Find variables in a given scope and flag redeclared ones.
* @param {Scope} scope - An escope scope object. * @param {Scope} scope - An eslint-scope scope object.
* @returns {void} * @returns {void}
* @private * @private
*/ */

21
tools/eslint/lib/rules/no-self-compare.js

@ -22,15 +22,28 @@ module.exports = {
}, },
create(context) { create(context) {
const sourceCode = context.getSourceCode();
/**
* Determines whether two nodes are composed of the same tokens.
* @param {ASTNode} nodeA The first node
* @param {ASTNode} nodeB The second node
* @returns {boolean} true if the nodes have identical token representations
*/
function hasSameTokens(nodeA, nodeB) {
const tokensA = sourceCode.getTokens(nodeA);
const tokensB = sourceCode.getTokens(nodeB);
return tokensA.length === tokensB.length &&
tokensA.every((token, index) => token.type === tokensB[index].type && token.value === tokensB[index].value);
}
return { return {
BinaryExpression(node) { BinaryExpression(node) {
const operators = ["===", "==", "!==", "!=", ">", "<", ">=", "<="]; const operators = new Set(["===", "==", "!==", "!=", ">", "<", ">=", "<="]);
if (operators.indexOf(node.operator) > -1 && if (operators.has(node.operator) && hasSameTokens(node.left, node.right)) {
(node.left.type === "Identifier" && node.right.type === "Identifier" && node.left.name === node.right.name ||
node.left.type === "Literal" && node.right.type === "Literal" && node.left.value === node.right.value)) {
context.report({ node, message: "Comparing to itself is potentially pointless." }); context.report({ node, message: "Comparing to itself is potentially pointless." });
} }
} }

6
tools/eslint/lib/rules/no-this-before-super.js

@ -89,7 +89,7 @@ module.exports = {
*/ */
function isBeforeCallOfSuper() { function isBeforeCallOfSuper() {
return ( return (
isInConstructorOfDerivedClass(funcInfo) && isInConstructorOfDerivedClass() &&
!funcInfo.codePath.currentSegments.every(isCalled) !funcInfo.codePath.currentSegments.every(isCalled)
); );
} }
@ -206,7 +206,7 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
onCodePathSegmentStart(segment) { onCodePathSegmentStart(segment) {
if (!isInConstructorOfDerivedClass(funcInfo)) { if (!isInConstructorOfDerivedClass()) {
return; return;
} }
@ -230,7 +230,7 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
onCodePathSegmentLoop(fromSegment, toSegment) { onCodePathSegmentLoop(fromSegment, toSegment) {
if (!isInConstructorOfDerivedClass(funcInfo)) { if (!isInConstructorOfDerivedClass()) {
return; return;
} }

2
tools/eslint/lib/rules/no-undefined.js

@ -36,7 +36,7 @@ module.exports = {
/** /**
* Checks the given scope for references to `undefined` and reports * Checks the given scope for references to `undefined` and reports
* all references found. * all references found.
* @param {escope.Scope} scope The scope to check. * @param {eslint-scope.Scope} scope The scope to check.
* @returns {void} * @returns {void}
*/ */
function checkScope(scope) { function checkScope(scope) {

16
tools/eslint/lib/rules/no-unexpected-multiline.js

@ -30,6 +30,9 @@ module.exports = {
const FUNCTION_MESSAGE = "Unexpected newline between function and ( of function call."; const FUNCTION_MESSAGE = "Unexpected newline between function and ( of function call.";
const PROPERTY_MESSAGE = "Unexpected newline between object and [ of property access."; const PROPERTY_MESSAGE = "Unexpected newline between object and [ of property access.";
const TAGGED_TEMPLATE_MESSAGE = "Unexpected newline between template tag and template literal."; const TAGGED_TEMPLATE_MESSAGE = "Unexpected newline between template tag and template literal.";
const DIVISION_MESSAGE = "Unexpected newline between numerator and division operator.";
const REGEX_FLAG_MATCHER = /^[gimuy]+$/;
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
@ -75,6 +78,19 @@ module.exports = {
return; return;
} }
checkForBreakAfter(node.callee, FUNCTION_MESSAGE); checkForBreakAfter(node.callee, FUNCTION_MESSAGE);
},
"BinaryExpression[operator='/'] > BinaryExpression[operator='/'].left"(node) {
const secondSlash = sourceCode.getTokenAfter(node, token => token.value === "/");
const tokenAfterOperator = sourceCode.getTokenAfter(secondSlash);
if (
tokenAfterOperator.type === "Identifier" &&
REGEX_FLAG_MATCHER.test(tokenAfterOperator.value) &&
secondSlash.range[1] === tokenAfterOperator.range[0]
) {
checkForBreakAfter(node.left, DIVISION_MESSAGE);
}
} }
}; };

18
tools/eslint/lib/rules/no-unmodified-loop-condition.js

@ -18,14 +18,14 @@ const Traverser = require("../util/traverser"),
const pushAll = Function.apply.bind(Array.prototype.push); const pushAll = Function.apply.bind(Array.prototype.push);
const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/; const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/;
const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property. const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property.
const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/; const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/;
const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/; const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/;
const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/; const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/;
/** /**
* @typedef {Object} LoopConditionInfo * @typedef {Object} LoopConditionInfo
* @property {escope.Reference} reference - The reference. * @property {eslint-scope.Reference} reference - The reference.
* @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes * @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes
* that the reference is belonging to. * that the reference is belonging to.
* @property {Function} isInLoop - The predicate which checks a given reference * @property {Function} isInLoop - The predicate which checks a given reference
@ -37,7 +37,7 @@ const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/;
/** /**
* Checks whether or not a given reference is a write reference. * Checks whether or not a given reference is a write reference.
* *
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is a write reference. * @returns {boolean} `true` if the reference is a write reference.
*/ */
function isWriteReference(reference) { function isWriteReference(reference) {
@ -77,7 +77,7 @@ function isUnmodifiedAndNotBelongToGroup(condition) {
* Checks whether or not a given reference is inside of a given node. * Checks whether or not a given reference is inside of a given node.
* *
* @param {ASTNode} node - A node to check. * @param {ASTNode} node - A node to check.
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is inside of the node. * @returns {boolean} `true` if the reference is inside of the node.
*/ */
function isInRange(node, reference) { function isInRange(node, reference) {
@ -91,7 +91,7 @@ function isInRange(node, reference) {
* Checks whether or not a given reference is inside of a loop node's condition. * Checks whether or not a given reference is inside of a loop node's condition.
* *
* @param {ASTNode} node - A node to check. * @param {ASTNode} node - A node to check.
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is inside of the loop node's * @returns {boolean} `true` if the reference is inside of the loop node's
* condition. * condition.
*/ */
@ -134,7 +134,7 @@ function hasDynamicExpressions(root) {
/** /**
* Creates the loop condition information from a given reference. * Creates the loop condition information from a given reference.
* *
* @param {escope.Reference} reference - A reference to create. * @param {eslint-scope.Reference} reference - A reference to create.
* @returns {LoopConditionInfo|null} Created loop condition info, or null. * @returns {LoopConditionInfo|null} Created loop condition info, or null.
*/ */
function toLoopCondition(reference) { function toLoopCondition(reference) {
@ -188,7 +188,7 @@ function toLoopCondition(reference) {
* Gets the function which encloses a given reference. * Gets the function which encloses a given reference.
* This supports only FunctionDeclaration. * This supports only FunctionDeclaration.
* *
* @param {escope.Reference} reference - A reference to get. * @param {eslint-scope.Reference} reference - A reference to get.
* @returns {ASTNode|null} The function node or null. * @returns {ASTNode|null} The function node or null.
*/ */
function getEncloseFunctionDeclaration(reference) { function getEncloseFunctionDeclaration(reference) {
@ -209,7 +209,7 @@ function getEncloseFunctionDeclaration(reference) {
* Updates the "modified" flags of given loop conditions with given modifiers. * Updates the "modified" flags of given loop conditions with given modifiers.
* *
* @param {LoopConditionInfo[]} conditions - The loop conditions to be updated. * @param {LoopConditionInfo[]} conditions - The loop conditions to be updated.
* @param {escope.Reference[]} modifiers - The references to update. * @param {eslint-scope.Reference[]} modifiers - The references to update.
* @returns {void} * @returns {void}
*/ */
function updateModifiedFlag(conditions, modifiers) { function updateModifiedFlag(conditions, modifiers) {
@ -311,7 +311,7 @@ module.exports = {
* Finds unmodified references which are inside of a loop condition. * Finds unmodified references which are inside of a loop condition.
* Then reports the references which are outside of groups. * Then reports the references which are outside of groups.
* *
* @param {escope.Variable} variable - A variable to report. * @param {eslint-scope.Variable} variable - A variable to report.
* @returns {void} * @returns {void}
*/ */
function checkReferences(variable) { function checkReferences(variable) {

12
tools/eslint/lib/rules/no-unneeded-ternary.js

@ -134,7 +134,17 @@ module.exports = {
node, node,
loc: node.consequent.loc.start, loc: node.consequent.loc.start,
message: "Unnecessary use of conditional expression for default assignment.", message: "Unnecessary use of conditional expression for default assignment.",
fix: fixer => fixer.replaceText(node, `${astUtils.getParenthesisedText(sourceCode, node.test)} || ${astUtils.getParenthesisedText(sourceCode, node.alternate)}`) fix: fixer => {
let nodeAlternate = astUtils.getParenthesisedText(sourceCode, node.alternate);
if (node.alternate.type === "ConditionalExpression") {
const isAlternateParenthesised = astUtils.isParenthesised(sourceCode, node.alternate);
nodeAlternate = isAlternateParenthesised ? nodeAlternate : `(${nodeAlternate})`;
}
return fixer.replaceText(node, `${astUtils.getParenthesisedText(sourceCode, node.test)} || ${nodeAlternate}`);
}
}); });
} }
} }

20
tools/eslint/lib/rules/no-unused-vars.js

@ -108,7 +108,7 @@ module.exports = {
/** /**
* Determines if a given variable is being exported from a module. * Determines if a given variable is being exported from a module.
* @param {Variable} variable - EScope variable object. * @param {Variable} variable - eslint-scope variable object.
* @returns {boolean} True if the variable is exported, false if not. * @returns {boolean} True if the variable is exported, false if not.
* @private * @private
*/ */
@ -134,7 +134,7 @@ module.exports = {
/** /**
* Determines if a variable has a sibling rest property * Determines if a variable has a sibling rest property
* @param {Variable} variable - EScope variable object. * @param {Variable} variable - eslint-scope variable object.
* @returns {boolean} True if the variable is exported, false if not. * @returns {boolean} True if the variable is exported, false if not.
* @private * @private
*/ */
@ -157,7 +157,7 @@ module.exports = {
/** /**
* Determines if a reference is a read operation. * Determines if a reference is a read operation.
* @param {Reference} ref - An escope Reference * @param {Reference} ref - An eslint-scope Reference
* @returns {boolean} whether the given reference represents a read operation * @returns {boolean} whether the given reference represents a read operation
* @private * @private
*/ */
@ -212,7 +212,7 @@ module.exports = {
* - The reference is inside of a function scope which is different from * - The reference is inside of a function scope which is different from
* the declaration. * the declaration.
* *
* @param {escope.Reference} ref - A reference to check. * @param {eslint-scope.Reference} ref - A reference to check.
* @param {ASTNode} prevRhsNode - The previous RHS node. * @param {ASTNode} prevRhsNode - The previous RHS node.
* @returns {ASTNode|null} The RHS node or null. * @returns {ASTNode|null} The RHS node or null.
* @private * @private
@ -322,7 +322,7 @@ module.exports = {
/** /**
* Checks whether a given reference is a read to update itself or not. * Checks whether a given reference is a read to update itself or not.
* *
* @param {escope.Reference} ref - A reference to check. * @param {eslint-scope.Reference} ref - A reference to check.
* @param {ASTNode} rhsNode - The RHS node of the previous assignment. * @param {ASTNode} rhsNode - The RHS node of the previous assignment.
* @returns {boolean} The reference is a read to update itself. * @returns {boolean} The reference is a read to update itself.
* @private * @private
@ -422,7 +422,7 @@ module.exports = {
/** /**
* Checks whether the given variable is the last parameter in the non-ignored parameters. * Checks whether the given variable is the last parameter in the non-ignored parameters.
* *
* @param {escope.Variable} variable - The variable to check. * @param {eslint-scope.Variable} variable - The variable to check.
* @returns {boolean} `true` if the variable is the last. * @returns {boolean} `true` if the variable is the last.
*/ */
function isLastInNonIgnoredParameters(variable) { function isLastInNonIgnoredParameters(variable) {
@ -448,7 +448,7 @@ module.exports = {
/** /**
* Gets an array of variables without read references. * Gets an array of variables without read references.
* @param {Scope} scope - an escope Scope object. * @param {Scope} scope - an eslint-scope Scope object.
* @param {Variable[]} unusedVars - an array that saving result. * @param {Variable[]} unusedVars - an array that saving result.
* @returns {Variable[]} unused variables of the scope and descendant scopes. * @returns {Variable[]} unused variables of the scope and descendant scopes.
* @private * @private
@ -513,7 +513,7 @@ module.exports = {
} }
// if "args" option is "after-used", skip all but the last parameter // if "args" option is "after-used", skip all but the last parameter
if (config.args === "after-used" && !isLastInNonIgnoredParameters(variable)) { if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isLastInNonIgnoredParameters(variable)) {
continue; continue;
} }
} else { } else {
@ -540,7 +540,7 @@ module.exports = {
/** /**
* Gets the index of a given variable name in a given comment. * Gets the index of a given variable name in a given comment.
* @param {escope.Variable} variable - A variable to get. * @param {eslint-scope.Variable} variable - A variable to get.
* @param {ASTNode} comment - A comment node which includes the variable name. * @param {ASTNode} comment - A comment node which includes the variable name.
* @returns {number} The index of the variable name's location. * @returns {number} The index of the variable name's location.
* @private * @private
@ -561,7 +561,7 @@ module.exports = {
* Creates the correct location of a given variables. * Creates the correct location of a given variables.
* The location is at its name string in a `/*global` comment. * The location is at its name string in a `/*global` comment.
* *
* @param {escope.Variable} variable - A variable to get its location. * @param {eslint-scope.Variable} variable - A variable to get its location.
* @returns {{line: number, column: number}} The location object for the variable. * @returns {{line: number, column: number}} The location object for the variable.
* @private * @private
*/ */

16
tools/eslint/lib/rules/no-use-before-define.js

@ -37,7 +37,7 @@ function parseOptions(options) {
/** /**
* Checks whether or not a given variable is a function declaration. * Checks whether or not a given variable is a function declaration.
* *
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is a function declaration. * @returns {boolean} `true` if the variable is a function declaration.
*/ */
function isFunction(variable) { function isFunction(variable) {
@ -47,8 +47,8 @@ function isFunction(variable) {
/** /**
* Checks whether or not a given variable is a class declaration in an upper function scope. * Checks whether or not a given variable is a class declaration in an upper function scope.
* *
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a class declaration. * @returns {boolean} `true` if the variable is a class declaration.
*/ */
function isOuterClass(variable, reference) { function isOuterClass(variable, reference) {
@ -60,8 +60,8 @@ function isOuterClass(variable, reference) {
/** /**
* Checks whether or not a given variable is a variable declaration in an upper function scope. * Checks whether or not a given variable is a variable declaration in an upper function scope.
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a variable declaration. * @returns {boolean} `true` if the variable is a variable declaration.
*/ */
function isOuterVariable(variable, reference) { function isOuterVariable(variable, reference) {
@ -167,8 +167,8 @@ module.exports = {
/** /**
* Determines whether a given use-before-define case should be reported according to the options. * Determines whether a given use-before-define case should be reported according to the options.
* @param {escope.Variable} variable The variable that gets used before being defined * @param {eslint-scope.Variable} variable The variable that gets used before being defined
* @param {escope.Reference} reference The reference to the variable * @param {eslint-scope.Reference} reference The reference to the variable
* @returns {boolean} `true` if the usage should be reported * @returns {boolean} `true` if the usage should be reported
*/ */
function isForbidden(variable, reference) { function isForbidden(variable, reference) {
@ -250,7 +250,7 @@ module.exports = {
ruleDefinition["ArrowFunctionExpression:exit"] = function(node) { ruleDefinition["ArrowFunctionExpression:exit"] = function(node) {
if (node.body.type !== "BlockStatement") { if (node.body.type !== "BlockStatement") {
findVariables(node); findVariables();
} }
}; };
} else { } else {

4
tools/eslint/lib/rules/no-useless-computed-key.js

@ -9,7 +9,6 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const astUtils = require("../ast-utils"); const astUtils = require("../ast-utils");
const esUtils = require("esutils");
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Rule Definition // Rule Definition
@ -61,8 +60,7 @@ module.exports = {
// Insert a space before the key to avoid changing identifiers, e.g. ({ get[2]() {} }) to ({ get2() {} }) // Insert a space before the key to avoid changing identifiers, e.g. ({ get[2]() {} }) to ({ get2() {} })
const needsSpaceBeforeKey = tokenBeforeLeftBracket.range[1] === leftSquareBracket.range[0] && const needsSpaceBeforeKey = tokenBeforeLeftBracket.range[1] === leftSquareBracket.range[0] &&
esUtils.code.isIdentifierPartES6(tokenBeforeLeftBracket.value.slice(-1).charCodeAt(0)) && !astUtils.canTokensBeAdjacent(tokenBeforeLeftBracket, sourceCode.getFirstToken(key));
esUtils.code.isIdentifierPartES6(key.raw.charCodeAt(0));
const replacementKey = (needsSpaceBeforeKey ? " " : "") + key.raw; const replacementKey = (needsSpaceBeforeKey ? " " : "") + key.raw;

2
tools/eslint/lib/rules/no-useless-escape.js

@ -75,7 +75,7 @@ module.exports = {
docs: { docs: {
description: "disallow unnecessary escape characters", description: "disallow unnecessary escape characters",
category: "Best Practices", category: "Best Practices",
recommended: false recommended: true
}, },
schema: [] schema: []

10
tools/eslint/lib/rules/no-var.js

@ -19,8 +19,8 @@ const astUtils = require("../ast-utils");
* Finds the nearest function scope or global scope walking up the scope * Finds the nearest function scope or global scope walking up the scope
* hierarchy. * hierarchy.
* *
* @param {escope.Scope} scope - The scope to traverse. * @param {eslint-scope.Scope} scope - The scope to traverse.
* @returns {escope.Scope} a function scope or global scope containing the given * @returns {eslint-scope.Scope} a function scope or global scope containing the given
* scope. * scope.
*/ */
function getEnclosingFunctionScope(scope) { function getEnclosingFunctionScope(scope) {
@ -34,7 +34,7 @@ function getEnclosingFunctionScope(scope) {
* Checks whether the given variable has any references from a more specific * Checks whether the given variable has any references from a more specific
* function expression (i.e. a closure). * function expression (i.e. a closure).
* *
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is used from a closure. * @returns {boolean} `true` if the variable is used from a closure.
*/ */
function isReferencedInClosure(variable) { function isReferencedInClosure(variable) {
@ -93,7 +93,7 @@ function getScopeNode(node) {
/** /**
* Checks whether a given variable is redeclared or not. * Checks whether a given variable is redeclared or not.
* *
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is redeclared. * @returns {boolean} `true` if the variable is redeclared.
*/ */
function isRedeclared(variable) { function isRedeclared(variable) {
@ -112,7 +112,7 @@ function isUsedFromOutsideOf(scopeNode) {
/** /**
* Checks whether a given reference is inside of the specified scope or not. * Checks whether a given reference is inside of the specified scope or not.
* *
* @param {escope.Reference} reference - A reference to check. * @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is inside of the specified * @returns {boolean} `true` if the reference is inside of the specified
* scope. * scope.
*/ */

10
tools/eslint/lib/rules/no-warning-comments.js

@ -40,7 +40,8 @@ module.exports = {
create(context) { create(context) {
const configuration = context.options[0] || {}, const sourceCode = context.getSourceCode(),
configuration = context.options[0] || {},
warningTerms = configuration.terms || ["todo", "fixme", "xxx"], warningTerms = configuration.terms || ["todo", "fixme", "xxx"],
location = configuration.location || "start", location = configuration.location || "start",
selfConfigRegEx = /\bno-warning-comments\b/; selfConfigRegEx = /\bno-warning-comments\b/;
@ -128,8 +129,11 @@ module.exports = {
} }
return { return {
BlockComment: checkComment, Program() {
LineComment: checkComment const comments = sourceCode.getAllComments();
comments.filter(token => token.type !== "Shebang").forEach(checkComment);
}
}; };
} }
}; };

23
tools/eslint/lib/rules/object-curly-newline.js

@ -30,6 +30,9 @@ const OPTION_VALUE = {
minProperties: { minProperties: {
type: "integer", type: "integer",
minimum: 0 minimum: 0
},
consistent: {
type: "boolean"
} }
}, },
additionalProperties: false, additionalProperties: false,
@ -42,11 +45,12 @@ const OPTION_VALUE = {
* Normalizes a given option value. * Normalizes a given option value.
* *
* @param {string|Object|undefined} value - An option value to parse. * @param {string|Object|undefined} value - An option value to parse.
* @returns {{multiline: boolean, minProperties: number}} Normalized option object. * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object.
*/ */
function normalizeOptionValue(value) { function normalizeOptionValue(value) {
let multiline = false; let multiline = false;
let minProperties = Number.POSITIVE_INFINITY; let minProperties = Number.POSITIVE_INFINITY;
let consistent = false;
if (value) { if (value) {
if (value === "always") { if (value === "always") {
@ -56,12 +60,13 @@ function normalizeOptionValue(value) {
} else { } else {
multiline = Boolean(value.multiline); multiline = Boolean(value.multiline);
minProperties = value.minProperties || Number.POSITIVE_INFINITY; minProperties = value.minProperties || Number.POSITIVE_INFINITY;
consistent = Boolean(value.consistent);
} }
} else { } else {
multiline = true; multiline = true;
} }
return { multiline, minProperties }; return { multiline, minProperties, consistent };
} }
/** /**
@ -172,7 +177,14 @@ module.exports = {
}); });
} }
} else { } else {
if (!astUtils.isTokenOnSameLine(openBrace, first)) { const consistent = options.consistent;
const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first);
const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
if (
(!consistent && hasLineBreakBetweenOpenBraceAndFirst) ||
(consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast)
) {
context.report({ context.report({
message: "Unexpected line break after this opening brace.", message: "Unexpected line break after this opening brace.",
node, node,
@ -185,7 +197,10 @@ module.exports = {
} }
}); });
} }
if (!astUtils.isTokenOnSameLine(last, closeBrace)) { if (
(!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
(consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
) {
context.report({ context.report({
message: "Unexpected line break before this closing brace.", message: "Unexpected line break before this closing brace.",
node, node,

2
tools/eslint/lib/rules/object-shorthand.js

@ -392,7 +392,7 @@ module.exports = {
}); });
} }
} else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) { } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) {
if (IGNORE_CONSTRUCTORS && isConstructor(node.key.name)) { if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
return; return;
} }
if (AVOID_QUOTES && isStringLiteral(node.key)) { if (AVOID_QUOTES && isStringLiteral(node.key)) {

6
tools/eslint/lib/rules/padded-blocks.js

@ -51,7 +51,11 @@ module.exports = {
const config = context.options[0] || "always"; const config = context.options[0] || "always";
if (typeof config === "string") { if (typeof config === "string") {
options.blocks = config === "always"; const shouldHavePadding = config === "always";
options.blocks = shouldHavePadding;
options.switches = shouldHavePadding;
options.classes = shouldHavePadding;
} else { } else {
if (config.hasOwnProperty("blocks")) { if (config.hasOwnProperty("blocks")) {
options.blocks = config.blocks === "always"; options.blocks = config.blocks === "always";

587
tools/eslint/lib/rules/padding-line-between-statements.js

@ -0,0 +1,587 @@
/**
* @fileoverview Rule to require or disallow newlines between statements
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`;
const PADDING_LINE_SEQUENCE = new RegExp(
String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`
);
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/;
const CJS_IMPORT = /^require\(/;
/**
* Creates tester which check if a node starts with specific keyword.
*
* @param {string} keyword The keyword to test.
* @returns {Object} the created tester.
* @private
*/
function newKeywordTester(keyword) {
return {
test: (node, sourceCode) =>
sourceCode.getFirstToken(node).value === keyword
};
}
/**
* Creates tester which check if a node is specific type.
*
* @param {string} type The node type to test.
* @returns {Object} the created tester.
* @private
*/
function newNodeTypeTester(type) {
return {
test: node =>
node.type === type
};
}
/**
* Checks the given node is an expression statement of IIFE.
*
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is an expression statement of IIFE.
* @private
*/
function isIIFEStatement(node) {
if (node.type === "ExpressionStatement") {
let call = node.expression;
if (call.type === "UnaryExpression") {
call = call.argument;
}
return call.type === "CallExpression" && astUtils.isFunction(call.callee);
}
return false;
}
/**
* Checks whether the given node is a block-like statement.
* This checks the last token of the node is the closing brace of a block.
*
* @param {SourceCode} sourceCode The source code to get tokens.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is a block-like statement.
* @private
*/
function isBlockLikeStatement(sourceCode, node) {
// do-while with a block is a block-like statement.
if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") {
return true;
}
// IIFE is a block-like statement specially from
// JSCS#disallowPaddingNewLinesAfterBlocks.
if (isIIFEStatement(node)) {
return true;
}
// Checks the last token is a closing brace of blocks.
const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
const belongingNode = astUtils.isClosingBraceToken(lastToken)
? sourceCode.getNodeByRangeIndex(lastToken.range[0])
: null;
return Boolean(belongingNode) && (
belongingNode.type === "BlockStatement" ||
belongingNode.type === "SwitchStatement"
);
}
/**
* Check whether the given node is a directive or not.
* @param {ASTNode} node The node to check.
* @param {SourceCode} sourceCode The source code object to get tokens.
* @returns {boolean} `true` if the node is a directive.
*/
function isDirective(node, sourceCode) {
return (
node.type === "ExpressionStatement" &&
(
node.parent.type === "Program" ||
(
node.parent.type === "BlockStatement" &&
astUtils.isFunction(node.parent.parent)
)
) &&
node.expression.type === "Literal" &&
typeof node.expression.value === "string" &&
!astUtils.isParenthesised(sourceCode, node.expression)
);
}
/**
* Check whether the given node is a part of directive prologue or not.
* @param {ASTNode} node The node to check.
* @param {SourceCode} sourceCode The source code object to get tokens.
* @returns {boolean} `true` if the node is a part of directive prologue.
*/
function isDirectivePrologue(node, sourceCode) {
if (isDirective(node, sourceCode)) {
for (const sibling of node.parent.body) {
if (sibling === node) {
break;
}
if (!isDirective(sibling, sourceCode)) {
return false;
}
}
return true;
}
return false;
}
/**
* Gets the actual last token.
*
* If a semicolon is semicolon-less style's semicolon, this ignores it.
* For example:
*
* foo()
* ;[1, 2, 3].forEach(bar)
*
* @param {SourceCode} sourceCode The source code to get tokens.
* @param {ASTNode} node The node to get.
* @returns {Token} The actual last token.
* @private
*/
function getActualLastToken(sourceCode, node) {
const semiToken = sourceCode.getLastToken(node);
const prevToken = sourceCode.getTokenBefore(semiToken);
const nextToken = sourceCode.getTokenAfter(semiToken);
const isSemicolonLessStyle = Boolean(
prevToken &&
nextToken &&
prevToken.range[0] >= node.range[0] &&
astUtils.isSemicolonToken(semiToken) &&
semiToken.loc.start.line !== prevToken.loc.end.line &&
semiToken.loc.end.line === nextToken.loc.start.line
);
return isSemicolonLessStyle ? prevToken : semiToken;
}
/**
* This returns the concatenation of the first 2 captured strings.
* @param {string} _ Unused. Whole matched string.
* @param {string} trailingSpaces The trailing spaces of the first line.
* @param {string} indentSpaces The indentation spaces of the last line.
* @returns {string} The concatenation of trailingSpaces and indentSpaces.
* @private
*/
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
return trailingSpaces + indentSpaces;
}
/**
* Check and report statements for `any` configuration.
* It does nothing.
*
* @returns {void}
* @private
*/
function verifyForAny() {
}
/**
* Check and report statements for `never` configuration.
* This autofix removes blank lines between the given 2 statements.
* However, if comments exist between 2 blank lines, it does not remove those
* blank lines automatically.
*
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} prevNode The previous node to check.
* @param {ASTNode} nextNode The next node to check.
* @param {Array<Token[]>} paddingLines The array of token pairs that blank
* lines exist between the pair.
* @returns {void}
* @private
*/
function verifyForNever(context, prevNode, nextNode, paddingLines) {
if (paddingLines.length === 0) {
return;
}
context.report({
node: nextNode,
message: "Unexpected blank line before this statement.",
fix(fixer) {
if (paddingLines.length >= 2) {
return null;
}
const prevToken = paddingLines[0][0];
const nextToken = paddingLines[0][1];
const start = prevToken.range[1];
const end = nextToken.range[0];
const text = context.getSourceCode().text
.slice(start, end)
.replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
return fixer.replaceTextRange([start, end], text);
}
});
}
/**
* Check and report statements for `always` configuration.
* This autofix inserts a blank line between the given 2 statements.
* If the `prevNode` has trailing comments, it inserts a blank line after the
* trailing comments.
*
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} prevNode The previous node to check.
* @param {ASTNode} nextNode The next node to check.
* @param {Array<Token[]>} paddingLines The array of token pairs that blank
* lines exist between the pair.
* @returns {void}
* @private
*/
function verifyForAlways(context, prevNode, nextNode, paddingLines) {
if (paddingLines.length > 0) {
return;
}
context.report({
node: nextNode,
message: "Expected blank line before this statement.",
fix(fixer) {
const sourceCode = context.getSourceCode();
let prevToken = getActualLastToken(sourceCode, prevNode);
const nextToken = sourceCode.getFirstTokenBetween(
prevToken,
nextNode,
{
includeComments: true,
/**
* Skip the trailing comments of the previous node.
* This inserts a blank line after the last trailing comment.
*
* For example:
*
* foo(); // trailing comment.
* // comment.
* bar();
*
* Get fixed to:
*
* foo(); // trailing comment.
*
* // comment.
* bar();
*
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is not a trailing comment.
* @private
*/
filter(token) {
if (astUtils.isTokenOnSameLine(prevToken, token)) {
prevToken = token;
return false;
}
return true;
}
}
) || nextNode;
const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken)
? "\n\n"
: "\n";
return fixer.insertTextAfter(prevToken, insertText);
}
});
}
/**
* Types of blank lines.
* `any`, `never`, and `always` are defined.
* Those have `verify` method to check and report statements.
* @private
*/
const PaddingTypes = {
any: { verify: verifyForAny },
never: { verify: verifyForNever },
always: { verify: verifyForAlways }
};
/**
* Types of statements.
* Those have `test` method to check it matches to the given statement.
* @private
*/
const StatementTypes = {
"*": { test: () => true },
"block-like": {
test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node)
},
"cjs-export": {
test: (node, sourceCode) =>
node.type === "ExpressionStatement" &&
node.expression.type === "AssignmentExpression" &&
CJS_EXPORT.test(sourceCode.getText(node.expression.left))
},
"cjs-import": {
test: (node, sourceCode) =>
node.type === "VariableDeclaration" &&
node.declarations.length > 0 &&
Boolean(node.declarations[0].init) &&
CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))
},
directive: {
test: isDirectivePrologue
},
expression: {
test: (node, sourceCode) =>
node.type === "ExpressionStatement" &&
!isDirectivePrologue(node, sourceCode)
},
"multiline-block-like": {
test: (node, sourceCode) =>
node.loc.start.line !== node.loc.end.line &&
isBlockLikeStatement(sourceCode, node)
},
block: newNodeTypeTester("BlockStatement"),
empty: newNodeTypeTester("EmptyStatement"),
break: newKeywordTester("break"),
case: newKeywordTester("case"),
class: newKeywordTester("class"),
const: newKeywordTester("const"),
continue: newKeywordTester("continue"),
debugger: newKeywordTester("debugger"),
default: newKeywordTester("default"),
do: newKeywordTester("do"),
export: newKeywordTester("export"),
for: newKeywordTester("for"),
function: newKeywordTester("function"),
if: newKeywordTester("if"),
import: newKeywordTester("import"),
let: newKeywordTester("let"),
return: newKeywordTester("return"),
switch: newKeywordTester("switch"),
throw: newKeywordTester("throw"),
try: newKeywordTester("try"),
var: newKeywordTester("var"),
while: newKeywordTester("while"),
with: newKeywordTester("with")
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow padding lines between statements",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: {
definitions: {
paddingType: {
enum: Object.keys(PaddingTypes)
},
statementType: {
anyOf: [
{ enum: Object.keys(StatementTypes) },
{
type: "array",
items: { enum: Object.keys(StatementTypes) },
minItems: 1,
uniqueItems: true,
additionalItems: false
}
]
}
},
type: "array",
items: {
type: "object",
properties: {
blankLine: { $ref: "#/definitions/paddingType" },
prev: { $ref: "#/definitions/statementType" },
next: { $ref: "#/definitions/statementType" }
},
additionalProperties: false,
required: ["blankLine", "prev", "next"]
},
additionalItems: false
}
},
create(context) {
const sourceCode = context.getSourceCode();
const configureList = context.options || [];
let scopeInfo = null;
/**
* Processes to enter to new scope.
* This manages the current previous statement.
* @returns {void}
* @private
*/
function enterScope() {
scopeInfo = {
upper: scopeInfo,
prevNode: null
};
}
/**
* Processes to exit from the current scope.
* @returns {void}
* @private
*/
function exitScope() {
scopeInfo = scopeInfo.upper;
}
/**
* Checks whether the given node matches the given type.
*
* @param {ASTNode} node The statement node to check.
* @param {string|string[]} type The statement type to check.
* @returns {boolean} `true` if the statement node matched the type.
* @private
*/
function match(node, type) {
while (node.type === "LabeledStatement") {
node = node.body;
}
if (Array.isArray(type)) {
return type.some(match.bind(null, node));
}
return StatementTypes[type].test(node, sourceCode);
}
/**
* Finds the last matched configure from configureList.
*
* @param {ASTNode} prevNode The previous statement to match.
* @param {ASTNode} nextNode The current statement to match.
* @returns {Object} The tester of the last matched configure.
* @private
*/
function getPaddingType(prevNode, nextNode) {
for (let i = configureList.length - 1; i >= 0; --i) {
const configure = configureList[i];
const matched =
match(prevNode, configure.prev) &&
match(nextNode, configure.next);
if (matched) {
return PaddingTypes[configure.blankLine];
}
}
return PaddingTypes.any;
}
/**
* Gets padding line sequences between the given 2 statements.
* Comments are separators of the padding line sequences.
*
* @param {ASTNode} prevNode The previous statement to count.
* @param {ASTNode} nextNode The current statement to count.
* @returns {Array<Token[]>} The array of token pairs.
* @private
*/
function getPaddingLineSequences(prevNode, nextNode) {
const pairs = [];
let prevToken = getActualLastToken(sourceCode, prevNode);
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
do {
const token = sourceCode.getTokenAfter(
prevToken,
{ includeComments: true }
);
if (token.loc.start.line - prevToken.loc.end.line >= 2) {
pairs.push([prevToken, token]);
}
prevToken = token;
} while (prevToken.range[0] < nextNode.range[0]);
}
return pairs;
}
/**
* Verify padding lines between the given node and the previous node.
*
* @param {ASTNode} node The node to verify.
* @returns {void}
* @private
*/
function verify(node) {
const parentType = node.parent.type;
const validParent =
astUtils.STATEMENT_LIST_PARENTS.has(parentType) ||
parentType === "SwitchStatement";
if (!validParent) {
return;
}
// Save this node as the current previous statement.
const prevNode = scopeInfo.prevNode;
// Verify.
if (prevNode) {
const type = getPaddingType(prevNode, node);
const paddingLines = getPaddingLineSequences(prevNode, node);
type.verify(context, prevNode, node, paddingLines);
}
scopeInfo.prevNode = node;
}
/**
* Verify padding lines between the given node and the previous node.
* Then process to enter to new scope.
*
* @param {ASTNode} node The node to verify.
* @returns {void}
* @private
*/
function verifyThenEnterScope(node) {
verify(node);
enterScope();
}
return {
Program: enterScope,
BlockStatement: enterScope,
SwitchStatement: enterScope,
"Program:exit": exitScope,
"BlockStatement:exit": exitScope,
"SwitchStatement:exit": exitScope,
":statement": verify,
SwitchCase: verifyThenEnterScope,
"SwitchCase:exit": exitScope
};
}
};

34
tools/eslint/lib/rules/prefer-arrow-callback.js

@ -11,7 +11,7 @@
/** /**
* Checks whether or not a given variable is a function name. * Checks whether or not a given variable is a function name.
* @param {escope.Variable} variable - A variable to check. * @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is a function name. * @returns {boolean} `true` if the variable is a function name.
*/ */
function isFunctionName(variable) { function isFunctionName(variable) {
@ -31,8 +31,8 @@ function checkMetaProperty(node, metaName, propertyName) {
/** /**
* Gets the variable object of `arguments` which is defined implicitly. * Gets the variable object of `arguments` which is defined implicitly.
* @param {escope.Scope} scope - A scope to get. * @param {eslint-scope.Scope} scope - A scope to get.
* @returns {escope.Variable} The found variable object. * @returns {eslint-scope.Variable} The found variable object.
*/ */
function getVariableOfArguments(scope) { function getVariableOfArguments(scope) {
const variables = scope.variables; const variables = scope.variables;
@ -159,7 +159,7 @@ module.exports = {
create(context) { create(context) {
const options = context.options[0] || {}; const options = context.options[0] || {};
const allowUnboundThis = options.allowUnboundThis !== false; // default to true const allowUnboundThis = options.allowUnboundThis !== false; // default to true
const allowNamedFunctions = options.allowNamedFunctions; const allowNamedFunctions = options.allowNamedFunctions;
const sourceCode = context.getSourceCode(); const sourceCode = context.getSourceCode();
@ -277,15 +277,23 @@ module.exports = {
const paramsRightParen = sourceCode.getTokenBefore(node.body); const paramsRightParen = sourceCode.getTokenBefore(node.body);
const asyncKeyword = node.async ? "async " : ""; const asyncKeyword = node.async ? "async " : "";
const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]); const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
if (callbackInfo.isLexicalThis) {
/*
// If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding. * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
return fixer.replaceText(node.parent.parent, `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`); * Otherwise, just replace the arrow function itself.
} */
const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
// Otherwise, only replace the `function` keyword and parameters with the arrow function parameters.
return fixer.replaceTextRange([node.start, node.body.start], `${asyncKeyword}${paramsFullText} => `); /*
* If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
* the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
* though `foo || function() {}` is valid.
*/
const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
return fixer.replaceText(replacedNode, replacementText);
} }
}); });
} }

19
tools/eslint/lib/rules/prefer-const.js

@ -73,7 +73,7 @@ function canBecomeVariableDeclaration(identifier) {
* warn such variables because this rule cannot distinguish whether the * warn such variables because this rule cannot distinguish whether the
* exported variables are reassigned or not. * exported variables are reassigned or not.
* *
* @param {escope.Variable} variable - A variable to get. * @param {eslint-scope.Variable} variable - A variable to get.
* @param {boolean} ignoreReadBeforeAssign - * @param {boolean} ignoreReadBeforeAssign -
* The value of `ignoreReadBeforeAssign` option. * The value of `ignoreReadBeforeAssign` option.
* @returns {ASTNode|null} * @returns {ASTNode|null}
@ -85,17 +85,6 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
return null; return null;
} }
/*
* Due to a bug in acorn, code such as `let foo = 1; let foo = 2;` will not throw a syntax error. As a sanity
* check, make sure that the variable only has one declaration. After the parsing bug is fixed, this check
* will no longer be necessary, because variables declared with `let` or `const` should always have exactly one
* declaration.
* https://github.com/ternjs/acorn/issues/487
*/
if (variable.defs.length > 1) {
return null;
}
// Finds the unique WriteReference. // Finds the unique WriteReference.
let writer = null; let writer = null;
let isReadBeforeInit = false; let isReadBeforeInit = false;
@ -146,7 +135,7 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
* This is used to detect a mix of reassigned and never reassigned in a * This is used to detect a mix of reassigned and never reassigned in a
* destructuring. * destructuring.
* *
* @param {escope.Reference} reference - A reference to get. * @param {eslint-scope.Reference} reference - A reference to get.
* @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
* null. * null.
*/ */
@ -172,7 +161,7 @@ function getDestructuringHost(reference) {
* This is used to detect a mix of reassigned and never reassigned in a * This is used to detect a mix of reassigned and never reassigned in a
* destructuring. * destructuring.
* *
* @param {escope.Variable[]} variables - Variables to group by destructuring. * @param {eslint-scope.Variable[]} variables - Variables to group by destructuring.
* @param {boolean} ignoreReadBeforeAssign - * @param {boolean} ignoreReadBeforeAssign -
* The value of `ignoreReadBeforeAssign` option. * The value of `ignoreReadBeforeAssign` option.
* @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes. * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
@ -274,7 +263,7 @@ module.exports = {
* the array is 1. In destructuring cases, the length of the array can * the array is 1. In destructuring cases, the length of the array can
* be 2 or more. * be 2 or more.
* *
* @param {(escope.Reference|null)[]} nodes - * @param {(eslint-scope.Reference|null)[]} nodes -
* References which are grouped by destructuring to report. * References which are grouped by destructuring to report.
* @returns {void} * @returns {void}
*/ */

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

Loading…
Cancel
Save