Browse Source

tools: update ESLint to version 2.1.0

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

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

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

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

14
tools/eslint/README.md

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

22
tools/eslint/bin/eslint.js

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

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

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

48
tools/eslint/conf/environments.js

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

107
tools/eslint/conf/eslint.json

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

5
tools/eslint/conf/replacements.json

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

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

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

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

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

16
tools/eslint/lib/cli.js

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

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

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

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

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

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

File diff suppressed because it is too large

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

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

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

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

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

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

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

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

80
tools/eslint/lib/config.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

197
tools/eslint/lib/eslint.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

18
tools/eslint/lib/options.js

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

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

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

24
tools/eslint/lib/rules.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save