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]
no-fallthrough: 2
no-global-assign: 2
no-multi-spaces: 2
no-multi-spaces: [2, {ignoreEOLComments: true}]
no-octal: 2
no-redeclare: 2
no-self-assign: 2
@ -93,11 +93,18 @@ rules:
func-call-spacing: 2
func-name-matching: 2
func-style: [2, declaration, {allowArrowFunctions: true}]
indent: [2, 2, {ArrayExpression: first,
CallExpression: {arguments: first},
MemberExpression: 1,
ObjectExpression: first,
SwitchCase: 1}]
# indent: [2, 2, {ArrayExpression: first,
# CallExpression: {arguments: first},
# FunctionDeclaration: {parameters: first},
# FunctionExpression: {parameters: first},
# 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}]
keyword-spacing: 2
linebreak-style: [2, unix]
@ -117,10 +124,10 @@ rules:
}, {
selector: "ThrowStatement > CallExpression[callee.name=/Error$/]",
message: "Use new keyword when throwing an Error."
}, {
}, {
selector: "CallExpression[callee.object.name='assert'][callee.property.name='fail'][arguments.length=1]",
message: "assert.fail() message should be third argument"
}]
}]
no-tabs: 2
no-trailing-spaces: 2
operator-linebreak: [2, after, {overrides: {'?': ignore, ':': ignore}}]
@ -128,7 +135,11 @@ rules:
semi: 2
semi-spacing: 2
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-infix-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';
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
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]
[![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)
[![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
@ -13,7 +14,7 @@
[Rules](http://eslint.org/docs/rules/) |
[Contributing](http://eslint.org/docs/developer-guide/contributing) |
[Reporting Bugs](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) |
[Code of Conduct](https://js.foundation/conduct/) |
[Code of Conduct](https://js.foundation/community/code-of-conduct) |
[Twitter](https://twitter.com/geteslint) |
[Mailing List](https://groups.google.com/group/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))
* Mark Pedrotti ([@pedrottimark](https://github.com/pedrottimark))
* 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))
* Marat Dulin ([@mdevils](https://github.com/mdevils))
* 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.
## 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
### 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?
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?

14
tools/eslint/bin/eslint.js

@ -61,14 +61,12 @@ if (useStdIn) {
} else if (init) {
const configInit = require("../lib/config/config-initializer");
configInit.initializeConfig(err => {
if (err) {
process.exitCode = 1;
console.error(err.message);
console.error(err.stack);
} else {
process.exitCode = 0;
}
configInit.initializeConfig().then(() => {
process.exitCode = 0;
}).catch(err => {
process.exitCode = 1;
console.error(err.message);
console.error(err.stack);
});
} else {
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,
envs: [],
globals: [],
rules: {},
extensions: [".js"],
ignore: true,
ignorePath: null,
parser: "", // must be empty
cache: false,
// 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"),
rules = require("../lib/rules");
Rules = require("../lib/rules");
const rules = new Rules();
//------------------------------------------------------------------------------
// Helpers

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

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

5
tools/eslint/lib/api.js

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

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

@ -10,6 +10,7 @@
//------------------------------------------------------------------------------
const esutils = require("esutils");
const espree = require("espree");
//------------------------------------------------------------------------------
// 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 LINEBREAKS = new Set(["\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
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.
// e.g.
// 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,
LINEBREAKS,
LINEBREAK_MATCHER,
SHEBANG_MATCHER,
STATEMENT_LIST_PARENTS,
/**
@ -524,7 +527,7 @@ module.exports = {
/**
* 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
*/
isDirectiveComment(node) {
@ -556,9 +559,9 @@ module.exports = {
/**
* 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.
* @returns {escope.Variable|null} A found variable or `null`.
* @returns {eslint-scope.Variable|null} A found variable or `null`.
*/
getVariableByName(initScope, name) {
let scope = initScope;
@ -1252,5 +1255,52 @@ module.exports = {
* `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020
*/
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"),
path = require("path"),
rules = require("./rules"),
eslint = require("./eslint"),
defaultOptions = require("../conf/cli-options"),
defaultOptions = require("../conf/default-cli-options"),
Linter = require("./linter"),
IgnoredPaths = require("./ignored-paths"),
Config = require("./config"),
Plugins = require("./config/plugins"),
fileEntryCache = require("file-entry-cache"),
globUtil = require("./util/glob-util"),
SourceCodeFixer = require("./util/source-code-fixer"),
@ -73,8 +71,10 @@ const debug = require("debug")("eslint:cli-engine");
* @typedef {Object} LintResult
* @property {string} filePath The path to the file that was linted.
* @property {LintMessage[]} messages All of the messages for the result.
* @property {number} errorCount Number or errors for the result.
* @property {number} warningCount Number or warnings for the result.
* @property {number} errorCount Number of errors 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=} [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) => {
if (message.fatal || message.severity === 2) {
stat.errorCount++;
if (message.fix) {
stat.fixableErrorCount++;
}
} else {
stat.warningCount++;
if (message.fix) {
stat.fixableWarningCount++;
}
}
return stat;
}, {
errorCount: 0,
warningCount: 0
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
});
}
@ -113,10 +121,14 @@ function calculateStatsPerRun(results) {
return results.reduce((stat, result) => {
stat.errorCount += result.errorCount;
stat.warningCount += result.warningCount;
stat.fixableErrorCount += result.fixableErrorCount;
stat.fixableWarningCount += result.fixableWarningCount;
return stat;
}, {
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 {boolean} options.allowInlineConfig Flag indicating if inline comments
* should be allowed.
* @param {Linter} linter Linter context
* @returns {Object} The result of the fix operation as returned from the
* SourceCodeFixer.
* @private
*/
function multipassFix(text, config, options) {
function multipassFix(text, config, options, linter) {
const MAX_PASSES = 10;
let messages = [],
fixedResult,
@ -153,10 +166,10 @@ function multipassFix(text, config, options) {
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})`);
fixedResult = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages);
fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
// stop if there are any syntax errors.
// 'fixedResult.output' is a empty string.
@ -181,7 +194,7 @@ function multipassFix(text, config, options) {
* the most up-to-date information.
*/
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 {boolean} fix Indicates if fixes should be processed.
* @param {boolean} allowInlineConfig Allow/ignore comments that change config.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this text.
* @private
*/
function processText(text, configHelper, filename, fix, allowInlineConfig) {
function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
// clear all existing settings for a new file
eslint.reset();
linter.reset();
let filePath,
messages,
@ -224,10 +238,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
const config = configHelper.getConfig(filePath);
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) {
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 = [];
parsedBlocks.forEach(block => {
unprocessedMessages.push(eslint.verify(block, config, {
unprocessedMessages.push(linter.verify(block, config, {
filename,
allowInlineConfig
}));
@ -258,10 +272,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
fixedResult = multipassFix(text, config, {
filename,
allowInlineConfig
});
}, linter);
messages = fixedResult.messages;
} else {
messages = eslint.verify(text, config, {
messages = linter.verify(text, config, {
filename,
allowInlineConfig
});
@ -274,7 +288,9 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
filePath: filename,
messages,
errorCount: stats.errorCount,
warningCount: stats.warningCount
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
};
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 {Object} configHelper The configuration options for ESLint.
* @param {Object} options The CLIEngine options object.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this file.
* @private
*/
function processFile(filename, configHelper, options) {
function processFile(filename, configHelper, options, linter) {
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;
@ -339,7 +356,9 @@ function createIgnoreResult(filePath, baseDir) {
}
],
errorCount: 0,
warningCount: 1
warningCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0
};
}
@ -432,141 +451,104 @@ function getCacheFile(cacheFile, cwd) {
// Public Interface
//------------------------------------------------------------------------------
/**
* 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);
class CLIEngine {
/**
* 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}
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} options The options for this instance.
* @constructor
*/
this._fileCache = fileEntryCache.create(cacheFile);
// 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");
});
}
constructor(options) {
/**
* 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.
*/
CLIEngine.getFormatter = function(format) {
let formatterPath;
options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
options
);
// 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
if (typeof format === "string") {
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
// 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
if (format.indexOf("/") > -1) {
const cwd = this.options ? this.options.cwd : process.cwd();
// load in additional rules
if (this.options.rulePaths) {
const cwd = this.options.cwd;
formatterPath = path.resolve(cwd, format);
} else {
formatterPath = `./formatters/${format}`;
this.options.rulePaths.forEach(rulesdir => {
debug(`Loading rules from ${rulesdir}`);
this.linter.rules.load(rulesdir, cwd);
});
}
try {
return require(formatterPath);
} catch (ex) {
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
throw ex;
}
Object.keys(this.options.rules || {}).forEach(name => {
validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
});
} else {
return null;
this.config = new Config(this.options, this.linter);
}
};
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
CLIEngine.getErrorResults = function(results) {
const filtered = [];
results.forEach(result => {
const filteredMessages = result.messages.filter(isErrorMessage);
if (filteredMessages.length > 0) {
filtered.push(
Object.assign(result, {
messages: filteredMessages,
errorCount: filteredMessages.length,
warningCount: 0
})
);
}
});
return filtered;
};
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
static getErrorResults(results) {
const filtered = [];
results.forEach(result => {
const filteredMessages = result.messages.filter(isErrorMessage);
if (filteredMessages.length > 0) {
filtered.push(
Object.assign(result, {
messages: filteredMessages,
errorCount: filteredMessages.length,
warningCount: 0,
fixableErrorCount: result.fixableErrorCount,
fixableWarningCount: 0
})
);
}
});
/**
* 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);
});
};
return filtered;
}
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 {Object} pluginobject Plugin configuration object.
* @returns {void}
*/
addPlugin(name, pluginobject) {
Plugins.define(name, pluginobject);
},
this.config.plugins.define(name, pluginobject);
}
/**
* Resolves the patterns passed into executeOnFiles() into glob-based patterns
@ -576,7 +558,7 @@ CLIEngine.prototype = {
*/
resolveFileGlobPatterns(patterns) {
return globUtil.resolveFileGlobPatterns(patterns, this.options);
},
}
/**
* Executes the current configuration on an array of file and directory names.
@ -587,7 +569,7 @@ CLIEngine.prototype = {
const results = [],
options = this.options,
fileCache = this._fileCache,
configHelper = new Config(options);
configHelper = this.config;
let prevConfig; // the previous configuration used
/**
@ -624,9 +606,10 @@ CLIEngine.prototype = {
* unsupported file extensions and any files that are already linted.
* @param {string} filename The resolved filename of the file to be linted
* @param {boolean} warnIgnored always warn when a file is ignored
* @param {Linter} linter Linter context
* @returns {void}
*/
function executeOnFile(filename, warnIgnored) {
function executeOnFile(filename, warnIgnored, linter) {
let hashOfConfig,
descriptor;
@ -669,7 +652,7 @@ CLIEngine.prototype = {
debug(`Processing ${filename}`);
const res = processFile(filename, configHelper, options);
const res = processFile(filename, configHelper, options, linter);
if (options.cache) {
@ -702,12 +685,11 @@ CLIEngine.prototype = {
const startTime = Date.now();
patterns = this.resolveFileGlobPatterns(patterns);
const fileList = globUtil.listFilesToProcess(patterns, options);
fileList.forEach(fileInfo => {
executeOnFile(fileInfo.filename, fileInfo.ignored);
executeOnFile(fileInfo.filename, fileInfo.ignored, this.linter);
});
const stats = calculateStatsPerRun(results);
@ -723,9 +705,11 @@ CLIEngine.prototype = {
return {
results,
errorCount: stats.errorCount,
warningCount: stats.warningCount
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
};
},
}
/**
* Executes the current configuration on text.
@ -738,7 +722,7 @@ CLIEngine.prototype = {
const results = [],
options = this.options,
configHelper = new Config(options),
configHelper = this.config,
ignoredPaths = new IgnoredPaths(options);
// resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
@ -751,7 +735,7 @@ CLIEngine.prototype = {
results.push(createIgnoreResult(filename, options.cwd));
}
} 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);
@ -759,9 +743,11 @@ CLIEngine.prototype = {
return {
results,
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.
@ -771,10 +757,10 @@ CLIEngine.prototype = {
* @returns {Object} A configuration object for the file.
*/
getConfigForFile(filePath) {
const configHelper = new Config(this.options);
const configHelper = this.config;
return configHelper.getConfig(filePath);
},
}
/**
* Checks if a given path is ignored by ESLint.
@ -786,12 +772,51 @@ CLIEngine.prototype = {
const ignoredPaths = new IgnoredPaths(this.options);
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.getFormatter = CLIEngine.prototype.getFormatter;
module.exports = CLIEngine;

3
tools/eslint/lib/cli.js

@ -17,7 +17,6 @@
const fs = require("fs"),
path = require("path"),
shell = require("shelljs"),
options = require("./options"),
CLIEngine = require("./cli-engine"),
mkdirp = require("mkdirp"),
@ -83,7 +82,7 @@ function printResults(engine, results, format, outputFile) {
if (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);
return false;
}

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

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

90
tools/eslint/lib/config.js

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

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

@ -10,12 +10,13 @@
//------------------------------------------------------------------------------
const lodash = require("lodash"),
eslint = require("../eslint"),
Linter = require("../linter"),
configRule = require("./config-rule"),
ConfigOps = require("./config-ops"),
recConfig = require("../../conf/eslint-recommended");
const debug = require("debug")("eslint:autoconfig");
const linter = new Linter();
//------------------------------------------------------------------------------
// 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
*/
/**
* This callback is used to measure execution status in a progress bar
* @callback progressCallback
* @param {number} The total number of times the callback will be called.
*/
/**
* This callback is used to measure execution status in a progress bar
* @callback progressCallback
* @param {number} The total number of times the callback will be called.
*/
/**
* Create registryItems for rules
@ -290,7 +291,7 @@ class Registry {
ruleSets.forEach(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 => {
@ -310,7 +311,7 @@ class Registry {
ruleSetIdx += 1;
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"),
path = require("path"),
shell = require("shelljs"),
ConfigOps = require("./config-ops"),
validator = require("./config-validator"),
Plugins = require("./plugins"),
pathUtil = require("../util/path-util"),
ModuleResolver = require("../util/module-resolver"),
pathIsInside = require("path-is-inside"),
stripBom = require("strip-bom"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify"),
defaultOptions = require("../../conf/eslint-recommended"),
requireUncached = require("require-uncached");
const debug = require("debug")("eslint:config-file");
@ -63,11 +59,11 @@ const resolver = new ModuleResolver();
/**
* Convenience wrapper for synchronously reading file contents.
* @param {string} filePath The filename to read.
* @returns {string} The file contents.
* @returns {string} The file contents, with the BOM removed.
* @private
*/
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) {
if (name === "eslint:recommended") {
/*
* Add an explicit substitution for eslint:recommended to
* conf/eslint-recommended.js.
*/
/*
* Add an explicit substitution for eslint:recommended to
* conf/eslint-recommended.js.
*/
return path.resolve(__dirname, "../../conf/eslint-recommended.js");
}
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");
}
@ -389,6 +385,7 @@ function getEslintCoreConfigPath(name) {
/**
* Applies values from the "extends" field in a configuration file.
* @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
* was loaded.
* @param {string} [relativeTo] The path to resolve relative to.
@ -396,7 +393,7 @@ function getEslintCoreConfigPath(name) {
* loaded and merged.
* @private
*/
function applyExtends(config, filePath, relativeTo) {
function applyExtends(config, configContext, filePath, relativeTo) {
let configExtends = config.extends;
// normalize into an array for easier handling
@ -421,7 +418,7 @@ function applyExtends(config, filePath, relativeTo) {
);
}
debug(`Loading ${parentPath}`);
return ConfigOps.merge(load(parentPath, false, relativeTo), previousValue);
return ConfigOps.merge(load(parentPath, configContext, false, relativeTo), previousValue);
} catch (e) {
/*
@ -516,19 +513,18 @@ function resolve(filePath, relativeTo) {
return { filePath };
}
/**
* Loads a configuration file from the given file path.
* @param {string} filePath The filename or package name to load the configuration
* information from.
* @param {Config} configContext Plugins context
* @param {boolean} [applyEnvironments=false] Set to true to merge in environment settings.
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} The configuration information.
* @private
*/
function load(filePath, applyEnvironments, relativeTo) {
function load(filePath, configContext, applyEnvironments, relativeTo) {
const resolvedPath = resolve(filePath, relativeTo),
dirname = path.dirname(resolvedPath.filePath),
lookupPath = getLookupPath(dirname);
@ -538,12 +534,7 @@ function load(filePath, applyEnvironments, relativeTo) {
// ensure plugins are properly loaded first
if (config.plugins) {
Plugins.loadAll(config.plugins);
}
// remove parser from config if it is the default parser
if (config.parser === defaultOptions.parser) {
config.parser = null;
configContext.plugins.loadAll(config.plugins);
}
// include full path of parser if present
@ -556,20 +547,20 @@ function load(filePath, applyEnvironments, relativeTo) {
}
// 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
* a "parent". Load the referenced file and merge the configuration recursively.
*/
if (config.extends) {
config = applyExtends(config, filePath, dirname);
config = applyExtends(config, configContext, filePath, dirname);
}
if (config.env && applyEnvironments) {
// 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++) {
const filename = path.join(directory, CONFIG_FILES[i]);
if (shell.test("-f", filename)) {
if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {
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*/
/**
* Ask use a few questions on command prompt
* @param {Function} callback callback function when file has been written
* @returns {void}
* @returns {Promise} The promise with the result of the prompt
*/
function promptUser(callback) {
function promptUser() {
let config;
inquirer.prompt([
return inquirer.prompt([
{
type: "list",
name: "source",
@ -343,29 +342,26 @@ function promptUser(callback) {
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
}
}
], earlyAnswers => {
]).then(earlyAnswers => {
// early exit if you are using a style guide
if (earlyAnswers.source === "guide") {
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.");
return;
return void 0;
}
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
earlyAnswers.styleguide = "airbnb-base";
}
try {
config = getConfigForStyleGuide(earlyAnswers.styleguide);
writeFile(config, earlyAnswers.format);
} catch (err) {
callback(err);
return;
}
return;
config = getConfigForStyleGuide(earlyAnswers.styleguide);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the questions otherwise...
inquirer.prompt([
return inquirer.prompt([
{
type: "confirm",
name: "es6",
@ -412,25 +408,21 @@ function promptUser(callback) {
return answers.jsx;
}
}
], secondAnswers => {
]).then(secondAnswers => {
// early exit if you are using automatic style generation
if (earlyAnswers.source === "auto") {
try {
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
config = processAnswers(combinedAnswers);
installModules(config);
writeFile(config, earlyAnswers.format);
} catch (err) {
callback(err);
return;
}
return;
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
config = processAnswers(combinedAnswers);
installModules(config);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the style questions otherwise...
inquirer.prompt([
return inquirer.prompt([
{
type: "list",
name: "indent",
@ -465,16 +457,12 @@ function promptUser(callback) {
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"]
}
], answers => {
try {
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
config = processAnswers(totalAnswers);
installModules(config);
writeFile(config, answers.format);
} catch (err) {
callback(err); // eslint-disable-line callback-return
}
]).then(answers => {
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
config = processAnswers(totalAnswers);
installModules(config);
writeFile(config, answers.format);
});
});
});
@ -487,8 +475,8 @@ function promptUser(callback) {
const init = {
getConfigForStyleGuide,
processAnswers,
/* istanbul ignore next */initializeConfig(callback) {
promptUser(callback);
/* istanbul ignore next */initializeConfig() {
return promptUser();
}
};

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

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

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

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

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

@ -9,9 +9,8 @@
// Requirements
//------------------------------------------------------------------------------
const rules = require("../rules"),
Environments = require("./environments"),
schemaValidator = require("is-my-json-valid"),
const schemaValidator = require("is-my-json-valid"),
configSchema = require("../../conf/config-schema.json"),
util = require("util");
const validators = {
@ -25,10 +24,11 @@ const validators = {
/**
* Gets a complete options schema for a rule.
* @param {string} id The rule's unique name.
* @param {Rules} rulesContext Rule context
* @returns {Object} JSON Schema for the rule's options.
*/
function getRuleOptionsSchema(id) {
const rule = rules.get(id),
function getRuleOptionsSchema(id, rulesContext) {
const rule = rulesContext.get(id),
schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
// 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.
* @param {string} id The rule's unique name
* @param {array} localOptions The options for the rule, excluding severity
* @param {Rules} rulesContext Rule context
* @returns {void}
*/
function validateRuleSchema(id, localOptions) {
const schema = getRuleOptionsSchema(id);
function validateRuleSchema(id, localOptions, rulesContext) {
const schema = getRuleOptionsSchema(id, rulesContext);
if (!validators.rules[id] && schema) {
validators.rules[id] = schemaValidator(schema, { verbose: true });
@ -95,15 +96,16 @@ function validateRuleSchema(id, localOptions) {
* Validates a rule's options against its schema.
* @param {string} id The rule's unique name.
* @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}
*/
function validateRuleOptions(id, options, source) {
function validateRuleOptions(id, options, source, rulesContext) {
try {
const severity = validateRuleSeverity(options);
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) {
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
* @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}
*/
function validateEnvironment(environment, source) {
function validateEnvironment(environment, source, envContext) {
// not having an environment is ok
if (!environment) {
return;
}
if (Array.isArray(environment)) {
throw new Error("Environment must not be an array");
}
Object.keys(environment).forEach(env => {
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 => {
if (!Environments.get(env)) {
const message = [
source, ":\n",
"\tEnvironment key \"", env, "\" is unknown\n"
];
throw new Error(message.join(""));
}
});
} else {
throw new Error("Environment must be an object");
/**
* Validates a rules config object
* @param {Object} rulesConfig The rules config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext Rule context
* @returns {void}
*/
function validateRules(rulesConfig, source, rulesContext) {
if (!rulesConfig) {
return;
}
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 {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}
*/
function validate(config, source) {
function validateConfigSchema(config, source) {
const validator = schemaValidator(configSchema, { verbose: true });
if (typeof config.rules === "object") {
Object.keys(config.rules).forEach(id => {
validateRuleOptions(id, config.rules[id], source);
});
if (!validator(config)) {
throw new Error(`${source}:\n\tESLint configuration is invalid:\n${formatErrors(validator.errors)}`);
}
}
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");
//------------------------------------------------------------------------------
// Private
// Public Interface
//------------------------------------------------------------------------------
let environments = new Map();
class Environments {
/**
* Loads the default environments.
* @returns {void}
* @private
*/
function load() {
Object.keys(envs).forEach(envName => {
environments.set(envName, envs[envName]);
});
}
// always load default environments upfront
load();
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* create env context
*/
constructor() {
this._environments = new Map();
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.
@ -44,8 +42,19 @@ module.exports = {
* @returns {Object?} The environment object or null if not found.
*/
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.
@ -54,8 +63,8 @@ module.exports = {
* @returns {void}
*/
define(name, env) {
environments.set(name, env);
},
this._environments.set(name, env);
}
/**
* Imports all environments from a plugin.
@ -69,14 +78,7 @@ module.exports = {
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
//------------------------------------------------------------------------------
const Environments = require("./environments"),
Rules = require("../rules");
const debug = require("debug")("eslint:plugins");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
let plugins = Object.create(null);
const PLUGIN_NAME_PREFIX = "eslint-plugin-",
NAMESPACE_REGEX = /^@.*\//i;
/**
* Removes the prefix `eslint-plugin-` from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without prefix.
*/
function removePrefix(pluginName) {
return pluginName.indexOf(PLUGIN_NAME_PREFIX) === 0 ? pluginName.substring(PLUGIN_NAME_PREFIX.length) : pluginName;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* 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.
* Plugin class
*/
function getNamespace(pluginName) {
return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
}
class Plugins {
/**
* Removes the namespace from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without the namespace.
*/
function removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}
/**
* Creates the plugins context
* @param {Environments} envContext - env context
* @param {Rules} rulesContext - rules context
*/
constructor(envContext, rulesContext) {
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,
removeNamespace,
/**
* Removes the namespace from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without the namespace.
*/
static removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}
/**
* Defines a plugin with a given name rather than loading from disk.
@ -66,22 +71,16 @@ module.exports = {
* @returns {void}
*/
define(pluginName, plugin) {
const pluginNamespace = getNamespace(pluginName),
pluginNameWithoutNamespace = removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace),
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix;
// load up environments and rules
plugins[shortName] = plugin;
Environments.importPlugin(plugin, shortName);
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);
},
this._plugins[shortName] = plugin;
this._environments.importPlugin(plugin, shortName);
this._rules.importPlugin(plugin, shortName);
}
/**
* Gets a plugin with the given name.
@ -89,16 +88,16 @@ module.exports = {
* @returns {Object} The plugin or null if not loaded.
*/
get(pluginName) {
return plugins[pluginName] || null;
},
return this._plugins[pluginName] || null;
}
/**
* Returns all plugins that are loaded.
* @returns {Object} The plugins cache.
*/
getAll() {
return plugins;
},
return this._plugins;
}
/**
* Loads a plugin with the given name.
@ -107,9 +106,9 @@ module.exports = {
* @throws {Error} If the plugin cannot be loaded.
*/
load(pluginName) {
const pluginNamespace = getNamespace(pluginName),
pluginNameWithoutNamespace = removeNamespace(pluginName),
pluginNameWithoutPrefix = removePrefix(pluginNameWithoutNamespace),
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix,
longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
let plugin = null;
@ -124,7 +123,7 @@ module.exports = {
throw whitespaceError;
}
if (!plugins[shortName]) {
if (!this._plugins[shortName]) {
try {
plugin = require(longName);
} catch (pluginLoadErr) {
@ -150,7 +149,7 @@ module.exports = {
this.define(pluginName, plugin);
}
},
}
/**
* Loads all plugins from an array.
@ -160,13 +159,7 @@ module.exports = {
*/
loadAll(pluginNames) {
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) {
try {
return fs.readdirSync(directory);
} catch (ex) {
return [];
@ -79,9 +80,9 @@ class FileFinder {
* Searches for all the file names in this.fileNames.
* Is currently used by lib/config.js to find .eslintrc and package.json files.
* @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;
if (directory) {
@ -91,7 +92,8 @@ class FileFinder {
}
if (cache.hasOwnProperty(directory)) {
return cache[directory];
yield* cache[directory];
return; // to avoid doing the normal loop afterwards
}
const dirs = [];
@ -114,19 +116,21 @@ class FileFinder {
for (let j = 0; j < searched; j++) {
cache[dirs[j]].push(filePath);
}
yield filePath;
break;
}
}
}
const child = directory;
// Assign parent directory to directory.
directory = path.dirname(directory);
if (directory === child) {
return cache[dirs[0]];
return;
}
} while (!cache.hasOwnProperty(directory));
// 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]);
}
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.
* @param {number} errors The number of errors.
* @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.
*/
function formatSummary(errors, warnings) {
function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
const summaryColor = errors > 0 ? "red" : "yellow";
const summary = [];
const fixablesSummary = [];
if (errors > 0) {
summary.push(`${errors} ${pluralize("error", errors)}`);
@ -88,7 +91,21 @@ function formatSummary(errors, 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) {
let errors = 0;
let warnings = 0;
let fixableErrors = 0;
let fixableWarnings = 0;
const resultsWithMessages = results.filter(result => result.messages.length > 0);
let output = resultsWithMessages.reduce((resultsOutput, result) => {
@ -105,12 +125,14 @@ module.exports = function(results) {
errors += result.errorCount;
warnings += result.warningCount;
fixableErrors += result.fixableErrorCount;
fixableWarnings += result.fixableWarningCount;
return resultsOutput.concat(messages);
}, []).join("\n");
output += "\n";
output += formatSummary(errors, warnings);
output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
return (errors + warnings) > 0 ? output : "";
};

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

@ -28,8 +28,10 @@ function pluralize(word, count) {
module.exports = function(results) {
let output = "\n",
errors = 0,
warnings = 0,
errorCount = 0,
warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow";
results.forEach(result => {
@ -39,8 +41,10 @@ module.exports = function(results) {
return;
}
errors += result.errorCount;
warnings += result.warningCount;
errorCount += result.errorCount;
warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
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`;
});
const total = errors + warnings;
const total = errorCount + warningCount;
if (total > 0) {
output += chalk[summaryColor].bold([
"\u2716 ", total, pluralize(" problem", total),
" (", errors, pluralize(" error", errors), ", ",
warnings, pluralize(" warning", warnings), ")\n"
" (", errorCount, pluralize(" error", errorCount), ", ",
warningCount, pluralize(" warning", warningCount), ")\n"
].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 : "";

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

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

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

@ -12,7 +12,6 @@
const fs = require("fs"),
path = require("path"),
ignore = require("ignore"),
shell = require("shelljs"),
pathUtil = require("./util/path-util");
const debug = require("debug")("eslint:ignored-paths");
@ -54,7 +53,7 @@ function findIgnoreFile(cwd) {
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;
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")) {
result = result || (this.ig.default.filter([relativePath]).length === 0);
@ -213,15 +212,7 @@ class IgnoredPaths {
const filter = ig.createFilter();
/**
* 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;
const base = this.baseDir;
return function(absolutePath) {
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.
* @author Nicholas C. Zakas
* @fileoverview Main Linter Class
* @author Gyandeep Singh
*/
"use strict";
@ -11,22 +11,22 @@
const assert = require("assert"),
EventEmitter = require("events").EventEmitter,
escope = require("escope"),
eslintScope = require("eslint-scope"),
levn = require("levn"),
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"),
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
ConfigOps = require("./config/config-ops"),
validator = require("./config/config-validator"),
Environments = require("./config/environments"),
CommentEventGenerator = require("./util/comment-event-generator"),
NodeEventGenerator = require("./util/node-event-generator"),
SourceCode = require("./util/source-code"),
Traverser = require("./util/traverser"),
RuleContext = require("./rule-context"),
rules = require("./rules"),
Rules = require("./rules"),
timing = require("./timing"),
astUtils = require("./ast-utils"),
pkg = require("../package.json");
@ -158,19 +158,20 @@ function parseListConfig(string) {
* @param {ASTNode} program The top node of the AST.
* @param {Scope} globalScope The global scope.
* @param {Object} config The existing configuration data.
* @param {Environments} envContext Env context
* @returns {void}
*/
function addDeclaredGlobals(program, globalScope, config) {
function addDeclaredGlobals(program, globalScope, config, envContext) {
const declaredGlobals = {},
exportedGlobals = {},
explicitGlobals = {},
builtin = Environments.get("builtin");
builtin = envContext.get("builtin");
Object.assign(declaredGlobals, builtin);
Object.keys(config.env).forEach(name => {
if (config.env[name]) {
const env = Environments.get(name),
const env = envContext.get(name),
environmentGlobals = env && env.globals;
if (environmentGlobals) {
@ -187,7 +188,7 @@ function addDeclaredGlobals(program, globalScope, config) {
let variable = globalScope.set.get(name);
if (!variable) {
variable = new escope.Variable(name, globalScope);
variable = new eslintScope.Variable(name, globalScope);
variable.eslintExplicitGlobal = false;
globalScope.variables.push(variable);
globalScope.set.set(name, variable);
@ -199,7 +200,7 @@ function addDeclaredGlobals(program, globalScope, config) {
let variable = globalScope.set.get(name);
if (!variable) {
variable = new escope.Variable(name, globalScope);
variable = new eslintScope.Variable(name, globalScope);
variable.eslintExplicitGlobal = true;
variable.eslintExplicitGlobalComment = explicitGlobals[name].comment;
globalScope.variables.push(variable);
@ -314,11 +315,10 @@ function enableReporting(reportingConfig, start, rulesToEnable) {
* @param {string} filename The file being checked.
* @param {ASTNode} ast The top node of the AST.
* @param {Object} config The existing configuration data.
* @param {Object[]} reportingConfig The existing reporting configuration data.
* @param {Object[]} messages The messages queue.
* @param {Linter} linterContext Linter context object
* @returns {Object} Modified config object
*/
function modifyConfigsFromComments(filename, ast, config, reportingConfig, messages) {
function modifyConfigsFromComments(filename, ast, config, linterContext) {
let commentConfig = {
exported: {},
@ -327,6 +327,8 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
env: {}
};
const commentRules = {};
const messages = linterContext.messages;
const reportingConfig = linterContext.reportingConfig;
ast.comments.forEach(comment => {
@ -365,7 +367,7 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
Object.keys(items).forEach(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;
});
break;
@ -373,7 +375,7 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa
// no default
}
} else { // comment.type === "Line"
} else { // comment.type === "Line"
if (match[1] === "eslint-disable-line") {
disableReporting(reportingConfig, { line: comment.loc.start.line, column: 0 }, 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
Object.keys(commentConfig.env).forEach(name => {
const env = Environments.get(name);
const env = linterContext.environments.get(name);
if (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
* @param {Object} config Initial config
* @param {Environments} envContext Env context
* @returns {Object} Processed config
*/
function prepareConfig(config) {
config.globals = config.globals || config.global || {};
delete config.global;
const copiedRules = {};
let parserOptions = {};
function prepareConfig(config, envContext) {
config.globals = config.globals || {};
const copiedRules = Object.assign({}, defaultConfig.rules);
let parserOptions = Object.assign({}, defaultConfig.parserOptions);
if (typeof config.rules === "object") {
Object.keys(config.rules).forEach(k => {
@ -474,7 +474,7 @@ function prepareConfig(config) {
// merge in environment parserOptions
if (typeof config.env === "object") {
Object.keys(config.env).forEach(envName => {
const env = Environments.get(envName);
const env = envContext.get(envName);
if (config.env[envName] && env && env.parserOptions) {
parserOptions = ConfigOps.merge(parserOptions, env.parserOptions);
@ -484,21 +484,18 @@ function prepareConfig(config) {
const preparedConfig = {
rules: copiedRules,
parser: config.parser || DEFAULT_PARSER,
globals: ConfigOps.merge({}, config.globals),
env: ConfigOps.merge({}, config.env || {}),
settings: ConfigOps.merge({}, config.settings || {}),
parser: config.parser || defaultConfig.parser,
globals: ConfigOps.merge(defaultConfig.globals, config.globals),
env: ConfigOps.merge(defaultConfig.env, config.env || {}),
settings: ConfigOps.merge(defaultConfig.settings, config.settings || {}),
parserOptions: ConfigOps.merge(parserOptions, config.parserOptions || {})
};
const isModule = preparedConfig.parserOptions.sourceType === "module";
if (isModule) {
if (!preparedConfig.parserOptions.ecmaFeatures) {
preparedConfig.parserOptions.ecmaFeatures = {};
}
// 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);
@ -586,150 +583,157 @@ function stripUnicodeBOM(text) {
return text;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Object that is responsible for verifying JavaScript text
* @name eslint
* Get the severity level of a rule (0 - none, 1 - warning, 2 - error)
* 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() {
const api = Object.create(new EventEmitter());
let messages = [],
currentConfig = null,
currentScopes = null,
scopeManager = null,
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;
}
function getRuleSeverity(ruleConfig) {
if (typeof ruleConfig === "number") {
return ruleConfig;
} else if (Array.isArray(ruleConfig)) {
return ruleConfig[0];
}
return 0;
// 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
* else can happen. However, a parsing error does not throw an error
* from this method - it's just considered a fatal error message, a
* problem that ESLint identified just like any other.
*/
try {
if (typeof parser.parseForESLint === "function") {
return parser.parseForESLint(text, parserOptions);
}
return parser.parse(text, parserOptions);
/**
* Get the options for a rule (not including severity), if any
* @param {Array|number} ruleConfig rule configuration
* @returns {Array} of rule options, empty Array if none
*/
function getRuleOptions(ruleConfig) {
if (Array.isArray(ruleConfig)) {
return ruleConfig.slice(1);
}
return [];
} catch (ex) {
}
// If the message includes a leading line number, strip it:
const message = ex.message.replace(/^line \d+:/i, "").trim();
const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : 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.
* @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({
ruleId: null,
fatal: true,
severity: 2,
source,
message: `Parsing error: ${message}`,
try {
parser = require(config.parser);
} catch (ex) {
messages.push({
ruleId: null,
fatal: true,
severity: 2,
source: null,
message: ex.message,
line: 0,
column: 0
});
line: ex.lineNumber,
column: ex.column
});
return null;
}
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)
* 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
/*
* Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error
* from this method - it's just considered a fatal error message, a
* problem that ESLint identified just like any other.
*/
function getRuleSeverity(ruleConfig) {
if (typeof ruleConfig === "number") {
return ruleConfig;
} else if (Array.isArray(ruleConfig)) {
return ruleConfig[0];
try {
if (typeof parser.parseForESLint === "function") {
return parser.parseForESLint(text, parserOptions);
}
return 0;
return parser.parse(text, parserOptions);
}
} catch (ex) {
/**
* Get the options for a rule (not including severity), if any
* @param {Array|number} ruleConfig rule configuration
* @returns {Array} of rule options, empty Array if none
*/
function getRuleOptions(ruleConfig) {
if (Array.isArray(ruleConfig)) {
return ruleConfig.slice(1);
}
return [];
// If the message includes a leading line number, strip it:
const message = ex.message.replace(/^line \d+:/i, "").trim();
const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
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.
* @returns {void}
*/
api.reset = function() {
reset() {
this.removeAllListeners();
messages = [];
currentConfig = null;
currentScopes = null;
scopeManager = null;
traverser = null;
reportingConfig = [];
sourceCode = null;
};
this.messages = [];
this.currentConfig = null;
this.currentScopes = null;
this.scopeManager = null;
this.traverser = null;
this.reportingConfig = [];
this.sourceCode = null;
}
/**
* 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} [settings] Global settings passed to each rule.
* @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.
* @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;
let ast,
parseResult,
shebang,
allowInlineConfig;
// evaluate arguments
if (typeof filenameOrOptions === "object") {
currentFilename = filenameOrOptions.filename;
this.currentFilename = filenameOrOptions.filename;
allowInlineConfig = filenameOrOptions.allowInlineConfig;
saveState = filenameOrOptions.saveState;
} else {
currentFilename = filenameOrOptions;
this.currentFilename = filenameOrOptions;
}
if (!saveState) {
@ -789,24 +792,22 @@ module.exports = (function() {
}
// process initial config to make it safe to extend
config = prepareConfig(config);
config = prepareConfig(config, this.environments);
// only do this for text
if (text !== null) {
// there's no input, just exit here
if (text.trim().length === 0) {
sourceCode = new SourceCode(text, blankScriptAST);
return messages;
this.sourceCode = new SourceCode(text, blankScriptAST);
return this.messages;
}
parseResult = parse(
stripUnicodeBOM(text).replace(/^#!([^\r\n]+)/, (match, captured) => {
shebang = captured;
return `//${captured}`;
}),
stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`),
config,
currentFilename
this.currentFilename,
this.messages
);
// if this result is from a parseForESLint() method, normalize
@ -818,12 +819,12 @@ module.exports = (function() {
}
if (ast) {
sourceCode = new SourceCode(text, ast);
this.sourceCode = new SourceCode(text, ast);
}
} else {
sourceCode = textOrSourceCode;
ast = sourceCode.ast;
this.sourceCode = textOrSourceCode;
ast = this.sourceCode.ast;
}
// 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
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
@ -841,7 +842,7 @@ module.exports = (function() {
Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
let ruleCreator;
ruleCreator = rules.get(key);
ruleCreator = this.rules.get(key);
if (!ruleCreator) {
const replacementMsg = getRuleReplacementMessage(key);
@ -851,7 +852,7 @@ module.exports = (function() {
} else {
ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
}
rules.define(key, ruleCreator);
this.rules.define(key, ruleCreator);
}
const severity = getRuleSeverity(config.rules[key]);
@ -859,7 +860,7 @@ module.exports = (function() {
try {
const ruleContext = new RuleContext(
key, api, severity, options,
key, this, severity, options,
config.settings, config.parserOptions, config.parser,
ruleCreator.meta,
(parseResult && parseResult.services ? parseResult.services : {})
@ -870,7 +871,7 @@ module.exports = (function() {
// add all the selectors from the rule as listeners
Object.keys(rule).forEach(selector => {
api.on(selector, timing.enabled
this.on(selector, timing.enabled
? timing.time(key, rule[selector])
: rule[selector]
);
@ -882,40 +883,30 @@ module.exports = (function() {
});
// save config so rules can access as necessary
currentConfig = config;
traverser = new Traverser();
this.currentConfig = config;
this.traverser = new Traverser();
const ecmaFeatures = currentConfig.parserOptions.ecmaFeatures || {};
const ecmaVersion = currentConfig.parserOptions.ecmaVersion || 5;
const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
// gather scope data that may be needed by the rules
scopeManager = escope.analyze(ast, {
this.scopeManager = eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion,
sourceType: currentConfig.parserOptions.sourceType || "script",
sourceType: this.currentConfig.parserOptions.sourceType || "script",
fallback: Traverser.getKeys
});
currentScopes = scopeManager.scopes;
this.currentScopes = this.scopeManager.scopes;
// augment global scope with declared global variables
addDeclaredGlobals(ast, currentScopes[0], currentConfig);
addDeclaredGlobals(ast, this.currentScopes[0], this.currentConfig, this.environments);
// remove shebang comments
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);
let eventGenerator = new NodeEventGenerator(this);
eventGenerator = new CodePathAnalyzer(eventGenerator);
eventGenerator = new CommentEventGenerator(eventGenerator, sourceCode);
/*
* 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
* and react accordingly.
*/
traverser.traverse(ast, {
this.traverser.traverse(ast, {
enter(node, parent) {
node.parent = parent;
eventGenerator.enterNode(node);
@ -935,7 +926,7 @@ module.exports = (function() {
}
// sort by line and column
messages.sort((a, b) => {
this.messages.sort((a, b) => {
const lineDiff = a.line - b.line;
if (lineDiff === 0) {
@ -945,8 +936,8 @@ module.exports = (function() {
});
return messages;
};
return this.messages;
}
/**
* Reports a message from one of the rules.
@ -963,11 +954,13 @@ module.exports = (function() {
* @param {Object} meta Metadata of the rule
* @returns {void}
*/
api.report = function(ruleId, severity, node, location, message, opts, fix, meta) {
report(ruleId, severity, node, location, message, opts, fix, meta) {
if (node) {
assert.strictEqual(typeof node, "object", "Node must be an object");
}
let endLocation;
if (typeof location === "string") {
assert.ok(node, "Node must be provided when reporting error if location is not provided");
@ -976,14 +969,14 @@ module.exports = (function() {
opts = message;
message = location;
location = node.loc.start;
endLocation = node.loc.end;
} else {
endLocation = location.end;
}
// Store end location.
const endLocation = location.end;
location = location.start || location;
if (isDisabledByReportingConfig(reportingConfig, ruleId, location)) {
if (isDisabledByReportingConfig(this.reportingConfig, ruleId, location)) {
return;
}
@ -1003,15 +996,15 @@ module.exports = (function() {
severity,
message,
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,
source: sourceCode.lines[location.line - 1] || ""
source: this.sourceCode.lines[location.line - 1] || ""
};
// Define endLine and endColumn if exists.
if (endLocation) {
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
@ -1025,73 +1018,39 @@ module.exports = (function() {
problem.fix = fix;
}
messages.push(problem);
};
this.messages.push(problem);
}
/**
* Gets the SourceCode object representing the parsed source.
* @returns {SourceCode} The SourceCode object.
*/
api.getSourceCode = function() {
return 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;
};
});
getSourceCode() {
return this.sourceCode;
}
/**
* Gets nodes that are ancestors of current node.
* @returns {ASTNode[]} Array of objects representing ancestors.
*/
api.getAncestors = function() {
return traverser.parents();
};
getAncestors() {
return this.traverser.parents();
}
/**
* Gets the scope for the current node.
* @returns {Object} An object representing the current node's scope.
*/
api.getScope = function() {
const parents = traverser.parents();
getScope() {
const parents = this.traverser.parents();
// Don't do this for Program nodes - they have no parents
if (parents.length) {
// 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) {
parents.push(current);
}
@ -1105,7 +1064,7 @@ module.exports = (function() {
for (let i = parents.length - 1; i >= 0; --i) {
// Get the innermost scope
const scope = scopeManager.acquire(parents[i], true);
const scope = this.scopeManager.acquire(parents[i], true);
if (scope) {
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
@ -1128,9 +1087,9 @@ module.exports = (function() {
* @returns {boolean} True if the variable was found and marked as used,
* false if not.
*/
api.markVariableAsUsed = function(name) {
const hasGlobalReturn = currentConfig.parserOptions.ecmaFeatures && currentConfig.parserOptions.ecmaFeatures.globalReturn,
specialScope = hasGlobalReturn || currentConfig.parserOptions.sourceType === "module";
markVariableAsUsed(name) {
const hasGlobalReturn = this.currentConfig.parserOptions.ecmaFeatures && this.currentConfig.parserOptions.ecmaFeatures.globalReturn,
specialScope = hasGlobalReturn || this.currentConfig.parserOptions.sourceType === "module";
let scope = this.getScope(),
i,
len;
@ -1152,20 +1111,20 @@ module.exports = (function() {
} while ((scope = scope.upper));
return false;
};
}
/**
* Gets the filename for the currently parsed source.
* @returns {string} The filename associated with the source being parsed.
* Defaults to "<input>" if no filename info is present.
*/
api.getFilename = function() {
if (typeof currentFilename === "string") {
return currentFilename;
getFilename() {
if (typeof this.currentFilename === "string") {
return this.currentFilename;
}
return "<input>";
};
}
/**
* 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
* @returns {void}
*/
const defineRule = api.defineRule = function(ruleId, ruleModule) {
rules.define(ruleId, ruleModule);
};
defineRule(ruleId, ruleModule) {
this.rules.define(ruleId, ruleModule);
}
/**
* Defines many new linting rules.
* @param {Object} rulesToDefine map from unique rule identifier to rule
* @returns {void}
*/
api.defineRules = function(rulesToDefine) {
defineRules(rulesToDefine) {
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
defineRule(ruleId, rulesToDefine[ruleId]);
this.defineRule(ruleId, rulesToDefine[ruleId]);
});
};
}
/**
* Gets the default eslint configuration.
* @returns {Object} Object mapping rule IDs to their default configurations
*/
api.defaults = function() {
return require("../conf/eslint-recommended");
};
defaults() { // eslint-disable-line class-methods-use-this
return defaultConfig;
}
/**
* Gets an object with all loaded rules.
* @returns {Map} All loaded rules
*/
api.getRules = function() {
return rules.getAllLoadedRules();
};
api.version = pkg.version;
getRules() {
return this.rules.getAllLoadedRules();
}
/**
* Gets variables that are declared by a specified node.
@ -1223,12 +1180,48 @@ module.exports = (function() {
* - others - always an empty array.
*
* @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) {
return (scopeManager && scopeManager.getDeclaredVariables(node)) || [];
};
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"),
path = require("path");
const rulesDirCache = {};
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
@ -29,6 +31,11 @@ module.exports = function(rulesDir, cwd) {
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);
fs.readdirSync(rulesDir).forEach(file => {
@ -37,5 +44,7 @@ module.exports = function(rulesDir, cwd) {
}
rules[file.slice(0, -3)] = path.join(rulesDir, file);
});
rulesDirCache[rulesDir] = rules;
return rules;
};

156
tools/eslint/lib/rules.js

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

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();
/**
* Determines whether a arrow function argument end with `)`
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
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
if (
@ -68,19 +85,11 @@ module.exports = {
node.body.type !== "BlockStatement" &&
!node.returnType
) {
if (astUtils.isOpeningParenToken(token)) {
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: requireForBlockBodyMessage,
fix(fixer) {
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
fix: fixParamsWithParenthesis
});
}
return;
@ -90,12 +99,12 @@ module.exports = {
requireForBlockBody &&
node.body.type === "BlockStatement"
) {
if (!astUtils.isOpeningParenToken(token)) {
if (!astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: requireForBlockBodyNoParensMessage,
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.returnType
) {
if (astUtils.isOpeningParenToken(token)) {
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: asNeededMessage,
fix(fixer) {
const paramToken = context.getTokenAfter(token);
const closingParenToken = context.getTokenAfter(paramToken);
return fixer.replaceTextRange([
token.range[0],
closingParenToken.range[1]
], paramToken.value);
}
fix: fixParamsWithParenthesis
});
}
return;
}
if (token.type === "Identifier") {
const after = sourceCode.getTokenAfter(token);
if (firstTokenOfParam.type === "Identifier") {
const after = sourceCode.getTokenAfter(firstTokenOfParam);
// (x) => x
if (after.value !== ")") {
@ -136,7 +137,7 @@ module.exports = {
node,
message,
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.
* @param {escope.Reference} reference - A reference to report.
* @param {eslint-scope.Reference} reference - A reference to report.
* @returns {void}
*/
function report(reference) {

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

@ -62,10 +62,12 @@ module.exports = {
function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]];
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
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",
DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/g,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
DEFAULTS = {
ignorePattern: null,
ignoreInlineComments: false,
@ -270,7 +270,7 @@ module.exports = {
: NEVER_MESSAGE;
context.report({
node: null, // Intentionally using loc instead
node: null, // Intentionally using loc instead
loc: comment.loc,
message,
fix(fixer) {
@ -295,7 +295,7 @@ module.exports = {
Program() {
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]
? "A space is required {{dir}} ','."
: "There should be no space {{dir}} ','.",
? "A space is required {{dir}} ','."
: "There should be no space {{dir}} ','.",
data: {
dir
}

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

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

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

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

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

@ -9,7 +9,6 @@
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const esUtils = require("esutils");
//------------------------------------------------------------------------------
// Rule Definition
@ -240,7 +239,7 @@ module.exports = {
// e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
const needsPrecedingSpace = node.type === "DoWhileStatement" &&
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 closingBracket = sourceCode.getLastToken(bodyNode);
@ -294,7 +293,7 @@ module.exports = {
}
} else if (multiOrNest) {
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;
} else if (!isOneLiner(body)) {

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

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

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

@ -79,11 +79,17 @@ module.exports = {
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 textAfterProperty = needsSpaceAfterProperty ? " " : "";
return fixer.replaceTextRange(
[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.
* @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.
*/
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 {ASTNode} node The identifier node.
* @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;
if (!(isShort || isLong) || exceptions[name]) {
return; // Nothing to report
return; // Nothing to report
}
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];
}
/**
* 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.
* @param {ASTNode} node AST Node being evaluated.
@ -350,6 +315,41 @@ module.exports = {
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.
* @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 {
LineComment(node) {
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) {
return;
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
return;
}
const previous = sourceCode.getTokenBefore(node, { includeComments: true });
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."
});
Program() {
const comments = sourceCode.getAllComments();
comments.filter(token => token.type === "Line").forEach(node => {
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) {
return;
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
return;
}
} else {
if (!isOnSameLine) {
context.report({
node,
message: "Expected comment to be beside code."
});
const previous = sourceCode.getTokenBefore(node, { includeComments: true });
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) {
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.
* @param {Array} comments An array of comment nodes.
* @param {Array} comments An array of comment tokens.
* @returns {Array} An array of line numbers.
*/
function getCommentLineNums(comments) {
@ -131,38 +131,28 @@ module.exports = {
emptyLines = getEmptyLineNums(lines),
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
* @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.
*/
function codeAroundComment(node) {
let token;
function codeAroundComment(token) {
let currentToken = token;
token = node;
do {
token = sourceCode.getTokenBefore(token, { includeComments: true });
} while (isCommentNodeType(token));
currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
} while (currentToken && astUtils.isCommentToken(currentToken));
if (token && astUtils.isTokenOnSameLine(token, node)) {
if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
return true;
}
token = node;
currentToken = token;
do {
token = sourceCode.getTokenAfter(token, { includeComments: true });
} while (isCommentNodeType(token));
currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
} while (currentToken && astUtils.isCommentToken(currentToken));
if (token && astUtils.isTokenOnSameLine(node, token)) {
if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
return true;
}
@ -171,137 +161,135 @@ module.exports = {
/**
* 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 {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is inside nodeType.
*/
function isCommentInsideNodeType(node, parent, nodeType) {
function isParentNodeType(parent, nodeType) {
return parent.type === nodeType ||
(parent.body && parent.body.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.
* @param {ASTNode} node The Comment node.
* @param {token} token The Comment token.
* @param {string} nodeType The parent type to check against.
* @returns {boolean} True if the comment is at parent start.
*/
function isCommentAtParentStart(node, nodeType) {
const ancestors = context.getAncestors();
let parent;
function isCommentAtParentStart(token, nodeType) {
const parent = getParentNodeOfToken(token);
if (ancestors.length) {
parent = ancestors.pop();
}
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
node.loc.start.line - parent.loc.start.line === 1;
return parent && isParentNodeType(parent, nodeType) &&
token.loc.start.line - parent.loc.start.line === 1;
}
/**
* 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.
* @returns {boolean} True if the comment is at parent end.
*/
function isCommentAtParentEnd(node, nodeType) {
const ancestors = context.getAncestors();
let parent;
if (ancestors.length) {
parent = ancestors.pop();
}
function isCommentAtParentEnd(token, nodeType) {
const parent = getParentNodeOfToken(token);
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
parent.loc.end.line - node.loc.end.line === 1;
return parent && isParentNodeType(parent, nodeType) &&
parent.loc.end.line - token.loc.end.line === 1;
}
/**
* 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.
*/
function isCommentAtBlockStart(node) {
return isCommentAtParentStart(node, "ClassBody") || isCommentAtParentStart(node, "BlockStatement") || isCommentAtParentStart(node, "SwitchCase");
function isCommentAtBlockStart(token) {
return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
}
/**
* 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.
*/
function isCommentAtBlockEnd(node) {
return isCommentAtParentEnd(node, "ClassBody") || isCommentAtParentEnd(node, "BlockStatement") || isCommentAtParentEnd(node, "SwitchCase") || isCommentAtParentEnd(node, "SwitchStatement");
function isCommentAtBlockEnd(token) {
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.
* @param {ASTNode} node The Comment node.
* @param {token} token The Comment token.
* @returns {boolean} True if the comment is at object start.
*/
function isCommentAtObjectStart(node) {
return isCommentAtParentStart(node, "ObjectExpression") || isCommentAtParentStart(node, "ObjectPattern");
function isCommentAtObjectStart(token) {
return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
}
/**
* 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.
*/
function isCommentAtObjectEnd(node) {
return isCommentAtParentEnd(node, "ObjectExpression") || isCommentAtParentEnd(node, "ObjectPattern");
function isCommentAtObjectEnd(token) {
return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
}
/**
* 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.
*/
function isCommentAtArrayStart(node) {
return isCommentAtParentStart(node, "ArrayExpression") || isCommentAtParentStart(node, "ArrayPattern");
function isCommentAtArrayStart(token) {
return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
}
/**
* 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.
*/
function isCommentAtArrayEnd(node) {
return isCommentAtParentEnd(node, "ArrayExpression") || isCommentAtParentEnd(node, "ArrayPattern");
function isCommentAtArrayEnd(token) {
return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
}
/**
* Checks if a comment node has lines around it (ignores inline comments)
* @param {ASTNode} node The Comment node.
* Checks if a comment token has lines around it (ignores inline comments)
* @param {token} token The Comment token.
* @param {Object} opts Options to determine the newline.
* @param {boolean} opts.after Should have a newline after this line.
* @param {boolean} opts.before Should have a newline before this line.
* @returns {void}
*/
function checkForEmptyLine(node, opts) {
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(node.value)) {
function checkForEmptyLine(token, opts) {
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
return;
}
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
if (ignorePattern && customIgnoreRegExp.test(token.value)) {
return;
}
let after = opts.after,
before = opts.before;
const prevLineNum = node.loc.start.line - 1,
nextLineNum = node.loc.end.line + 1,
commentIsNotAlone = codeAroundComment(node);
const prevLineNum = token.loc.start.line - 1,
nextLineNum = token.loc.end.line + 1,
commentIsNotAlone = codeAroundComment(token);
const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(node),
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(node),
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(node),
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(node),
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(node),
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(node);
const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(token),
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token),
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed;
const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed;
@ -319,17 +307,17 @@ module.exports = {
return;
}
const previousTokenOrComment = sourceCode.getTokenBefore(node, { includeComments: true });
const nextTokenOrComment = sourceCode.getTokenAfter(node, { includeComments: true });
const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
// check for newline before
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
!(isCommentNodeType(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, node))) {
const lineStart = node.range[0] - node.loc.start.column;
!(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
const lineStart = token.range[0] - token.loc.start.column;
const range = [lineStart, lineStart];
context.report({
node,
node: token,
message: "Expected line before comment.",
fix(fixer) {
return fixer.insertTextBeforeRange(range, "\n");
@ -339,12 +327,12 @@ module.exports = {
// check for newline after
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
!(isCommentNodeType(nextTokenOrComment) && astUtils.isTokenOnSameLine(node, nextTokenOrComment))) {
!(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
context.report({
node,
node: token,
message: "Expected line after comment.",
fix(fixer) {
return fixer.insertTextAfter(node, "\n");
return fixer.insertTextAfter(token, "\n");
}
});
}
@ -356,25 +344,25 @@ module.exports = {
//--------------------------------------------------------------------------
return {
LineComment(node) {
if (options.beforeLineComment || options.afterLineComment) {
checkForEmptyLine(node, {
after: options.afterLineComment,
before: options.beforeLineComment
});
}
},
BlockComment(node) {
if (options.beforeBlockComment || options.afterBlockComment) {
checkForEmptyLine(node, {
after: options.afterBlockComment,
before: options.beforeBlockComment
});
}
Program() {
comments.forEach(token => {
if (token.type === "Line") {
if (options.beforeLineComment || options.afterLineComment) {
checkForEmptyLine(token, {
after: options.afterLineComment,
before: options.beforeLineComment
});
}
} else if (token.type === "Block") {
if (options.beforeBlockComment || options.afterBlockComment) {
checkForEmptyLine(token, {
after: options.afterBlockComment,
before: options.beforeBlockComment
});
}
}
});
}
};
}
};

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

@ -1,6 +1,7 @@
/**
* @fileoverview Require or disallow newlines around directives.
* @author Kai Cataldo
* @deprecated
*/
"use strict";
@ -16,7 +17,8 @@ module.exports = {
docs: {
description: "require or disallow newlines around directives",
category: "Stylistic Issues",
recommended: false
recommended: false,
replacedBy: ["padding-line-between-statements"]
},
schema: [{
oneOf: [
@ -38,7 +40,8 @@ module.exports = {
}
]
}],
fixable: "whitespace"
fixable: "whitespace",
deprecated: true
},
create(context) {
@ -131,17 +134,12 @@ module.exports = {
}
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
// 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.
if (
firstDirective.leadingComments && firstDirective.leadingComments.length ||
// Shebangs are not added to leading comments but are accounted for by the following.
node.type === "Program" && hasTokenOrCommentBefore
) {
if (leadingComments.length) {
if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) {
reportError(firstDirective, "before", true);
}
@ -152,7 +150,7 @@ module.exports = {
} else if (
node.type === "Program" &&
expectLineBefore === "never" &&
!hasTokenOrCommentBefore &&
!leadingComments.length &&
hasNewlineBefore(firstDirective)
) {
reportError(firstDirective, "before", false);

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

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

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

@ -38,7 +38,7 @@ module.exports = {
return {
NewExpression(node) {
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);

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

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

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

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

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

@ -33,6 +33,8 @@ module.exports = {
recommended: false
},
fixable: "code",
schema: [{
type: "object",
properties: {
@ -55,7 +57,15 @@ module.exports = {
const body = node.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.
*
* @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'.
*/
function isConsole(reference) {
@ -74,7 +74,7 @@ module.exports = {
* Checks whether the given reference is a member access which is 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
* is not allowed by options.
*/
@ -92,7 +92,7 @@ module.exports = {
/**
* 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}
*/
function report(reference) {

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

@ -16,7 +16,7 @@ module.exports = {
category: "Possible Errors",
recommended: true
},
fixable: "code",
schema: []
},
@ -24,7 +24,13 @@ module.exports = {
return {
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.
* @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.
*/
function isParameter(def) {

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

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

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

@ -166,7 +166,7 @@ module.exports = {
/**
* 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}
*/
function reportAccessingEvalViaGlobalObject(globalScope) {
@ -200,7 +200,7 @@ module.exports = {
/**
* 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}
*/
function reportAccessingEval(globalScope) {

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

@ -9,8 +9,15 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const globals = require("globals");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]);
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -43,73 +50,123 @@ module.exports = {
create(context) {
const config = context.options[0] || {};
const exceptions = config.exceptions || [];
let modifiedBuiltins = Object.keys(globals.builtin).filter(builtin => builtin[0].toUpperCase() === builtin[0]);
const exceptions = new Set(config.exceptions || []);
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) {
const lhs = node.left;
/**
* Checks that an identifier is an object of a prototype whose member
* 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
? lhs.object.property.type === "Literal" && lhs.object.property.value === "prototype"
: lhs.object.property.name === "prototype";
return {
if (!affectsProto) {
return;
}
"Program:exit"() {
const globalScope = context.getScope();
modifiedBuiltins.forEach(builtin => {
if (lhs.object.object.name === builtin) {
context.report({
node,
message: "{{builtin}} prototype is read only, properties should not be added.",
data: {
builtin
}
});
const builtinVar = globalScope.set.get(builtin);
if (builtinVar && builtinVar.references) {
builtinVar.references
.map(ref => ref.identifier)
.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 esUtils = require("esutils");
module.exports = {
meta: {
@ -250,28 +249,27 @@ module.exports = {
const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1);
const firstToken = sourceCode.getFirstToken(node);
// If there is already whitespace before the previous token, don't add more.
if (!tokenBeforeLeftParen || tokenBeforeLeftParen.end !== leftParenToken.start) {
return false;
}
// 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 tokenBeforeLeftParen &&
tokenBeforeLeftParen.range[1] === leftParenToken.range[0] &&
leftParenToken.range[1] === firstToken.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, firstToken);
}
return (precededByIdentiferPart && startsWithIdentifierPart) ||
(precededByUnaryPlus && startsWithUnaryPlus) ||
(precededByUnaryMinus && startsWithUnaryMinus);
/**
* Determines whether a node should be followed by an additional space when removing parens
* @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([
leftParenToken.range[0],
rightParenToken.range[1]
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource);
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
}
});
}
@ -507,12 +505,18 @@ module.exports = {
if (hasExcessParens(node.right)) {
report(node.right);
}
if (hasExcessParens(node.left)) {
report(node.left);
}
},
ForOfStatement(node) {
if (hasExcessParens(node.right)) {
report(node.right);
}
if (hasExcessParens(node.left)) {
report(node.left);
}
},
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) {
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));
}

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

@ -5,6 +5,12 @@
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -23,16 +29,24 @@ module.exports = {
},
create(context) {
const sourceCode = context.getSourceCode();
return {
Literal(node) {
if (typeof node.value === "number") {
if (node.raw.indexOf(".") === 0) {
if (node.raw.startsWith(".")) {
context.report({
node,
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) {

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

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

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

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

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

@ -58,16 +58,14 @@ module.exports = {
* @returns {void}
*/
function check(node) {
const body = nearestBody(node),
const body = nearestBody(),
valid = ((body.type === "Program" && body.distance === 1) ||
body.distance === 2);
if (!valid) {
context.report({ node, message: "Move {{type}} declaration to {{body}} root.", data: {
type: (node.type === "FunctionDeclaration"
? "function" : "variable"),
body: (body.type === "Program"
? "program" : "function body")
type: (node.type === "FunctionDeclaration" ? "function" : "variable"),
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
//------------------------------------------------------------------------------
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 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 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\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 LINE_BREAK = astUtils.createGlobalLinebreakMatcher();
@ -60,9 +60,6 @@ module.exports = {
// Module store of errors that we have found
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`.
const options = context.options[0] || {};
const skipComments = !!options.skipComments;
@ -71,6 +68,7 @@ module.exports = {
const skipTemplates = !!options.skipTemplates;
const sourceCode = context.getSourceCode();
const commentNodes = sourceCode.getAllComments();
/**
* 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`.
* @returns {void}
@ -220,7 +208,6 @@ module.exports = {
* We can later filter the errors when they are found to be not an
* issue in nodes we don't care about.
*/
checkForIrregularWhitespace(node);
checkForIrregularLineTerminators(node);
};
@ -228,13 +215,10 @@ module.exports = {
nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral;
nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral;
nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop;
nodes.LineComment = skipComments ? rememberCommentNode : noop;
nodes.BlockComment = skipComments ? rememberCommentNode : noop;
nodes["Program:exit"] = function() {
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);
}

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

@ -94,13 +94,13 @@ module.exports = {
ruleDef.VariableDeclaration = function(node) {
if (node.kind === "let" || node.kind === "const") {
markLoneBlock(node);
markLoneBlock();
}
};
ruleDef.FunctionDeclaration = function(node) {
ruleDef.FunctionDeclaration = function() {
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} 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.
*/
function isSafe(funcNode, loopNode, reference) {
@ -131,7 +131,7 @@ function isSafe(funcNode, loopNode, reference) {
* - is readonly.
* - 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.
*/
function isSafeReference(upperRef) {

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

@ -33,6 +33,9 @@ module.exports = {
}
},
additionalProperties: false
},
ignoreEOLComments: {
type: "boolean"
}
},
additionalProperties: false
@ -43,8 +46,10 @@ module.exports = {
create(context) {
// the index of the last comment that was checked
const exceptions = { Property: true },
options = context.options[0];
const sourceCode = context.getSourceCode(),
exceptions = { Property: true },
options = context.options[0] || {},
ignoreEOLComments = options.ignoreEOLComments;
let hasExceptions = true,
lastCommentIndex = 0;
@ -59,6 +64,23 @@ module.exports = {
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
* the index against the comment range. Since the check goes straight
@ -73,7 +95,7 @@ module.exports = {
while (lastCommentIndex < comments.length) {
const comment = comments[lastCommentIndex];
if (comment.range[0] <= index && index < comment.range[1]) {
if (comment.range[0] < index && index < comment.range[1]) {
return true;
} else if (index > comment.range[1]) {
lastCommentIndex++;
@ -85,6 +107,33 @@ module.exports = {
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
//--------------------------------------------------------------------------
@ -92,47 +141,43 @@ module.exports = {
return {
Program() {
const sourceCode = context.getSourceCode(),
source = sourceCode.getText(),
const source = sourceCode.getText(),
allComments = sourceCode.getAllComments(),
JOINED_LINEBEAKS = Array.from(astUtils.LINEBREAKS).join(""),
pattern = new RegExp(String.raw`[^ \t${JOINED_LINEBEAKS}].? {2,}`, "g"); // note: repeating space
pattern = /[^\s].*? {2,}/g;
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)) {
// do not flag anything inside of comments
if (!isIndexInComment(pattern.lastIndex, allComments)) {
const token = sourceCode.getTokenByRangeStart(pattern.lastIndex);
const token = sourceCode.getTokenByRangeStart(pattern.lastIndex, { includeComments: true });
if (token) {
const previousToken = sourceCode.getTokenBefore(token);
if (ignoreEOLComments && astUtils.isCommentToken(token) && isLastTokenOfLine(token)) {
return;
}
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
if (hasExceptions) {
parent = sourceCode.getNodeByRangeIndex(pattern.lastIndex - 1);
}
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({
node: token,
loc: token.loc.start,
message: "Multiple spaces found before '{{value}}'.",
data: { value: token.value },
data: { value },
fix: createFix(previousToken, token)
});
}

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

@ -111,10 +111,19 @@ module.exports = {
message,
data: { max: maxAllowed, pluralizedLines: maxAllowed === 1 ? "line" : "lines" },
fix(fixer) {
return fixer.removeRange([
sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 }),
sourceCode.getIndexFromLoc({ line: lineNumber - maxAllowed, column: 0 })
]);
const rangeStart = sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, 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.
* @param {Scope} scope - An escope scope object.
* @param {Scope} scope - An eslint-scope scope object.
* @returns {void}
* @private
*/

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

@ -22,15 +22,28 @@ module.exports = {
},
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 {
BinaryExpression(node) {
const operators = ["===", "==", "!==", "!=", ">", "<", ">=", "<="];
const operators = new Set(["===", "==", "!==", "!=", ">", "<", ">=", "<="]);
if (operators.indexOf(node.operator) > -1 &&
(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)) {
if (operators.has(node.operator) && hasSameTokens(node.left, node.right)) {
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() {
return (
isInConstructorOfDerivedClass(funcInfo) &&
isInConstructorOfDerivedClass() &&
!funcInfo.codePath.currentSegments.every(isCalled)
);
}
@ -206,7 +206,7 @@ module.exports = {
* @returns {void}
*/
onCodePathSegmentStart(segment) {
if (!isInConstructorOfDerivedClass(funcInfo)) {
if (!isInConstructorOfDerivedClass()) {
return;
}
@ -230,7 +230,7 @@ module.exports = {
* @returns {void}
*/
onCodePathSegmentLoop(fromSegment, toSegment) {
if (!isInConstructorOfDerivedClass(funcInfo)) {
if (!isInConstructorOfDerivedClass()) {
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
* all references found.
* @param {escope.Scope} scope The scope to check.
* @param {eslint-scope.Scope} scope The scope to check.
* @returns {void}
*/
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 PROPERTY_MESSAGE = "Unexpected newline between object and [ of property access.";
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();
@ -75,6 +78,19 @@ module.exports = {
return;
}
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 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 SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/;
const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/;
/**
* @typedef {Object} LoopConditionInfo
* @property {escope.Reference} reference - The reference.
* @property {eslint-scope.Reference} reference - The reference.
* @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes
* that the reference is belonging to.
* @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.
*
* @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.
*/
function isWriteReference(reference) {
@ -77,7 +77,7 @@ function isUnmodifiedAndNotBelongToGroup(condition) {
* Checks whether or not a given reference is inside of a given node.
*
* @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.
*/
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.
*
* @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
* condition.
*/
@ -134,7 +134,7 @@ function hasDynamicExpressions(root) {
/**
* 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.
*/
function toLoopCondition(reference) {
@ -188,7 +188,7 @@ function toLoopCondition(reference) {
* Gets the function which encloses a given reference.
* 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.
*/
function getEncloseFunctionDeclaration(reference) {
@ -209,7 +209,7 @@ function getEncloseFunctionDeclaration(reference) {
* Updates the "modified" flags of given loop conditions with given modifiers.
*
* @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}
*/
function updateModifiedFlag(conditions, modifiers) {
@ -311,7 +311,7 @@ module.exports = {
* Finds unmodified references which are inside of a loop condition.
* 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}
*/
function checkReferences(variable) {

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

@ -134,7 +134,17 @@ module.exports = {
node,
loc: node.consequent.loc.start,
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.
* @param {Variable} variable - EScope variable object.
* @param {Variable} variable - eslint-scope variable object.
* @returns {boolean} True if the variable is exported, false if not.
* @private
*/
@ -134,7 +134,7 @@ module.exports = {
/**
* 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.
* @private
*/
@ -157,7 +157,7 @@ module.exports = {
/**
* 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
* @private
*/
@ -212,7 +212,7 @@ module.exports = {
* - The reference is inside of a function scope which is different from
* 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.
* @returns {ASTNode|null} The RHS node or null.
* @private
@ -322,7 +322,7 @@ module.exports = {
/**
* 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.
* @returns {boolean} The reference is a read to update itself.
* @private
@ -422,7 +422,7 @@ module.exports = {
/**
* 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.
*/
function isLastInNonIgnoredParameters(variable) {
@ -448,7 +448,7 @@ module.exports = {
/**
* 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.
* @returns {Variable[]} unused variables of the scope and descendant scopes.
* @private
@ -513,7 +513,7 @@ module.exports = {
}
// 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;
}
} else {
@ -540,7 +540,7 @@ module.exports = {
/**
* 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.
* @returns {number} The index of the variable name's location.
* @private
@ -561,7 +561,7 @@ module.exports = {
* Creates the correct location of a given variables.
* 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.
* @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.
*
* @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.
*/
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.
*
* @param {escope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check.
* @param {eslint-scope.Variable} variable - A variable to check.
* @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a class declaration.
*/
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.
* @param {escope.Variable} variable - A variable to check.
* @param {escope.Reference} reference - A reference to check.
* @param {eslint-scope.Variable} variable - A variable to check.
* @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a variable declaration.
*/
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.
* @param {escope.Variable} variable The variable that gets used before being defined
* @param {escope.Reference} reference The reference to the variable
* @param {eslint-scope.Variable} variable The variable that gets used before being defined
* @param {eslint-scope.Reference} reference The reference to the variable
* @returns {boolean} `true` if the usage should be reported
*/
function isForbidden(variable, reference) {
@ -250,7 +250,7 @@ module.exports = {
ruleDefinition["ArrowFunctionExpression:exit"] = function(node) {
if (node.body.type !== "BlockStatement") {
findVariables(node);
findVariables();
}
};
} else {

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

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

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

@ -75,7 +75,7 @@ module.exports = {
docs: {
description: "disallow unnecessary escape characters",
category: "Best Practices",
recommended: false
recommended: true
},
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
* hierarchy.
*
* @param {escope.Scope} scope - The scope to traverse.
* @returns {escope.Scope} a function scope or global scope containing the given
* @param {eslint-scope.Scope} scope - The scope to traverse.
* @returns {eslint-scope.Scope} a function scope or global scope containing the given
* scope.
*/
function getEnclosingFunctionScope(scope) {
@ -34,7 +34,7 @@ function getEnclosingFunctionScope(scope) {
* Checks whether the given variable has any references from a more specific
* 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.
*/
function isReferencedInClosure(variable) {
@ -93,7 +93,7 @@ function getScopeNode(node) {
/**
* 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.
*/
function isRedeclared(variable) {
@ -112,7 +112,7 @@ function isUsedFromOutsideOf(scopeNode) {
/**
* 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
* scope.
*/

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

@ -40,7 +40,8 @@ module.exports = {
create(context) {
const configuration = context.options[0] || {},
const sourceCode = context.getSourceCode(),
configuration = context.options[0] || {},
warningTerms = configuration.terms || ["todo", "fixme", "xxx"],
location = configuration.location || "start",
selfConfigRegEx = /\bno-warning-comments\b/;
@ -128,8 +129,11 @@ module.exports = {
}
return {
BlockComment: checkComment,
LineComment: checkComment
Program() {
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: {
type: "integer",
minimum: 0
},
consistent: {
type: "boolean"
}
},
additionalProperties: false,
@ -42,11 +45,12 @@ const OPTION_VALUE = {
* Normalizes a given option value.
*
* @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) {
let multiline = false;
let minProperties = Number.POSITIVE_INFINITY;
let consistent = false;
if (value) {
if (value === "always") {
@ -56,12 +60,13 @@ function normalizeOptionValue(value) {
} else {
multiline = Boolean(value.multiline);
minProperties = value.minProperties || Number.POSITIVE_INFINITY;
consistent = Boolean(value.consistent);
}
} else {
multiline = true;
}
return { multiline, minProperties };
return { multiline, minProperties, consistent };
}
/**
@ -172,7 +177,14 @@ module.exports = {
});
}
} 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({
message: "Unexpected line break after this opening brace.",
node,
@ -185,7 +197,10 @@ module.exports = {
}
});
}
if (!astUtils.isTokenOnSameLine(last, closeBrace)) {
if (
(!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
(consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
) {
context.report({
message: "Unexpected line break before this closing brace.",
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")) {
if (IGNORE_CONSTRUCTORS && isConstructor(node.key.name)) {
if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) {
return;
}
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";
if (typeof config === "string") {
options.blocks = config === "always";
const shouldHavePadding = config === "always";
options.blocks = shouldHavePadding;
options.switches = shouldHavePadding;
options.classes = shouldHavePadding;
} else {
if (config.hasOwnProperty("blocks")) {
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.
* @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.
*/
function isFunctionName(variable) {
@ -31,8 +31,8 @@ function checkMetaProperty(node, metaName, propertyName) {
/**
* Gets the variable object of `arguments` which is defined implicitly.
* @param {escope.Scope} scope - A scope to get.
* @returns {escope.Variable} The found variable object.
* @param {eslint-scope.Scope} scope - A scope to get.
* @returns {eslint-scope.Variable} The found variable object.
*/
function getVariableOfArguments(scope) {
const variables = scope.variables;
@ -159,7 +159,7 @@ module.exports = {
create(context) {
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 sourceCode = context.getSourceCode();
@ -277,15 +277,23 @@ module.exports = {
const paramsRightParen = sourceCode.getTokenBefore(node.body);
const asyncKeyword = node.async ? "async " : "";
const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
if (callbackInfo.isLexicalThis) {
// 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, only replace the `function` keyword and parameters with the arrow function parameters.
return fixer.replaceTextRange([node.start, node.body.start], `${asyncKeyword}${paramsFullText} => `);
const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
/*
* If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
* Otherwise, just replace the arrow function itself.
*/
const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
/*
* 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
* 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 -
* The value of `ignoreReadBeforeAssign` option.
* @returns {ASTNode|null}
@ -85,17 +85,6 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
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.
let writer = null;
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
* 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
* null.
*/
@ -172,7 +161,7 @@ function getDestructuringHost(reference) {
* This is used to detect a mix of reassigned and never reassigned in a
* destructuring.
*
* @param {escope.Variable[]} variables - Variables to group by destructuring.
* @param {eslint-scope.Variable[]} variables - Variables to group by destructuring.
* @param {boolean} ignoreReadBeforeAssign -
* The value of `ignoreReadBeforeAssign` option.
* @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
* be 2 or more.
*
* @param {(escope.Reference|null)[]} nodes -
* @param {(eslint-scope.Reference|null)[]} nodes -
* References which are grouped by destructuring to report.
* @returns {void}
*/

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

Loading…
Cancel
Save