Added logging, changed some directory structure

This commit is contained in:
2018-01-13 21:33:40 -05:00
parent f079a5f067
commit 8e72ffb917
73656 changed files with 35284 additions and 53718 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
Copyright JS Foundation and other contributors, https://js.foundation
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.

View File

@@ -0,0 +1,254 @@
[![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)
[![Join the chat at https://gitter.im/eslint/eslint](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/eslint/eslint?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint?ref=badge_shield)
# ESLint
[Website](https://eslint.org) |
[Configuring](https://eslint.org/docs/user-guide/configuring) |
[Rules](https://eslint.org/docs/rules/) |
[Contributing](https://eslint.org/docs/developer-guide/contributing) |
[Reporting Bugs](https://eslint.org/docs/developer-guide/contributing/reporting-bugs) |
[Code of Conduct](https://js.foundation/community/code-of-conduct) |
[Twitter](https://twitter.com/geteslint) |
[Mailing List](https://groups.google.com/group/eslint) |
[Chat Room](https://gitter.im/eslint/eslint)
ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. In many ways, it is similar to JSLint and JSHint with a few exceptions:
* ESLint uses [Espree](https://github.com/eslint/espree) for JavaScript parsing.
* ESLint uses an AST to evaluate patterns in code.
* ESLint is completely pluggable, every single rule is a plugin and you can add more at runtime.
## Installation and Usage
Prerequisites: [Node.js](https://nodejs.org/en/) (>=4.x), npm version 2+.
There are two ways to install ESLint: globally and locally.
### Local Installation and Usage
If you want to include ESLint as part of your project's build system, we recommend installing it locally. You can do so using npm:
```
$ npm install eslint --save-dev
```
You should then setup a configuration file:
```
$ ./node_modules/.bin/eslint --init
```
After that, you can run ESLint on any file or directory like this:
```
$ ./node_modules/.bin/eslint yourfile.js
```
Any plugins or shareable configs that you use must also be installed locally to work with a locally-installed ESLint.
### Global Installation and Usage
If you want to make ESLint available to tools that run across all of your projects, we recommend installing ESLint globally. You can do so using npm:
```
$ npm install -g eslint
```
You should then setup a configuration file:
```
$ eslint --init
```
After that, you can run ESLint on any file or directory like this:
```
$ eslint yourfile.js
```
Any plugins or shareable configs that you use must also be installed globally to work with a globally-installed ESLint.
**Note:** `eslint --init` is intended for setting up and configuring ESLint on a per-project basis and will perform a local installation of ESLint and its plugins in the directory in which it is run. If you prefer using a global installation of ESLint, any plugins used in your configuration must also be installed globally.
## Configuration
After running `eslint --init`, you'll have a `.eslintrc` file in your directory. In it, you'll see some rules configured like this:
```json
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
```
The names `"semi"` and `"quotes"` are the names of [rules](https://eslint.org/docs/rules) in ESLint. The first value is the error level of the rule and can be one of these values:
* `"off"` or `0` - turn the rule off
* `"warn"` or `1` - turn the rule on as a warning (doesn't affect exit code)
* `"error"` or `2` - turn the rule on as an error (exit code will be 1)
The three error levels allow you fine-grained control over how ESLint applies rules (for more configuration options and details, see the [configuration docs](https://eslint.org/docs/user-guide/configuring)).
## Sponsors
* Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com)
## Team
These folks keep the project moving and are resources for help.
### Technical Steering Committee (TSC)
* Nicholas C. Zakas ([@nzakas](https://github.com/nzakas))
* Ilya Volodin ([@ilyavolodin](https://github.com/ilyavolodin))
* Brandon Mills ([@btmills](https://github.com/btmills))
* Gyandeep Singh ([@gyandeeps](https://github.com/gyandeeps))
* Toru Nagashima ([@mysticatea](https://github.com/mysticatea))
* Alberto Rodríguez ([@alberto](https://github.com/alberto))
* Kai Cataldo ([@kaicataldo](https://github.com/kaicataldo))
* Teddy Katz ([@not-an-aardvark](https://github.com/not-an-aardvark))
### Development Team
* Mathias Schreck ([@lo1tuma](https://github.com/lo1tuma))
* Jamund Ferguson ([@xjamundx](https://github.com/xjamundx))
* Ian VanSchooten ([@ianvs](https://github.com/ianvs))
* Burak Yiğit Kaya ([@byk](https://github.com/byk))
* Michael Ficarra ([@michaelficarra](https://github.com/michaelficarra))
* Mark Pedrotti ([@pedrottimark](https://github.com/pedrottimark))
* Oleg Gaidarenko ([@markelog](https://github.com/markelog))
* Mike Sherov ([@mikesherov](https://github.com/mikesherov))
* Henry Zhu ([@hzoo](https://github.com/hzoo))
* Marat Dulin ([@mdevils](https://github.com/mdevils))
* Alexej Yaroshevich ([@zxqfox](https://github.com/zxqfox))
* Kevin Partington ([@platinumazure](https://github.com/platinumazure))
* Vitor Balocco ([@vitorbal](https://github.com/vitorbal))
* James Henry ([@JamesHenry](https://github.com/JamesHenry))
* Reyad Attiyat ([@soda0289](https://github.com/soda0289))
* 薛定谔的猫 ([@Aladdin-ADD](https://github.com/Aladdin-ADD))
* Victor Hom ([@VictorHom](https://github.com/VictorHom))
## Releases
We have scheduled releases every two weeks on Friday or Saturday.
## Code of Conduct
ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
## Filing Issues
Before filing an issue, please be sure to read the guidelines for what you're reporting:
* [Bug Report](https://eslint.org/docs/developer-guide/contributing/reporting-bugs)
* [Propose a New Rule](https://eslint.org/docs/developer-guide/contributing/new-rules)
* [Proposing a Rule Change](https://eslint.org/docs/developer-guide/contributing/rule-changes)
* [Request a Change](https://eslint.org/docs/developer-guide/contributing/changes)
## Semantic Versioning Policy
ESLint follows [semantic versioning](http://semver.org). However, due to the nature of ESLint as a code quality tool, it's not always clear when a minor or major version bump occurs. To help clarify this for everyone, we've defined the following semantic versioning policy for ESLint:
* Patch release (intended to not break your lint build)
* A bug fix in a rule that results in ESLint reporting fewer errors.
* A bug fix to the CLI or core (including formatters).
* Improvements to documentation.
* Non-user-facing changes such as refactoring code, adding, deleting, or modifying tests, and increasing test coverage.
* Re-releasing after a failed release (i.e., publishing a release that doesn't work for anyone).
* Minor release (might break your lint build)
* A bug fix in a rule that results in ESLint reporting more errors.
* A new rule is created.
* A new option to an existing rule that does not result in ESLint reporting more errors by default.
* An existing rule is deprecated.
* A new CLI capability is created.
* New capabilities to the public API are added (new classes, new methods, new arguments to existing methods, etc.).
* A new formatter is created.
* Major release (likely to break your lint build)
* `eslint:recommended` is updated.
* A new option to an existing rule that results in ESLint reporting more errors by default.
* An existing formatter is removed.
* Part of the public API is removed or changed in an incompatible way.
According to our policy, any minor update may report more errors than the previous release (ex: from a bug fix). As such, we recommend using the tilde (`~`) in `package.json` e.g. `"eslint": "~3.1.0"` to guarantee the results of your builds.
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint?ref=badge_large)
## Frequently Asked Questions
### How is ESLint different from JSHint?
The most significant difference is that ESlint has pluggable linting rules. That means you can use the rules it comes with, or you can extend it with rules created by others or by yourself!
### How does ESLint performance compare to JSHint?
ESLint is slower than JSHint, usually 2-3x slower on a single file. This is because ESLint uses Espree to construct an AST before it can evaluate your code whereas JSHint evaluates your code as it's being parsed. The speed is also based on the number of rules you enable; the more rules you enable, the slower the process.
Despite being slower, we believe that ESLint is fast enough to replace JSHint without causing significant pain.
### I heard ESLint is going to replace JSCS?
Yes. Since we are solving the same problems, ESLint and JSCS teams have decided to join forces and work together in the development of ESLint instead of competing with each other. You can read more about this in both [ESLint](https://eslint.org/blog/2016/04/welcoming-jscs-to-eslint) and [JSCS](https://medium.com/@markelog/jscs-end-of-the-line-bc9bf0b3fdb2#.u76sx334n) announcements.
### So, should I stop using JSCS and start using ESLint?
Maybe, depending on how much you need it. [JSCS has reached end of life](https://eslint.org/blog/2016/07/jscs-end-of-life), but if it is working for you then there is no reason to move yet. We are still working to smooth the transition. You can see our progress [here](https://github.com/eslint/eslint/milestones/JSCS%20Compatibility). Well announce when all of the changes necessary to support JSCS users in ESLint are complete and will start encouraging JSCS users to switch to ESLint at that time.
If you are having issues with JSCS, you can try to move to ESLint. We are focusing our time and energy on JSCS compatibility issues.
### Is ESLint just linting or does it also check style?
ESLint does both traditional linting (looking for problematic patterns) and style checking (enforcement of conventions). You can use it for both.
### Why can't ESLint find my plugins?
ESLint can be [globally or locally installed](#installation-and-usage). If you install ESLint globally, your plugins must also be installed globally; if you install ESLint locally, your plugins must also be installed locally.
If you are trying to run globally, make sure your plugins are installed globally (use `npm ls -g`).
If you are trying to run locally:
* Make sure your plugins (and ESLint) are both in your project's `package.json` as devDependencies (or dependencies, if your project uses ESLint at runtime).
* Make sure you have run `npm install` and all your dependencies are installed.
In all cases, make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDepencies` to see what peer dependencies `eslint-plugin-myplugin` has.
### Does ESLint support JSX?
Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](https://eslint.org/docs/user-guide/configuring).). Please note that supporting JSX syntax *is not* the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics.
### What about ECMAScript 6 support?
ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 syntax and global variables through [configuration](https://eslint.org/docs/user-guide/configuring).
### What about experimental features?
ESLint doesn't natively support experimental ECMAScript language features. You can use [babel-eslint](https://github.com/babel/babel-eslint) to use any option available in Babel.
Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/developer-guide/contributing). Until then, please use the appropriate parser and plugin(s) for your experimental feature.
### Where to ask for help?
Join our [Mailing List](https://groups.google.com/group/eslint) or [Chatroom](https://gitter.im/eslint/eslint)
[npm-image]: https://img.shields.io/npm/v/eslint.svg?style=flat-square
[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
[downloads-url]: https://www.npmjs.com/package/eslint

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
/**
* @fileoverview Main CLI that is run via the eslint command.
* @author Nicholas C. Zakas
*/
/* eslint no-console:off */
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const useStdIn = (process.argv.indexOf("--stdin") > -1),
init = (process.argv.indexOf("--init") > -1),
debug = (process.argv.indexOf("--debug") > -1);
// must do this initialization *before* other requires in order to work
if (debug) {
require("debug").enable("eslint:*,-eslint:code-path");
}
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
// now we can safely include the other modules that use debug
const concat = require("concat-stream"),
cli = require("../lib/cli"),
path = require("path"),
fs = require("fs");
//------------------------------------------------------------------------------
// Execution
//------------------------------------------------------------------------------
process.once("uncaughtException", err => {
// lazy load
const lodash = require("lodash");
if (typeof err.messageTemplate === "string" && err.messageTemplate.length > 0) {
const template = lodash.template(fs.readFileSync(path.resolve(__dirname, `../messages/${err.messageTemplate}.txt`), "utf-8"));
const pkg = require("../package.json");
console.error("\nOops! Something went wrong! :(");
console.error(`\nESLint: ${pkg.version}.\n${template(err.messageData || {})}`);
} else {
console.error(err.message);
console.error(err.stack);
}
process.exitCode = 1;
});
if (useStdIn) {
process.stdin.pipe(concat({ encoding: "string" }, text => {
process.exitCode = cli.execute(process.argv, text);
}));
} else if (init) {
const configInit = require("../lib/config/config-initializer");
configInit.initializeConfig().then(() => {
process.exitCode = 0;
}).catch(err => {
process.exitCode = 1;
console.error(err.message);
console.error(err.stack);
});
} else {
process.exitCode = cli.execute(process.argv);
}

View File

@@ -0,0 +1,21 @@
{
"type": "Program",
"body": [],
"sourceType": "script",
"range": [
0,
0
],
"loc": {
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
}
},
"comments": [],
"tokens": []
}

View File

@@ -0,0 +1,40 @@
{
"categories": [
{ "name": "Possible Errors", "description": "These rules relate to possible syntax or logic errors in JavaScript code:" },
{ "name": "Best Practices", "description": "These rules relate to better ways of doing things to help you avoid problems:" },
{ "name": "Strict Mode", "description": "These rules relate to strict mode directives:" },
{ "name": "Variables", "description": "These rules relate to variable declarations:" },
{ "name": "Node.js and CommonJS", "description": "These rules relate to code running in Node.js, or in browsers with CommonJS:" },
{ "name": "Stylistic Issues", "description": "These rules relate to style guidelines, and are therefore quite subjective:" },
{ "name": "ECMAScript 6", "description": "These rules relate to ES6, also known as ES2015:" }
],
"deprecated": {
"name": "Deprecated",
"description": "These rules have been deprecated in accordance with the [deprecation policy](/docs/user-guide/rule-deprecation), and replaced by newer rules:",
"rules": []
},
"removed": {
"name": "Removed",
"description": "These rules from older versions of ESLint (before the [deprecation policy](/docs/user-guide/rule-deprecation) existed) have been replaced by newer rules:",
"rules": [
{ "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
{ "removed": "global-strict", "replacedBy": ["strict"] },
{ "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
{ "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
{ "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
{ "removed": "no-empty-label", "replacedBy": ["no-labels"] },
{ "removed": "no-extra-strict", "replacedBy": ["strict"] },
{ "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
{ "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
{ "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
{ "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
{ "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
{ "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
{ "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
]
}
}

View File

@@ -0,0 +1,70 @@
/**
* @fileoverview Defines a schema for configs.
* @author Sylvan Mably
*/
"use strict";
const baseConfigProperties = {
env: { type: "object" },
globals: { type: "object" },
parser: { type: ["string", "null"] },
parserOptions: { type: "object" },
plugins: { type: "array" },
rules: { type: "object" },
settings: { type: "object" },
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
};
const overrideProperties = Object.assign(
{},
baseConfigProperties,
{
files: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
minItems: 1
}
]
},
excludedFiles: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" }
}
]
}
}
);
const topLevelConfigProperties = Object.assign(
{},
baseConfigProperties,
{
extends: { type: ["string", "array"] },
root: { type: "boolean" },
overrides: {
type: "array",
items: {
type: "object",
properties: overrideProperties,
required: ["files"],
additionalProperties: false
}
}
}
);
const configSchema = {
type: "object",
properties: topLevelConfigProperties,
additionalProperties: false
};
module.exports = configSchema;

View File

@@ -0,0 +1,28 @@
/**
* @fileoverview Default CLIEngineOptions.
* @author Ian VanSchooten
*/
"use strict";
module.exports = {
configFile: null,
baseConfig: false,
rulePaths: [],
useEslintrc: true,
envs: [],
globals: [],
extensions: [".js"],
ignore: true,
ignorePath: null,
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,
reportUnusedDisableDirectives: false
};

View File

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

View File

@@ -0,0 +1,107 @@
/**
* @fileoverview Defines environment settings and globals.
* @author Elan Shanker
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const globals = require("globals");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
builtin: globals.es5,
browser: {
globals: globals.browser
},
node: {
globals: globals.node,
parserOptions: {
ecmaFeatures: {
globalReturn: true
}
}
},
commonjs: {
globals: globals.commonjs,
parserOptions: {
ecmaFeatures: {
globalReturn: true
}
}
},
"shared-node-browser": {
globals: globals["shared-node-browser"]
},
worker: {
globals: globals.worker
},
amd: {
globals: globals.amd
},
mocha: {
globals: globals.mocha
},
jasmine: {
globals: globals.jasmine
},
jest: {
globals: globals.jest
},
phantomjs: {
globals: globals.phantomjs
},
jquery: {
globals: globals.jquery
},
qunit: {
globals: globals.qunit
},
prototypejs: {
globals: globals.prototypejs
},
shelljs: {
globals: globals.shelljs
},
meteor: {
globals: globals.meteor
},
mongo: {
globals: globals.mongo
},
protractor: {
globals: globals.protractor
},
applescript: {
globals: globals.applescript
},
nashorn: {
globals: globals.nashorn
},
serviceworker: {
globals: globals.serviceworker
},
atomtest: {
globals: globals.atomtest
},
embertest: {
globals: globals.embertest
},
webextensions: {
globals: globals.webextensions
},
es6: {
globals: globals.es6,
parserOptions: {
ecmaVersion: 6
}
},
greasemonkey: {
globals: globals.greasemonkey
}
};

View File

@@ -0,0 +1,31 @@
/**
* @fileoverview Config to enable all rules.
* @author Robert Fletcher
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const load = require("../lib/load-rules"),
Rules = require("../lib/rules");
const rules = new Rules();
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const enabledRules = Object.keys(load()).reduce((result, ruleId) => {
if (!rules.get(ruleId).meta.deprecated) {
result[ruleId] = "error";
}
return result;
}, {});
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { rules: enabledRules };

View File

@@ -0,0 +1,270 @@
/**
* @fileoverview Configuration applied when a user configuration extends from
* eslint:recommended.
* @author Nicholas C. Zakas
*/
"use strict";
/* eslint sort-keys: ["error", "asc"] */
module.exports = {
rules: {
"accessor-pairs": "off",
"array-bracket-newline": "off",
"array-bracket-spacing": "off",
"array-callback-return": "off",
"array-element-newline": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"arrow-spacing": "off",
"block-scoped-var": "off",
"block-spacing": "off",
"brace-style": "off",
"callback-return": "off",
camelcase: "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": "off",
"comma-spacing": "off",
"comma-style": "off",
complexity: "off",
"computed-property-spacing": "off",
"consistent-return": "off",
"consistent-this": "off",
"constructor-super": "error",
curly: "off",
"default-case": "off",
"dot-location": "off",
"dot-notation": "off",
"eol-last": "off",
eqeqeq: "off",
"for-direction": "off",
"func-call-spacing": "off",
"func-name-matching": "off",
"func-names": "off",
"func-style": "off",
"function-paren-newline": "off",
"generator-star-spacing": "off",
"getter-return": "off",
"global-require": "off",
"guard-for-in": "off",
"handle-callback-err": "off",
"id-blacklist": "off",
"id-length": "off",
"id-match": "off",
indent: "off",
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "off",
"key-spacing": "off",
"keyword-spacing": "off",
"line-comment-position": "off",
"linebreak-style": "off",
"lines-around-comment": "off",
"lines-around-directive": "off",
"lines-between-class-members": "off",
"max-depth": "off",
"max-len": "off",
"max-lines": "off",
"max-nested-callbacks": "off",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-comment-style": "off",
"multiline-ternary": "off",
"new-cap": "off",
"new-parens": "off",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "off",
"no-array-constructor": "off",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-buffer-constructor": "off",
"no-caller": "off",
"no-case-declarations": "error",
"no-catch-shadow": "off",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-confusing-arrow": "off",
"no-console": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-continue": "off",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "off",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "off",
"no-else-return": "off",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-function": "off",
"no-empty-pattern": "error",
"no-eq-null": "off",
"no-eval": "off",
"no-ex-assign": "error",
"no-extend-native": "off",
"no-extra-bind": "off",
"no-extra-boolean-cast": "error",
"no-extra-label": "off",
"no-extra-parens": "off",
"no-extra-semi": "error",
"no-fallthrough": "error",
"no-floating-decimal": "off",
"no-func-assign": "error",
"no-global-assign": "error",
"no-implicit-coercion": "off",
"no-implicit-globals": "off",
"no-implied-eval": "off",
"no-inline-comments": "off",
"no-inner-declarations": "error",
"no-invalid-regexp": "error",
"no-invalid-this": "off",
"no-irregular-whitespace": "error",
"no-iterator": "off",
"no-label-var": "off",
"no-labels": "off",
"no-lone-blocks": "off",
"no-lonely-if": "off",
"no-loop-func": "off",
"no-magic-numbers": "off",
"no-mixed-operators": "off",
"no-mixed-requires": "off",
"no-mixed-spaces-and-tabs": "error",
"no-multi-assign": "off",
"no-multi-spaces": "off",
"no-multi-str": "off",
"no-multiple-empty-lines": "off",
"no-native-reassign": "off",
"no-negated-condition": "off",
"no-negated-in-lhs": "off",
"no-nested-ternary": "off",
"no-new": "off",
"no-new-func": "off",
"no-new-object": "off",
"no-new-require": "off",
"no-new-symbol": "error",
"no-new-wrappers": "off",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "off",
"no-param-reassign": "off",
"no-path-concat": "off",
"no-plusplus": "off",
"no-process-env": "off",
"no-process-exit": "off",
"no-proto": "off",
"no-prototype-builtins": "off",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-restricted-globals": "off",
"no-restricted-imports": "off",
"no-restricted-modules": "off",
"no-restricted-properties": "off",
"no-restricted-syntax": "off",
"no-return-assign": "off",
"no-return-await": "off",
"no-script-url": "off",
"no-self-assign": "error",
"no-self-compare": "off",
"no-sequences": "off",
"no-shadow": "off",
"no-shadow-restricted-names": "off",
"no-spaced-func": "off",
"no-sparse-arrays": "error",
"no-sync": "off",
"no-tabs": "off",
"no-template-curly-in-string": "off",
"no-ternary": "off",
"no-this-before-super": "error",
"no-throw-literal": "off",
"no-trailing-spaces": "off",
"no-undef": "error",
"no-undef-init": "off",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unexpected-multiline": "error",
"no-unmodified-loop-condition": "off",
"no-unneeded-ternary": "off",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"no-unused-vars": "error",
"no-use-before-define": "off",
"no-useless-call": "off",
"no-useless-computed-key": "off",
"no-useless-concat": "off",
"no-useless-constructor": "off",
"no-useless-escape": "error",
"no-useless-rename": "off",
"no-useless-return": "off",
"no-var": "off",
"no-void": "off",
"no-warning-comments": "off",
"no-whitespace-before-property": "off",
"no-with": "off",
"nonblock-statement-body-position": "off",
"object-curly-newline": "off",
"object-curly-spacing": "off",
"object-property-newline": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"operator-assignment": "off",
"operator-linebreak": "off",
"padded-blocks": "off",
"padding-line-between-statements": "off",
"prefer-arrow-callback": "off",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-numeric-literals": "off",
"prefer-promise-reject-errors": "off",
"prefer-reflect": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"prefer-template": "off",
"quote-props": "off",
quotes: "off",
radix: "off",
"require-await": "off",
"require-jsdoc": "off",
"require-yield": "error",
"rest-spread-spacing": "off",
semi: "off",
"semi-spacing": "off",
"semi-style": "off",
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-in-parens": "off",
"space-infix-ops": "off",
"space-unary-ops": "off",
"spaced-comment": "off",
strict: "off",
"switch-colon-spacing": "off",
"symbol-description": "off",
"template-curly-spacing": "off",
"template-tag-spacing": "off",
"unicode-bom": "off",
"use-isnan": "error",
"valid-jsdoc": "off",
"valid-typeof": "error",
"vars-on-top": "off",
"wrap-iife": "off",
"wrap-regex": "off",
"yield-star-spacing": "off",
yoda: "off"
}
};

View File

@@ -0,0 +1,22 @@
{
"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"]
}
}

View File

@@ -0,0 +1,16 @@
/**
* @fileoverview Expose out ESLint and CLI to require.
* @author Ian Christian Myers
*/
"use strict";
const Linter = require("./linter");
module.exports = {
linter: new Linter(),
Linter,
CLIEngine: require("./cli-engine"),
RuleTester: require("./testers/rule-tester"),
SourceCode: require("./util/source-code")
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,700 @@
/**
* @fileoverview Main CLI object.
* @author Nicholas C. Zakas
*/
"use strict";
/*
* The CLI object should *not* call process.exit() directly. It should only return
* exit codes. This allows other programs to use the CLI object and still control
* when the program exits.
*/
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
defaultOptions = require("../conf/default-cli-options"),
Linter = require("./linter"),
IgnoredPaths = require("./ignored-paths"),
Config = require("./config"),
fileEntryCache = require("file-entry-cache"),
globUtil = require("./util/glob-util"),
validator = require("./config/config-validator"),
stringify = require("json-stable-stringify"),
hash = require("./util/hash"),
pkg = require("../package.json");
const debug = require("debug")("eslint:cli-engine");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* The options to configure a CLI engine with.
* @typedef {Object} CLIEngineOptions
* @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
* @property {boolean|Object} baseConfig Base config object. True enables recommend rules and environments.
* @property {boolean} cache Enable result caching.
* @property {string} cacheLocation The cache file to use instead of .eslintcache.
* @property {string} configFile The configuration file to use.
* @property {string} cwd The value to use for the current working directory.
* @property {string[]} envs An array of environments to load.
* @property {string[]} extensions An array of file extensions to check.
* @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
* @property {string[]} globals An array of global variables to declare.
* @property {boolean} ignore False disables use of .eslintignore.
* @property {string} ignorePath The ignore file to use instead of .eslintignore.
* @property {string} ignorePattern A glob pattern of files to ignore.
* @property {boolean} useEslintrc False disables looking for .eslintrc
* @property {string} parser The name of the parser to use.
* @property {Object} parserOptions An object of parserOption settings to use.
* @property {string[]} plugins An array of plugins to load.
* @property {Object<string,*>} rules An object of rules to use.
* @property {string[]} rulePaths An array of directories to load custom rules from.
* @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
*/
/**
* A linting warning or error.
* @typedef {Object} LintMessage
* @property {string} message The message to display to the user.
*/
/**
* A linting result.
* @typedef {Object} LintResult
* @property {string} filePath The path to the file that was linted.
* @property {LintMessage[]} messages All of the messages for the result.
* @property {number} errorCount Number of errors for the result.
* @property {number} warningCount Number of warnings for the result.
* @property {number} fixableErrorCount Number of fixable errors for the result.
* @property {number} fixableWarningCount Number of fixable warnings for the result.
* @property {string=} [source] The source code of the file that was linted.
* @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* It will calculate the error and warning count for collection of messages per file
* @param {Object[]} messages - Collection of messages
* @returns {Object} Contains the stats
* @private
*/
function calculateStatsPerFile(messages) {
return messages.reduce((stat, message) => {
if (message.fatal || message.severity === 2) {
stat.errorCount++;
if (message.fix) {
stat.fixableErrorCount++;
}
} else {
stat.warningCount++;
if (message.fix) {
stat.fixableWarningCount++;
}
}
return stat;
}, {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
});
}
/**
* It will calculate the error and warning count for collection of results from all files
* @param {Object[]} results - Collection of messages from all the files
* @returns {Object} Contains the stats
* @private
*/
function calculateStatsPerRun(results) {
return results.reduce((stat, result) => {
stat.errorCount += result.errorCount;
stat.warningCount += result.warningCount;
stat.fixableErrorCount += result.fixableErrorCount;
stat.fixableWarningCount += result.fixableWarningCount;
return stat;
}, {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
});
}
/**
* Processes an source code using ESLint.
* @param {string} text The source code to check.
* @param {Object} configHelper The configuration options for ESLint.
* @param {string} filename An optional string representing the texts filename.
* @param {boolean|Function} fix Indicates if fixes should be processed.
* @param {boolean} allowInlineConfig Allow/ignore comments that change config.
* @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this text.
* @private
*/
function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) {
let filePath,
fileExtension,
processor;
if (filename) {
filePath = path.resolve(filename);
fileExtension = path.extname(filename);
}
filename = filename || "<text>";
debug(`Linting ${filename}`);
const config = configHelper.getConfig(filePath);
if (config.plugins) {
configHelper.plugins.loadAll(config.plugins);
}
const loadedPlugins = configHelper.plugins.getAll();
for (const plugin in loadedPlugins) {
if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
processor = loadedPlugins[plugin].processors[fileExtension];
break;
}
}
const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);
const fixedResult = linter.verifyAndFix(text, config, {
filename,
allowInlineConfig,
reportUnusedDisableDirectives,
fix: !!autofixingEnabled && fix,
preprocess: processor && (rawText => processor.preprocess(rawText, filename)),
postprocess: processor && (problemLists => processor.postprocess(problemLists, filename))
});
const stats = calculateStatsPerFile(fixedResult.messages);
const result = {
filePath: filename,
messages: fixedResult.messages,
errorCount: stats.errorCount,
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
};
if (fixedResult.fixed) {
result.output = fixedResult.output;
}
if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
result.source = text;
}
return result;
}
/**
* Processes an individual file using ESLint. Files used here are known to
* exist, so no need to check that here.
* @param {string} filename The filename of the file being checked.
* @param {Object} configHelper The configuration options for ESLint.
* @param {Object} options The CLIEngine options object.
* @param {Linter} linter Linter context
* @returns {LintResult} The results for linting on this file.
* @private
*/
function processFile(filename, configHelper, options, linter) {
const text = fs.readFileSync(path.resolve(filename), "utf8"),
result = processText(
text,
configHelper,
filename,
options.fix,
options.allowInlineConfig,
options.reportUnusedDisableDirectives,
linter
);
return result;
}
/**
* Returns result with warning by ignore settings
* @param {string} filePath - File path of checked code
* @param {string} baseDir - Absolute path of base directory
* @returns {LintResult} Result with single warning
* @private
*/
function createIgnoreResult(filePath, baseDir) {
let message;
const isHidden = /^\./.test(path.basename(filePath));
const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components");
if (isHidden) {
message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
} else if (isInNodeModules) {
message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
} else if (isInBowerComponents) {
message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
} else {
message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
}
return {
filePath: path.resolve(filePath),
messages: [
{
fatal: false,
severity: 1,
message
}
],
errorCount: 0,
warningCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0
};
}
/**
* Checks if the given message is an error message.
* @param {Object} message The message to check.
* @returns {boolean} Whether or not the message is an error message.
* @private
*/
function isErrorMessage(message) {
return message.severity === 2;
}
/**
* return the cacheFile to be used by eslint, based on whether the provided parameter is
* a directory or looks like a directory (ends in `path.sep`), in which case the file
* name will be the `cacheFile/.cache_hashOfCWD`
*
* 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, cwd) {
/*
* make sure the path separators are normalized for the environment/os
* keeping the trailing path separator if present
*/
cacheFile = path.normalize(cacheFile);
const resolvedCacheFile = path.resolve(cwd, cacheFile);
const looksLikeADirectory = cacheFile[cacheFile.length - 1] === path.sep;
/**
* return the name for the cache file in case the provided parameter is a directory
* @returns {string} the resolved path to the cacheFile
*/
function getCacheFileForDirectory() {
return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
}
let fileStats;
try {
fileStats = fs.lstatSync(resolvedCacheFile);
} catch (ex) {
fileStats = null;
}
/*
* in case the file exists we need to verify if the provided path
* is a directory or a file. If it is a directory we want to create a file
* inside that directory
*/
if (fileStats) {
/*
* is a directory or is a file, but the original file the user provided
* looks like a directory but `path.resolve` removed the `last path.sep`
* so we need to still treat this like a directory
*/
if (fileStats.isDirectory() || looksLikeADirectory) {
return getCacheFileForDirectory();
}
// is file so just use that file
return resolvedCacheFile;
}
/*
* here we known the file or directory doesn't exist,
* so we will try to infer if its a directory if it looks like a directory
* for the current operating system.
*/
// if the last character passed is a path separator we assume is a directory
if (looksLikeADirectory) {
return getCacheFileForDirectory();
}
return resolvedCacheFile;
}
const configHashCache = new WeakMap();
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class CLIEngine {
/**
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} options The options for this instance.
* @constructor
*/
constructor(options) {
options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
options
);
/**
* Stored options for this instance
* @type {Object}
*/
this.options = options;
this.linter = new Linter();
if (options.cache) {
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
/**
* Cache used to avoid operating on files that haven't changed since the
* last successful execution (e.g., file passed linting with no errors and
* no warnings).
* @type {Object}
*/
this._fileCache = fileEntryCache.create(cacheFile);
}
// load in additional rules
if (this.options.rulePaths) {
const cwd = this.options.cwd;
this.options.rulePaths.forEach(rulesdir => {
debug(`Loading rules from ${rulesdir}`);
this.linter.rules.load(rulesdir, cwd);
});
}
Object.keys(this.options.rules || {}).forEach(name => {
validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
});
this.config = new Config(this.options, this.linter);
}
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
static getErrorResults(results) {
const filtered = [];
results.forEach(result => {
const filteredMessages = result.messages.filter(isErrorMessage);
if (filteredMessages.length > 0) {
filtered.push(
Object.assign(result, {
messages: filteredMessages,
errorCount: filteredMessages.length,
warningCount: 0,
fixableErrorCount: result.fixableErrorCount,
fixableWarningCount: 0
})
);
}
});
return filtered;
}
/**
* Outputs fixes from the given results to files.
* @param {Object} report The report object created by CLIEngine.
* @returns {void}
*/
static outputFixes(report) {
report.results.filter(result => result.hasOwnProperty("output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
}
/**
* Add a plugin by passing its configuration
* @param {string} name Name of the plugin.
* @param {Object} pluginobject Plugin configuration object.
* @returns {void}
*/
addPlugin(name, pluginobject) {
this.config.plugins.define(name, pluginobject);
}
/**
* Resolves the patterns passed into executeOnFiles() into glob-based patterns
* for easier handling.
* @param {string[]} patterns The file patterns passed on the command line.
* @returns {string[]} The equivalent glob patterns.
*/
resolveFileGlobPatterns(patterns) {
return globUtil.resolveFileGlobPatterns(patterns, this.options);
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
* @returns {Object} The results for all files that were linted.
*/
executeOnFiles(patterns) {
const options = this.options,
fileCache = this._fileCache,
configHelper = this.config;
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
if (!options.cache && fs.existsSync(cacheFile)) {
fs.unlinkSync(cacheFile);
}
/**
* Calculates the hash of the config file used to validate a given file
* @param {string} filename The path of the file to retrieve a config object for to calculate the hash
* @returns {string} the hash of the config
*/
function hashOfConfigFor(filename) {
const config = configHelper.getConfig(filename);
if (!configHashCache.has(config)) {
configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`));
}
return configHashCache.get(config);
}
const startTime = Date.now();
const fileList = globUtil.listFilesToProcess(this.resolveFileGlobPatterns(patterns), options);
const results = fileList.map(fileInfo => {
if (fileInfo.ignored) {
return createIgnoreResult(fileInfo.filename, options.cwd);
}
if (options.cache) {
/*
* get the descriptor for this file
* with the metadata and the flag that determines if
* the file has changed
*/
const descriptor = fileCache.getFileDescriptor(fileInfo.filename);
const hashOfConfig = hashOfConfigFor(fileInfo.filename);
const changed = descriptor.changed || descriptor.meta.hashOfConfig !== hashOfConfig;
if (!changed) {
debug(`Skipping file since hasn't changed: ${fileInfo.filename}`);
/*
* Add the the cached results (always will be 0 error and
* 0 warnings). We should not cache results for files that
* failed, in order to guarantee that next execution will
* process those files as well.
*/
return descriptor.meta.results;
}
}
debug(`Processing ${fileInfo.filename}`);
return processFile(fileInfo.filename, configHelper, options, this.linter);
});
if (options.cache) {
results.forEach(result => {
if (result.messages.length) {
/*
* if a file contains errors or warnings we don't want to
* store the file in the cache so we can guarantee that
* next execution will also operate on this file
*/
fileCache.removeEntry(result.filePath);
} else {
/*
* since the file passed we store the result here
* TODO: it might not be necessary to store the results list in the cache,
* since it should always be 0 errors/warnings
*/
const descriptor = fileCache.getFileDescriptor(result.filePath);
descriptor.meta.hashOfConfig = hashOfConfigFor(result.filePath);
descriptor.meta.results = result;
}
});
// persist the cache to disk
fileCache.reconcile();
}
const stats = calculateStatsPerRun(results);
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
errorCount: stats.errorCount,
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
};
}
/**
* Executes the current configuration on text.
* @param {string} text A string of JavaScript code to lint.
* @param {string} filename An optional string representing the texts filename.
* @param {boolean} warnIgnored Always warn when a file is ignored
* @returns {Object} The results for the linting.
*/
executeOnText(text, filename, warnIgnored) {
const results = [],
options = this.options,
configHelper = this.config,
ignoredPaths = new IgnoredPaths(options);
// resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
if (filename && !path.isAbsolute(filename)) {
filename = path.resolve(options.cwd, filename);
}
if (filename && ignoredPaths.contains(filename)) {
if (warnIgnored) {
results.push(createIgnoreResult(filename, options.cwd));
}
} else {
results.push(
processText(
text,
configHelper,
filename,
options.fix,
options.allowInlineConfig,
options.reportUnusedDisableDirectives,
this.linter
)
);
}
const stats = calculateStatsPerRun(results);
return {
results,
errorCount: stats.errorCount,
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount
};
}
/**
* Returns a configuration object for the given file based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} filePath The path of the file to retrieve a config object for.
* @returns {Object} A configuration object for the file.
*/
getConfigForFile(filePath) {
const configHelper = this.config;
return configHelper.getConfig(filePath);
}
/**
* Checks if a given path is ignored by ESLint.
* @param {string} filePath The path of the file to check.
* @returns {boolean} Whether or not the given path is ignored.
*/
isPathIgnored(filePath) {
const resolvedPath = path.resolve(this.options.cwd, filePath);
const ignoredPaths = new IgnoredPaths(this.options);
return ignoredPaths.contains(resolvedPath);
}
/**
* Returns the formatter representing the given format or null if no formatter
* with the given name can be found.
* @param {string} [format] The name of the format to load or the path to a
* custom formatter.
* @returns {Function} The formatter function or null if not found.
*/
getFormatter(format) {
// default is stylish
format = format || "stylish";
// only strings are valid formatters
if (typeof format === "string") {
// replace \ with / for Windows compatibility
format = format.replace(/\\/g, "/");
let formatterPath;
// if there's a slash, then it's a file
if (format.indexOf("/") > -1) {
const cwd = this.options ? this.options.cwd : process.cwd();
formatterPath = path.resolve(cwd, format);
} else {
formatterPath = `./formatters/${format}`;
}
try {
return require(formatterPath);
} catch (ex) {
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
throw ex;
}
} else {
return null;
}
}
}
CLIEngine.version = pkg.version;
CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
module.exports = CLIEngine;

View File

@@ -0,0 +1,219 @@
/**
* @fileoverview Main CLI object.
* @author Nicholas C. Zakas
*/
"use strict";
/*
* The CLI object should *not* call process.exit() directly. It should only return
* exit codes. This allows other programs to use the CLI object and still control
* when the program exits.
*/
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
options = require("./options"),
CLIEngine = require("./cli-engine"),
mkdirp = require("mkdirp"),
log = require("./logging");
const debug = require("debug")("eslint:cli");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Predicate function for whether or not to apply fixes in quiet mode.
* If a message is a warning, do not apply a fix.
* @param {LintResult} lintResult The lint result.
* @returns {boolean} True if the lint message is an error (and thus should be
* autofixed), false otherwise.
*/
function quietFixPredicate(lintResult) {
return lintResult.severity === 2;
}
/**
* Translates the CLI options into the options expected by the CLIEngine.
* @param {Object} cliOptions The CLI options to translate.
* @returns {CLIEngineOptions} The options object for the CLIEngine.
* @private
*/
function translateOptions(cliOptions) {
return {
envs: cliOptions.env,
extensions: cliOptions.ext,
rules: cliOptions.rule,
plugins: cliOptions.plugin,
globals: cliOptions.global,
ignore: cliOptions.ignore,
ignorePath: cliOptions.ignorePath,
ignorePattern: cliOptions.ignorePattern,
configFile: cliOptions.config,
rulePaths: cliOptions.rulesdir,
useEslintrc: cliOptions.eslintrc,
parser: cliOptions.parser,
parserOptions: cliOptions.parserOptions,
cache: cliOptions.cache,
cacheFile: cliOptions.cacheFile,
cacheLocation: cliOptions.cacheLocation,
fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true),
allowInlineConfig: cliOptions.inlineConfig,
reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives
};
}
/**
* Outputs the results of the linting.
* @param {CLIEngine} engine The CLIEngine to use.
* @param {LintResult[]} results The results to print.
* @param {string} format The name of the formatter to use or the path to the formatter.
* @param {string} outputFile The path for the output file.
* @returns {boolean} True if the printing succeeds, false if not.
* @private
*/
function printResults(engine, results, format, outputFile) {
let formatter;
try {
formatter = engine.getFormatter(format);
} catch (e) {
log.error(e.message);
return false;
}
const output = formatter(results);
if (output) {
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false;
}
try {
mkdirp.sync(path.dirname(filePath));
fs.writeFileSync(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else {
log.info(output);
}
}
return true;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
* for other Node.js programs to effectively run the CLI.
*/
const cli = {
/**
* Executes the CLI based on an array of arguments that is passed in.
* @param {string|Array|Object} args The arguments to process.
* @param {string} [text] The text to lint (used for TTY).
* @returns {int} The exit code for the operation.
*/
execute(args, text) {
let currentOptions;
try {
currentOptions = options.parse(args);
} catch (error) {
log.error(error.message);
return 1;
}
const files = currentOptions._;
const useStdin = typeof text === "string";
if (currentOptions.version) { // version from package.json
log.info(`v${require("../package.json").version}`);
} else if (currentOptions.printConfig) {
if (files.length) {
log.error("The --print-config option must be used with exactly one file name.");
return 1;
}
if (useStdin) {
log.error("The --print-config option is not available for piped-in code.");
return 1;
}
const engine = new CLIEngine(translateOptions(currentOptions));
const fileConfig = engine.getConfigForFile(currentOptions.printConfig);
log.info(JSON.stringify(fileConfig, null, " "));
return 0;
} else if (currentOptions.help || (!files.length && !useStdin)) {
log.info(options.generateHelp());
} else {
debug(`Running on ${useStdin ? "text" : "files"}`);
if (currentOptions.fix && currentOptions.fixDryRun) {
log.error("The --fix option and the --fix-dry-run option cannot be used together.");
return 1;
}
if (useStdin && currentOptions.fix) {
log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
return 1;
}
const engine = new CLIEngine(translateOptions(currentOptions));
const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files);
if (currentOptions.fix) {
debug("Fix mode enabled - applying fixes");
CLIEngine.outputFixes(report);
}
if (currentOptions.quiet) {
debug("Quiet mode enabled - filtering out warnings");
report.results = CLIEngine.getErrorResults(report.results);
}
if (printResults(engine, report.results, currentOptions.format, currentOptions.outputFile)) {
const tooManyWarnings = currentOptions.maxWarnings >= 0 && report.warningCount > currentOptions.maxWarnings;
if (!report.errorCount && tooManyWarnings) {
log.error("ESLint found too many warnings (maximum: %s).", currentOptions.maxWarnings);
}
return (report.errorCount || tooManyWarnings) ? 1 : 0;
}
return 1;
}
return 0;
}
};
module.exports = cli;

View File

@@ -0,0 +1,655 @@
/**
* @fileoverview A class of the code path analyzer.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const 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) {
const 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) {
const 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) {
const codePath = analyzer.codePath;
const state = CodePath.getState(codePath);
const currentSegments = state.currentSegments;
const headSegments = state.headSegments;
const end = Math.max(currentSegments.length, headSegments.length);
let 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) {
const state = CodePath.getState(analyzer.codePath);
const currentSegments = state.currentSegments;
for (let i = 0; i < currentSegments.length; ++i) {
const 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) {
const codePath = analyzer.codePath;
const state = CodePath.getState(codePath);
const 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) {
let codePath = analyzer.codePath;
let state = codePath && CodePath.getState(codePath);
const 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) {
const codePath = analyzer.codePath;
const state = CodePath.getState(codePath);
let 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;
}
// Emits onCodePathSegmentStart events if updated.
if (!dontForward) {
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": {
let 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.
*/
class CodePathAnalyzer {
/**
* @param {EventGenerator} eventGenerator - An event generator to wrap.
*/
constructor(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);
}
/**
* 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(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(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(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;

View File

@@ -0,0 +1,243 @@
/**
* @fileoverview A class of the code path segment.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("./debug-helpers");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* 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.
*/
class CodePathSegment {
/**
* @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.
*/
constructor(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,
loopedPrevSegments: []
}
});
/* istanbul ignore if */
if (debug.enabled) {
this.internal.nodes = [];
this.internal.exitNodes = [];
}
}
/**
* Checks a given previous segment is coming from the end of a loop.
*
* @param {CodePathSegment} segment - A previous segment to check.
* @returns {boolean} `true` if the segment is coming from the end of a loop.
*/
isLoopedPrevSegment(segment) {
return this.internal.loopedPrevSegments.indexOf(segment) !== -1;
}
/**
* Creates the root segment.
*
* @param {string} id - An identifier.
* @returns {CodePathSegment} The created segment.
*/
static newRoot(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.
*/
static newNext(id, allPrevSegments) {
return new CodePathSegment(
id,
CodePathSegment.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.
*/
static newUnreachable(id, allPrevSegments) {
const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false);
// In `if (a) return a; foo();` case, the unreachable segment preceded by
// the return statement is not used but must not be remove.
CodePathSegment.markUsed(segment);
return segment;
}
/**
* 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.
*/
static newDisconnected(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}
*/
static markUsed(segment) {
if (segment.internal.used) {
return;
}
segment.internal.used = true;
let i;
if (segment.reachable) {
for (i = 0; i < segment.allPrevSegments.length; ++i) {
const 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);
}
}
}
/**
* Marks a previous segment as looped.
*
* @param {CodePathSegment} segment - A segment.
* @param {CodePathSegment} prevSegment - A previous segment to mark.
* @returns {void}
*/
static markPrevSegmentAsLooped(segment, prevSegment) {
segment.internal.loopedPrevSegments.push(prevSegment);
}
/**
* 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.
*/
static flattenUnusedSegments(segments) {
const done = Object.create(null);
const retv = [];
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
// Ignores duplicated.
if (done[segment.id]) {
continue;
}
// Use previous segments if unused.
if (!segment.internal.used) {
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
const 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;
}
}
module.exports = CodePathSegment;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,234 @@
/**
* @fileoverview A class of the code path.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const CodePathState = require("./code-path-state");
const IdGenerator = require("./id-generator");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A code path.
*/
class CodePath {
/**
* @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.
*/
constructor(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);
}
}
/**
* Gets the state of a given code path.
*
* @param {CodePath} codePath - A code path to get.
* @returns {CodePathState} The state of the code path.
*/
static getState(codePath) {
return codePath.internal;
}
/**
* 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;
}
/**
* Traverses all segments in this code path.
*
* codePath.traverseSegments(function(segment, controller) {
* // do something.
* });
*
* This method enumerates segments in order from the head.
*
* The `controller` object has two methods.
*
* - `controller.skip()` - Skip the following segments in this branch.
* - `controller.break()` - Skip all following segments.
*
* @param {Object} [options] - Omittable.
* @param {CodePathSegment} [options.first] - The first segment to traverse.
* @param {CodePathSegment} [options.last] - The last segment to traverse.
* @param {Function} callback - A callback function.
* @returns {void}
*/
traverseSegments(options, callback) {
if (typeof options === "function") {
callback = options;
options = null;
}
options = options || {};
const startSegment = options.first || this.internal.initialSegment;
const lastSegment = options.last;
let item = null;
let index = 0;
let end = 0;
let segment = null;
const visited = Object.create(null);
const stack = [[startSegment, 0]];
let skippedSegment = null;
let broken = false;
const controller = {
skip() {
if (stack.length <= 1) {
broken = true;
} else {
skippedSegment = stack[stack.length - 2][0];
}
},
break() {
broken = true;
}
};
/**
* Checks a given previous segment has been visited.
* @param {CodePathSegment} prevSegment - A previous segment to check.
* @returns {boolean} `true` if the segment has been visited.
*/
function isVisited(prevSegment) {
return (
visited[prevSegment.id] ||
segment.isLoopedPrevSegment(prevSegment)
);
}
while (stack.length > 0) {
item = stack[stack.length - 1];
segment = item[0];
index = item[1];
if (index === 0) {
// Skip if this segment has been visited already.
if (visited[segment.id]) {
stack.pop();
continue;
}
// Skip if all previous segments have not been visited.
if (segment !== startSegment &&
segment.prevSegments.length > 0 &&
!segment.prevSegments.every(isVisited)
) {
stack.pop();
continue;
}
// Reset the flag of skipping if all branches have been skipped.
if (skippedSegment && segment.prevSegments.indexOf(skippedSegment) !== -1) {
skippedSegment = null;
}
visited[segment.id] = true;
// Call the callback when the first time.
if (!skippedSegment) {
callback.call(this, segment, controller);
if (segment === lastSegment) {
controller.skip();
}
if (broken) {
break;
}
}
}
// Update the stack.
end = segment.nextSegments.length - 1;
if (index < end) {
item[1] += 1;
stack.push([segment.nextSegments[index], 0]);
} else if (index === end) {
item[0] = segment.nextSegments[index];
item[1] = 0;
} else {
stack.pop();
}
}
}
}
module.exports = CodePath;

View File

@@ -0,0 +1,200 @@
/**
* @fileoverview Helpers to debug for code path analysis.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const 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 (let i = 0; i < state.currentSegments.length; ++i) {
const 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" : ""}`
].join(" "));
},
/**
* 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) {
let 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";
}
const traceMap = Object.create(null);
const arrows = this.makeDotArrows(codePath, traceMap);
for (const id in traceMap) { // eslint-disable-line guard-for-in
const 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 || segment.internal.exitNodes.length > 0) {
text += [].concat(
segment.internal.nodes.map(node => {
switch (node.type) {
case "Identifier": return `${node.type} (${node.name})`;
case "Literal": return `${node.type} (${node.value})`;
default: return node.type;
}
}),
segment.internal.exitNodes.map(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(codePath, traceMap) {
const stack = [[codePath.initialSegment, 0]];
const done = traceMap || Object.create(null);
let lastId = codePath.initialSegment.id;
let text = `initial->${codePath.initialSegment.id}`;
while (stack.length > 0) {
const item = stack.pop();
const segment = item[0];
const index = item[1];
if (done[segment.id] && index === 0) {
continue;
}
done[segment.id] = segment;
const 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(finalSegment => {
if (lastId === finalSegment.id) {
text += "->final";
} else {
text += `;\n${finalSegment.id}->final`;
}
lastId = null;
});
codePath.thrownSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->thrown";
} else {
text += `;\n${finalSegment.id}->thrown`;
}
lastId = null;
});
return `${text};`;
}
};

View File

@@ -0,0 +1,262 @@
/**
* @fileoverview A class to operate forking.
*
* This is state of forking.
* This has a fork list and manages it.
*
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const 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) {
const list = context.segmentsList;
if (begin < 0) {
begin = list.length + begin;
}
if (end < 0) {
end = list.length + end;
}
const segments = [];
for (let i = 0; i < context.count; ++i) {
const allPrevSegments = [];
for (let 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) {
const merged = [];
for (let 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.
*/
class ForkContext {
/**
* @param {IdGenerator} idGenerator - An identifier generator for segments.
* @param {ForkContext|null} upper - An upper fork context.
* @param {number} count - A number of parallel segments.
*/
constructor(idGenerator, upper, count) {
this.idGenerator = idGenerator;
this.upper = upper;
this.count = count;
this.segmentsList = [];
}
/**
* The head segments.
* @type {CodePathSegment[]}
*/
get head() {
const 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() {
const 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(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(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(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(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(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(context) {
assert(context.count === this.count);
const source = context.segmentsList;
for (let i = 0; i < source.length; ++i) {
this.segmentsList.push(source[i]);
}
}
/**
* Clears all secments in this context.
*
* @returns {void}
*/
clear() {
this.segmentsList = [];
}
/**
* Creates the root fork context.
*
* @param {IdGenerator} idGenerator - An identifier generator for segments.
* @returns {ForkContext} New fork context.
*/
static newRoot(idGenerator) {
const 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.
*/
static newEmpty(parentContext, forkLeavingPath) {
return new ForkContext(
parentContext.idGenerator,
parentContext,
(forkLeavingPath ? 2 : 1) * parentContext.count
);
}
}
module.exports = ForkContext;

View File

@@ -0,0 +1,46 @@
/**
* @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
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A generator for unique ids.
*/
class IdGenerator {
/**
* @param {string} prefix - Optional. A prefix of generated ids.
*/
constructor(prefix) {
this.prefix = String(prefix);
this.n = 0;
}
/**
* Generates id.
*
* @returns {string} A generated id.
*/
next() {
this.n = 1 + this.n | 0;
/* istanbul ignore if */
if (this.n < 0) {
this.n = 1;
}
return this.prefix + this.n;
}
}
module.exports = IdGenerator;

View File

@@ -0,0 +1,361 @@
/**
* @fileoverview Responsible for loading config files
* @author Seth McLaughlin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("path"),
os = require("os"),
ConfigOps = require("./config/config-ops"),
ConfigFile = require("./config/config-file"),
ConfigCache = require("./config/config-cache"),
Plugins = require("./config/plugins"),
FileFinder = require("./file-finder"),
isResolvable = require("is-resolvable");
const debug = require("debug")("eslint:config");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const PERSONAL_CONFIG_DIR = os.homedir();
const SUBCONFIG_SEP = ":";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines if any rules were explicitly passed in as options.
* @param {Object} options The options used to create our configuration.
* @returns {boolean} True if rules were passed in as options, false otherwise.
* @private
*/
function hasRules(options) {
return options.rules && Object.keys(options.rules).length > 0;
}
//------------------------------------------------------------------------------
// API
//------------------------------------------------------------------------------
/**
* Configuration class
*/
class Config {
/**
* @param {Object} options Options to be passed in
* @param {Linter} linterContext Linter instance object
*/
constructor(options, linterContext) {
options = options || {};
this.linterContext = linterContext;
this.plugins = new Plugins(linterContext.environments, linterContext.rules);
this.options = options;
this.ignore = options.ignore;
this.ignorePath = options.ignorePath;
this.parser = options.parser;
this.parserOptions = options.parserOptions || {};
this.configCache = new ConfigCache();
this.baseConfig = options.baseConfig
? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))
: { rules: {} };
this.baseConfig.filePath = "";
this.baseConfig.baseDirectory = this.options.cwd;
this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);
this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);
this.useEslintrc = (options.useEslintrc !== false);
this.env = (options.envs || []).reduce((envs, name) => {
envs[name] = true;
return envs;
}, {});
/*
* Handle declared globals.
* For global variable foo, handle "foo:false" and "foo:true" to set
* whether global is writable.
* If user declares "foo", convert to "foo:false".
*/
this.globals = (options.globals || []).reduce((globals, def) => {
const parts = def.split(SUBCONFIG_SEP);
globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
return globals;
}, {});
this.loadSpecificConfig(options.configFile);
// Empty values in configs don't merge properly
const cliConfigOptions = {
env: this.env,
rules: this.options.rules,
globals: this.globals,
parserOptions: this.parserOptions,
plugins: this.options.plugins
};
this.cliConfig = {};
Object.keys(cliConfigOptions).forEach(configKey => {
const value = cliConfigOptions[configKey];
if (value) {
this.cliConfig[configKey] = value;
}
});
}
/**
* Loads the config options from a config specified on the command line.
* @param {string} [config] A shareable named config or path to a config file.
* @returns {void}
*/
loadSpecificConfig(config) {
if (config) {
debug(`Using command line config ${config}`);
const isNamedConfig =
isResolvable(config) ||
isResolvable(`eslint-config-${config}`) ||
config.charAt(0) === "@";
if (!isNamedConfig) {
config = path.resolve(this.options.cwd, config);
}
this.specificConfig = ConfigFile.load(config, this);
}
}
/**
* Gets the personal config object from user's home directory.
* @returns {Object} the personal config object (null if there is no personal config)
* @private
*/
getPersonalConfig() {
if (typeof this.personalConfig === "undefined") {
let config;
const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
if (filename) {
debug("Using personal config");
config = ConfigFile.load(filename, this);
}
this.personalConfig = config || null;
}
return this.personalConfig;
}
/**
* Builds a hierarchy of config objects, including the base config, all local configs from the directory tree,
* and a config file specified on the command line, if applicable.
* @param {string} directory a file in whose directory we start looking for a local config
* @returns {Object[]} The config objects, in ascending order of precedence
* @private
*/
getConfigHierarchy(directory) {
debug(`Constructing config file hierarchy for ${directory}`);
// Step 1: Always include baseConfig
let configs = [this.baseConfig];
// Step 2: Add user-specified config from .eslintrc.* and package.json files
if (this.useEslintrc) {
debug("Using .eslintrc and package.json files");
configs = configs.concat(this.getLocalConfigHierarchy(directory));
} else {
debug("Not using .eslintrc or package.json files");
}
// Step 3: Merge in command line config file
if (this.specificConfig) {
debug("Using command line config file");
configs.push(this.specificConfig);
}
return configs;
}
/**
* Gets a list of config objects extracted from local config files that apply to the current directory, in
* descending order, beginning with the config that is highest in the directory tree.
* @param {string} directory The directory to start looking in for local config files.
* @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current
* directory at the end), or an empty array if there are no local configs.
* @private
*/
getLocalConfigHierarchy(directory) {
const localConfigFiles = this.findLocalConfigFiles(directory),
projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd),
searched = [],
configs = [];
for (const localConfigFile of localConfigFiles) {
const localConfigDirectory = path.dirname(localConfigFile);
const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory);
if (localConfigHierarchyCache) {
const localConfigHierarchy = localConfigHierarchyCache.concat(configs.reverse());
this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy);
return localConfigHierarchy;
}
// Don't consider the personal config file in the home directory,
// except if the home directory is the same as the current working directory
if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
continue;
}
debug(`Loading ${localConfigFile}`);
const localConfig = ConfigFile.load(localConfigFile, this);
// Ignore empty config files
if (!localConfig) {
continue;
}
debug(`Using ${localConfigFile}`);
configs.push(localConfig);
searched.push(localConfigDirectory);
// Stop traversing if a config is found with the root flag set
if (localConfig.root) {
break;
}
}
if (!configs.length && !this.specificConfig) {
// Fall back on the personal config from ~/.eslintrc
debug("Using personal config file");
const personalConfig = this.getPersonalConfig();
if (personalConfig) {
configs.push(personalConfig);
} else if (!hasRules(this.options) && !this.options.baseConfig) {
// No config file, no manual configuration, and no rules, so error.
const noConfigError = new Error("No ESLint configuration found.");
noConfigError.messageTemplate = "no-config-found";
noConfigError.messageData = {
directory,
filesExamined: localConfigFiles
};
throw noConfigError;
}
}
// Set the caches for the parent directories
this.configCache.setHierarchyLocalConfigs(searched, configs.reverse());
return configs;
}
/**
* Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of
* entries, each of which in an object specifying a config file path and an array of override indices corresponding
* to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the
* vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main
* project .eslintrc file and its first and third override blocks apply to the current file.
* @param {string} filePath The file path for which to build the hierarchy and config vector.
* @returns {Array<Object>} config vector applicable to the specified path
* @private
*/
getConfigVector(filePath) {
const directory = filePath ? path.dirname(filePath) : this.options.cwd;
return this.getConfigHierarchy(directory).map(config => {
const vectorEntry = {
filePath: config.filePath,
matchingOverrides: []
};
if (config.overrides) {
const relativePath = path.relative(config.baseDirectory, filePath || directory);
config.overrides.forEach((override, i) => {
if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) {
vectorEntry.matchingOverrides.push(i);
}
});
}
return vectorEntry;
});
}
/**
* Finds local config files from the specified directory and its parent directories.
* @param {string} directory The directory to start searching from.
* @returns {GeneratorFunction} The paths of local config files found.
*/
findLocalConfigFiles(directory) {
if (!this.localConfigFinder) {
this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
}
return this.localConfigFinder.findAllInDirectoryAndParents(directory);
}
/**
* Builds the authoritative config object for the specified file path by merging the hierarchy of config objects
* that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config
* from their homedir, all local configs from the directory tree, any specific config file passed on the command
* line, any configuration overrides set directly on the command line, and finally the environment configs
* (conf/environments).
* @param {string} filePath a file in whose directory we start looking for a local config
* @returns {Object} config object
*/
getConfig(filePath) {
const vector = this.getConfigVector(filePath);
let config = this.configCache.getMergedConfig(vector);
if (config) {
debug("Using config from cache");
return config;
}
// Step 1: Merge in the filesystem configurations (base, local, and personal)
config = ConfigOps.getConfigFromVector(vector, this.configCache);
// Step 2: Merge in command line configurations
config = ConfigOps.merge(config, this.cliConfig);
if (this.cliConfig.plugins) {
this.plugins.loadAll(this.cliConfig.plugins);
}
// Step 3: Override parser only if it is passed explicitly through the command line
// or if it's not defined yet (because the final object will at least have the parser key)
if (this.parser || !config.parser) {
config = ConfigOps.merge(config, { parser: this.parser });
}
// Step 4: Apply environments to the config
config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
this.configCache.setMergedConfig(vector, config);
return config;
}
}
module.exports = Config;

View File

@@ -0,0 +1,357 @@
/**
* @fileoverview Used for creating a suggested configuration based on project code.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
Linter = require("../linter"),
configRule = require("./config-rule"),
ConfigOps = require("./config-ops"),
recConfig = require("../../conf/eslint-recommended");
const debug = require("debug")("eslint:autoconfig");
const linter = new Linter();
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only
RECOMMENDED_CONFIG_NAME = "eslint:recommended";
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* 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((accumulator, ruleId) => {
accumulator[ruleId] = rulesConfig[ruleId].map(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.
*
* Registry class
*/
class Registry {
/**
* @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations
*/
constructor(rulesConfig) {
this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {};
}
/**
* 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() {
const 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() {
let idx = 0;
const 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}
*/
const addRuleToRuleSet = function(rule) {
/*
* This check ensures that there is a rule configuration and that
* it has fewer than the max combinations allowed.
* If it has too many configs, we will only use the most basic of
* the possible configurations.
*/
const 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() {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (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() {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (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() {
const ruleIds = Object.keys(this.rules),
failingRegistry = new Registry();
ruleIds.forEach(ruleId => {
const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0));
if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
failingRegistry.rules[ruleId] = failingConfigs;
}
});
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() {
const ruleIds = Object.keys(this.rules),
config = { rules: {} };
ruleIds.forEach(ruleId => {
if (this.rules[ruleId].length === 1) {
config.rules[ruleId] = this.rules[ruleId][0].config;
}
});
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(specificity) {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity));
});
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(sourceCodes, config, cb) {
let lintedRegistry = new Registry();
lintedRegistry.rules = Object.assign({}, this.rules);
const ruleSets = lintedRegistry.buildRuleSets();
lintedRegistry = lintedRegistry.stripExtraConfigs();
debug("Linting with all possible rule combinations");
const filenames = Object.keys(sourceCodes);
const totalFilesLinting = filenames.length * ruleSets.length;
filenames.forEach(filename => {
debug(`Linting file: ${filename}`);
let ruleSetIdx = 0;
ruleSets.forEach(ruleSet => {
const lintConfig = Object.assign({}, config, { rules: ruleSet });
const lintResults = linter.verify(sourceCodes[filename], lintConfig);
lintResults.forEach(result => {
// It is possible that the error is from a configuration comment
// in a linted file, in which case there may not be a config
// set in this ruleSetIdx.
// (https://github.com/eslint/eslint/issues/5992)
// (https://github.com/eslint/eslint/issues/7860)
if (
lintedRegistry.rules[result.ruleId] &&
lintedRegistry.rules[result.ruleId][ruleSetIdx]
) {
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) {
const newConfig = Object.assign({}, config);
ConfigOps.normalizeToStrings(newConfig);
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
recRules.forEach(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,
extendFromRecommended
};

View File

@@ -0,0 +1,130 @@
/**
* @fileoverview Responsible for caching config files
* @author Sylvan Mably
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Get a string hash for a config vector
* @param {Array<Object>} vector config vector to hash
* @returns {string} hash of the vector values
* @private
*/
function hash(vector) {
return JSON.stringify(vector);
}
//------------------------------------------------------------------------------
// API
//------------------------------------------------------------------------------
/**
* Configuration caching class
*/
module.exports = class ConfigCache {
constructor() {
this.configFullNameCache = new Map();
this.localHierarchyCache = new Map();
this.mergedVectorCache = new Map();
this.mergedCache = new Map();
}
/**
* Gets a config object from the cache for the specified config file path.
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @returns {Object|null} config object, if found in the cache, otherwise null
* @private
*/
getConfig(configFullName) {
return this.configFullNameCache.get(configFullName);
}
/**
* Sets a config object in the cache for the specified config file path.
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @param {Object} config the config object to add to the cache
* @returns {void}
* @private
*/
setConfig(configFullName, config) {
this.configFullNameCache.set(configFullName, config);
}
/**
* Gets a list of hierarchy-local config objects that apply to the specified directory.
* @param {string} directory the path to the directory
* @returns {Object[]|null} a list of config objects, if found in the cache, otherwise null
* @private
*/
getHierarchyLocalConfigs(directory) {
return this.localHierarchyCache.get(directory);
}
/**
* For each of the supplied parent directories, sets the list of config objects for that directory to the
* appropriate subset of the supplied parent config objects.
* @param {string[]} parentDirectories a list of parent directories to add to the config cache
* @param {Object[]} parentConfigs a list of config objects that apply to the lowest directory in parentDirectories
* @returns {void}
* @private
*/
setHierarchyLocalConfigs(parentDirectories, parentConfigs) {
parentDirectories.forEach((localConfigDirectory, i) => {
const directoryParentConfigs = parentConfigs.slice(0, parentConfigs.length - i);
this.localHierarchyCache.set(localConfigDirectory, directoryParentConfigs);
});
}
/**
* Gets a merged config object corresponding to the supplied vector.
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
getMergedVectorConfig(vector) {
return this.mergedVectorCache.get(hash(vector));
}
/**
* Sets a merged config object in the cache for the supplied vector.
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
*/
setMergedVectorConfig(vector, config) {
this.mergedVectorCache.set(hash(vector), config);
}
/**
* Gets a merged config object corresponding to the supplied vector, including configuration options from outside
* the vector.
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
getMergedConfig(vector) {
return this.mergedCache.get(hash(vector));
}
/**
* Sets a merged config object in the cache for the supplied vector, including configuration options from outside
* the vector.
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
*/
setMergedConfig(vector, config) {
this.mergedCache.set(hash(vector), config);
}
};

View File

@@ -0,0 +1,638 @@
/**
* @fileoverview Helper to locate and load configuration files.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
ConfigOps = require("./config-ops"),
validator = require("./config-validator"),
pathUtil = require("../util/path-util"),
ModuleResolver = require("../util/module-resolver"),
pathIsInside = require("path-is-inside"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify"),
requireUncached = require("require-uncached");
const debug = require("debug")("eslint:config-file");
//------------------------------------------------------------------------------
// 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
//------------------------------------------------------------------------------
const CONFIG_FILES = [
".eslintrc.js",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
"package.json"
];
const resolver = new ModuleResolver();
/**
* Convenience wrapper for synchronously reading file contents.
* @param {string} filePath The filename to read.
* @returns {string} The file contents, with the BOM removed.
* @private
*/
function readFile(filePath) {
return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");
}
/**
* Determines if a given string represents a filepath or not using the same
* conventions as require(), meaning that the first character must be nonalphanumeric
* and not the @ sign which is used for scoped packages to be considered a file path.
* @param {string} filePath The string to check.
* @returns {boolean} True if it's a filepath, false if not.
* @private
*/
function isFilePath(filePath) {
return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0));
}
/**
* Loads a YAML configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadYAMLConfigFile(filePath) {
debug(`Loading YAML config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
// empty YAML file can be null, so always use
return yaml.safeLoad(readFile(filePath)) || {};
} catch (e) {
debug(`Error reading YAML file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a JSON configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadJSONConfigFile(filePath) {
debug(`Loading JSON config file: ${filePath}`);
try {
return JSON.parse(stripComments(readFile(filePath)));
} catch (e) {
debug(`Error reading JSON file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a legacy (.eslintrc) configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadLegacyConfigFile(filePath) {
debug(`Loading config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
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}`;
throw e;
}
}
/**
* Loads a JavaScript configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadJSConfigFile(filePath) {
debug(`Loading JS config file: ${filePath}`);
try {
return requireUncached(filePath);
} catch (e) {
debug(`Error reading JavaScript file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a configuration from a package.json file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadPackageJSONConfigFile(filePath) {
debug(`Loading package.json config file: ${filePath}`);
try {
return loadJSONConfigFile(filePath).eslintConfig || null;
} catch (e) {
debug(`Error reading package.json file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Creates an error to notify about a missing config to extend from.
* @param {string} configName The name of the missing config.
* @returns {Error} The error object to throw
* @private
*/
function configMissingError(configName) {
const error = new Error(`Failed to load config "${configName}" to extend from.`);
error.messageTemplate = "extend-config-missing";
error.messageData = {
configName
};
return error;
}
/**
* Loads a configuration file regardless of the source. Inspects the file path
* to determine the correctly way to load the config file.
* @param {Object} file The path to the configuration.
* @returns {Object} The configuration information.
* @private
*/
function loadConfigFile(file) {
const filePath = file.filePath;
let config;
switch (path.extname(filePath)) {
case ".js":
config = loadJSConfigFile(filePath);
if (file.configName) {
config = config.configs[file.configName];
if (!config) {
throw configMissingError(file.configFullName);
}
}
break;
case ".json":
if (path.basename(filePath) === "package.json") {
config = loadPackageJSONConfigFile(filePath);
if (config === null) {
return null;
}
} else {
config = loadJSONConfigFile(filePath);
}
break;
case ".yaml":
case ".yml":
config = loadYAMLConfigFile(filePath);
break;
default:
config = loadLegacyConfigFile(filePath);
}
return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);
}
/**
* Writes a configuration file in JSON format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeJSONConfigFile(config, filePath) {
debug(`Writing JSON config file: ${filePath}`);
const content = stringify(config, { cmp: sortByKey, space: 4 });
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file in YAML format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeYAMLConfigFile(config, filePath) {
debug(`Writing YAML config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
const content = yaml.safeDump(config, { sortKeys: true });
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file in JavaScript format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeJSConfigFile(config, filePath) {
debug(`Writing JS config file: ${filePath}`);
const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @throws {Error} When an unknown file type is specified.
* @private
*/
function write(config, filePath) {
switch (path.extname(filePath)) {
case ".js":
writeJSConfigFile(config, filePath);
break;
case ".json":
writeJSONConfigFile(config, filePath);
break;
case ".yaml":
case ".yml":
writeYAMLConfigFile(config, filePath);
break;
default:
throw new Error("Can't write to unknown file type.");
}
}
/**
* Determines the base directory for node packages referenced in a config file.
* This does not include node_modules in the path so it can be used for all
* references relative to a config file.
* @param {string} configFilePath The config file referencing the file.
* @returns {string} The base directory for the file path.
* @private
*/
function getBaseDir(configFilePath) {
// calculates the path of the project including ESLint as dependency
const projectPath = path.resolve(__dirname, "../../../");
if (configFilePath && pathIsInside(configFilePath, projectPath)) {
// be careful of https://github.com/substack/node-resolve/issues/78
return path.join(path.resolve(configFilePath));
}
/*
* default to ESLint project path since it's unlikely that plugins will be
* in this directory
*/
return path.join(projectPath);
}
/**
* Determines the lookup path, including node_modules, for package
* references relative to a config file.
* @param {string} configFilePath The config file referencing the file.
* @returns {string} The lookup path for the file path.
* @private
*/
function getLookupPath(configFilePath) {
const basedir = getBaseDir(configFilePath);
return path.join(basedir, "node_modules");
}
/**
* Resolves a eslint core config path
* @param {string} name The eslint config name.
* @returns {string} The resolved path of the config.
* @private
*/
function getEslintCoreConfigPath(name) {
if (name === "eslint:recommended") {
/*
* Add an explicit substitution for eslint:recommended to
* conf/eslint-recommended.js.
*/
return path.resolve(__dirname, "../../conf/eslint-recommended.js");
}
if (name === "eslint:all") {
/*
* Add an explicit substitution for eslint:all to conf/eslint-all.js
*/
return path.resolve(__dirname, "../../conf/eslint-all.js");
}
throw configMissingError(name);
}
/**
* Applies values from the "extends" field in a configuration file.
* @param {Object} config The configuration information.
* @param {Config} configContext Plugin context for the config instance
* @param {string} filePath The file path from which the configuration information
* was loaded.
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} A new configuration object with all of the "extends" fields
* loaded and merged.
* @private
*/
function applyExtends(config, configContext, filePath, relativeTo) {
let configExtends = config.extends;
// normalize into an array for easier handling
if (!Array.isArray(config.extends)) {
configExtends = [config.extends];
}
// Make the last element in an array take the highest precedence
config = configExtends.reduceRight((previousValue, parentPath) => {
try {
if (parentPath.startsWith("eslint:")) {
parentPath = getEslintCoreConfigPath(parentPath);
} else if (isFilePath(parentPath)) {
/*
* If the `extends` path is relative, use the directory of the current configuration
* file as the reference point. Otherwise, use as-is.
*/
parentPath = (path.isAbsolute(parentPath)
? parentPath
: path.join(relativeTo || path.dirname(filePath), parentPath)
);
}
debug(`Loading ${parentPath}`);
// eslint-disable-next-line no-use-before-define
return ConfigOps.merge(load(parentPath, configContext, relativeTo), previousValue);
} catch (e) {
/*
* If the file referenced by `extends` failed to load, add the path
* to the configuration file that referenced it to the error
* message so the user is able to see where it was referenced from,
* then re-throw.
*/
e.message += `\nReferenced from: ${filePath}`;
throw e;
}
}, config);
return config;
}
/**
* Brings package name to correct format based on prefix
* @param {string} name The name of the package.
* @param {string} prefix Can be either "eslint-plugin" or "eslint-config
* @returns {string} Normalized name of the package
* @private
*/
function normalizePackageName(name, prefix) {
/*
* On Windows, name can come in with Windows slashes instead of Unix slashes.
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (name.indexOf("\\") > -1) {
name = pathUtil.convertPathToPosix(name);
}
if (name.charAt(0) === "@") {
/*
* it's a scoped package
* package name is "eslint-config", or just a username
*/
const 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.
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} An object containing 3 properties:
* - 'filePath' (required) the resolved path that can be used directly to load the configuration.
* - 'configName' the name of the configuration inside the plugin.
* - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @private
*/
function resolve(filePath, relativeTo) {
if (isFilePath(filePath)) {
const fullPath = path.resolve(relativeTo || "", filePath);
return { filePath: fullPath, configFullName: fullPath };
}
let normalizedPackageName;
if (filePath.startsWith("plugin:")) {
const configFullName = filePath;
const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
const configName = filePath.slice(filePath.lastIndexOf("/") + 1);
normalizedPackageName = normalizePackageName(pluginName, "eslint-plugin");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configName, configFullName };
}
normalizedPackageName = normalizePackageName(filePath, "eslint-config");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configFullName: filePath };
}
/**
* Loads a configuration file from the given file path.
* @param {Object} resolvedPath The value from calling resolve() on a filename or package name.
* @param {Config} configContext Plugins context
* @returns {Object} The configuration information.
*/
function loadFromDisk(resolvedPath, configContext) {
const dirname = path.dirname(resolvedPath.filePath),
lookupPath = getLookupPath(dirname);
let config = loadConfigFile(resolvedPath);
if (config) {
// ensure plugins are properly loaded first
if (config.plugins) {
configContext.plugins.loadAll(config.plugins);
}
// include full path of parser if present
if (config.parser) {
if (isFilePath(config.parser)) {
config.parser = path.resolve(dirname || "", config.parser);
} else {
config.parser = resolver.resolve(config.parser, lookupPath);
}
}
// validate the configuration before continuing
validator.validate(config, resolvedPath.configFullName, configContext.linterContext.rules, configContext.linterContext.environments);
/*
* If an `extends` property is defined, it represents a configuration file to use as
* a "parent". Load the referenced file and merge the configuration recursively.
*/
if (config.extends) {
config = applyExtends(config, configContext, resolvedPath.filePath, dirname);
}
}
return config;
}
/**
* Loads a config object, applying extends if present.
* @param {Object} configObject a config object to load
* @param {Config} configContext Context for the config instance
* @returns {Object} the config object with extends applied if present, or the passed config if not
* @private
*/
function loadObject(configObject, configContext) {
return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;
}
/**
* Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet
* cached.
* @param {string} filePath the path to the config file
* @param {Config} configContext Context for the config instance
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} the parsed config object (empty object if there was a parse error)
* @private
*/
function load(filePath, configContext, relativeTo) {
const resolvedPath = resolve(filePath, relativeTo);
const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);
if (cachedConfig) {
return cachedConfig;
}
const config = loadFromDisk(resolvedPath, configContext);
if (config) {
config.filePath = resolvedPath.filePath;
config.baseDirectory = path.dirname(resolvedPath.filePath);
configContext.configCache.setConfig(resolvedPath.configFullName, config);
}
return config;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
getBaseDir,
getLookupPath,
load,
loadObject,
resolve,
write,
applyExtends,
normalizePackageName,
CONFIG_FILES,
/**
* Retrieves the configuration filename for a given directory. It loops over all
* of the valid configuration filenames in order to find the first one that exists.
* @param {string} directory The directory to check for a config file.
* @returns {?string} The filename of the configuration file for the directory
* or null if there is no configuration file in the directory.
*/
getFilenameForDirectory(directory) {
for (let i = 0, len = CONFIG_FILES.length; i < len; i++) {
const filename = path.join(directory, CONFIG_FILES[i]);
if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {
return filename;
}
}
return null;
}
};

View File

@@ -0,0 +1,601 @@
/**
* @fileoverview Config initialization wizard.
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const util = require("util"),
inquirer = require("inquirer"),
ProgressBar = require("progress"),
semver = require("semver"),
autoconfig = require("./autoconfig.js"),
ConfigFile = require("./config-file"),
ConfigOps = require("./config-ops"),
getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
ModuleResolver = require("../util/module-resolver"),
npmUtil = require("../util/npm-util"),
recConfig = require("../../conf/eslint-recommended"),
log = require("../logging");
const debug = require("debug")("eslint:config-initializer");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/* 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 {string} format The file format to write to.
* @returns {void}
*/
function writeFile(config, format) {
// default is .js
let extname = ".js";
if (format === "YAML") {
extname = ".yml";
} else if (format === "JSON") {
extname = ".json";
}
const installedESLint = config.installedESLint;
delete config.installedESLint;
ConfigFile.write(config, `./.eslintrc${extname}`);
log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
if (installedESLint) {
log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
}
}
/**
* Get the peer dependencies of the given module.
* This adds the gotten value to cache at the first time, then reuses it.
* In a process, this function is called twice, but `npmUtil.fetchPeerDependencies` needs to access network which is relatively slow.
* @param {string} moduleName The module name to get.
* @returns {Object} The peer dependencies of the given module.
* This object is the object of `peerDependencies` field of `package.json`.
* Returns null if npm was not found.
*/
function getPeerDependencies(moduleName) {
let result = getPeerDependencies.cache.get(moduleName);
if (!result) {
log.info(`Checking peerDependencies of ${moduleName}`);
result = npmUtil.fetchPeerDependencies(moduleName);
getPeerDependencies.cache.set(moduleName, result);
}
return result;
}
getPeerDependencies.cache = new Map();
/**
* Synchronously install necessary plugins, configs, parsers, etc. based on the config
* @param {Object} config config object
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
* @returns {void}
*/
function installModules(config, installESLint) {
const modules = {};
// Create a list of modules which should be installed based on config
if (config.plugins) {
for (const plugin of config.plugins) {
modules[`eslint-plugin-${plugin}`] = "latest";
}
}
if (config.extends && config.extends.indexOf("eslint:") === -1) {
const moduleName = `eslint-config-${config.extends}`;
modules[moduleName] = "latest";
Object.assign(
modules,
getPeerDependencies(`${moduleName}@latest`)
);
}
// If no modules, do nothing.
if (Object.keys(modules).length === 0) {
return;
}
if (installESLint === false) {
delete modules.eslint;
} else {
const installStatus = npmUtil.checkDevDeps(["eslint"]);
// Mark to show messages if it's new installation of eslint.
if (installStatus.eslint === false) {
log.info("Local ESLint installation not found.");
modules.eslint = modules.eslint || "latest";
config.installedESLint = true;
}
}
// Install packages
const modulesToInstall = Object.keys(modules).map(name => `${name}@${modules[name]}`);
log.info(`Installing ${modulesToInstall.join(", ")}`);
npmUtil.installSyncSaveDev(modulesToInstall);
}
/**
* 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) {
const BAR_TOTAL = 20,
BAR_SOURCE_CODE_TOTAL = 4,
newConfig = Object.assign({}, config),
disabledConfigs = {};
let sourceCodes,
registry;
// Set up a progress bar, as this process can take a long time
const 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
const patterns = answers.patterns.split(/[\s]+/);
try {
sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => {
bar.tick((BAR_SOURCE_CODE_TOTAL / total));
});
} catch (e) {
log.info("\n");
throw e;
}
const fileQty = Object.keys(sourceCodes).length;
if (fileQty === 0) {
log.info("\n");
throw new Error("Automatic Configuration failed. No files were able to be parsed.");
}
// 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, 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
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
// Find and disable rules which had no error-free configuration
const failingRegistry = registry.getFailingRulesRegistry();
Object.keys(failingRegistry.rules).forEach(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.
const 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.
const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
// Maybe a specific combination using all three options works
const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
// If all else fails, try to use the default (severity only)
const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
// Combine configs in reverse priority order (later take precedence)
newConfig.rules = Object.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
const finalRuleIds = Object.keys(newConfig.rules);
const totalRules = finalRuleIds.length;
const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;
const resultMessage = [
`\nEnabled ${enabledRules} out of ${totalRules}`,
`rules based on ${fileQty}`,
`file${(fileQty === 1) ? "." : "s."}`
].join(" ");
log.info(resultMessage);
ConfigOps.normalizeToStrings(newConfig);
return newConfig;
}
/**
* process user's answers and create config object
* @param {Object} answers answers received from inquirer
* @returns {Object} config object
*/
function processAnswers(answers) {
let 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(env => {
config.env[env] = true;
});
if (answers.jsx) {
config.parserOptions = config.parserOptions || {};
config.parserOptions.ecmaFeatures = config.parserOptions.ecmaFeatures || {};
config.parserOptions.ecmaFeatures.jsx = true;
if (answers.react) {
config.plugins = ["react"];
config.parserOptions.ecmaFeatures.experimentalObjectRestSpread = true;
}
}
if (answers.source === "prompt") {
config.extends = "eslint:recommended";
config.rules.indent = ["error", answers.indent];
config.rules.quotes = ["error", answers.quotes];
config.rules["linebreak-style"] = ["error", answers.linebreak];
config.rules.semi = ["error", answers.semi ? "always" : "never"];
}
installModules(config);
if (answers.source === "auto") {
config = configureRules(answers, config);
config = autoconfig.extendFromRecommended(config);
}
ConfigOps.normalizeToStrings(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
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
* @returns {Object} config object
*/
function getConfigForStyleGuide(guide, installESLint) {
const guides = {
google: { extends: "google" },
airbnb: { extends: "airbnb" },
"airbnb-base": { extends: "airbnb-base" },
standard: { extends: "standard" }
};
if (!guides[guide]) {
throw new Error("You referenced an unsupported guide.");
}
installModules(guides[guide], installESLint);
return guides[guide];
}
/**
* Get the version of the local ESLint.
* @returns {string|null} The version. If the local ESLint was not found, returns null.
*/
function getLocalESLintVersion() {
try {
const resolver = new ModuleResolver();
const eslintPath = resolver.resolve("eslint", process.cwd());
const eslint = require(eslintPath);
return eslint.linter.version || null;
} catch (_err) {
return null;
}
}
/**
* Get the shareable config name of the chosen style guide.
* @param {Object} answers The answers object.
* @returns {string} The shareable config name.
*/
function getStyleGuideName(answers) {
if (answers.styleguide === "airbnb" && !answers.airbnbReact) {
return "airbnb-base";
}
return answers.styleguide;
}
/**
* Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
* @param {Object} answers The answers object.
* @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
*/
function hasESLintVersionConflict(answers) {
// Get the local ESLint version.
const localESLintVersion = getLocalESLintVersion();
if (!localESLintVersion) {
return false;
}
// Get the required range of ESLint version.
const configName = getStyleGuideName(answers);
const moduleName = `eslint-config-${configName}@latest`;
const peerDependencies = getPeerDependencies(moduleName) || {};
const requiredESLintVersionRange = peerDependencies.eslint;
if (!requiredESLintVersionRange) {
return false;
}
answers.localESLintVersion = localESLintVersion;
answers.requiredESLintVersionRange = requiredESLintVersionRange;
// Check the version.
if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
answers.installESLint = false;
return false;
}
return true;
}
/* istanbul ignore next: no need to test inquirer*/
/**
* Ask use a few questions on command prompt
* @returns {Promise} The promise with the result of the prompt
*/
function promptUser() {
return 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" },
{ name: "Inspect your JavaScript file(s)", value: "auto" }
]
},
{
type: "list",
name: "styleguide",
message: "Which style guide do you want to follow?",
choices: [{ name: "Google", value: "google" }, { name: "Airbnb", value: "airbnb" }, { name: "Standard", value: "standard" }],
when(answers) {
answers.packageJsonExists = npmUtil.checkPackageJson();
return answers.source === "guide" && answers.packageJsonExists;
}
},
{
type: "confirm",
name: "airbnbReact",
message: "Do you use React?",
default: false,
when(answers) {
return answers.styleguide === "airbnb";
}
},
{
type: "input",
name: "patterns",
message: "Which file(s), path(s), or glob(s) should be examined?",
when(answers) {
return (answers.source === "auto");
},
validate(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",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"],
when(answers) {
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
}
},
{
type: "confirm",
name: "installESLint",
message(answers) {
const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
? "upgrade"
: "downgrade";
return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
},
default: true,
when(answers) {
return answers.source === "guide" && answers.packageJsonExists && hasESLintVersionConflict(answers);
}
}
]).then(earlyAnswers => {
// early exit if you are using a style guide
if (earlyAnswers.source === "guide") {
if (!earlyAnswers.packageJsonExists) {
log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
return void 0;
}
if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
}
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
earlyAnswers.styleguide = "airbnb-base";
}
const config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the questions otherwise...
return inquirer.prompt([
{
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(answers) {
return answers.es6 === true;
}
},
{
type: "checkbox",
name: "env",
message: "Where will your code run?",
default: ["browser"],
choices: [{ name: "Browser", value: "browser" }, { name: "Node", value: "node" }]
},
{
type: "confirm",
name: "commonjs",
message: "Do you use CommonJS?",
default: false,
when(answers) {
return answers.env.some(env => env === "browser");
}
},
{
type: "confirm",
name: "jsx",
message: "Do you use JSX?",
default: false
},
{
type: "confirm",
name: "react",
message: "Do you use React?",
default: false,
when(answers) {
return answers.jsx;
}
}
]).then(secondAnswers => {
// early exit if you are using automatic style generation
if (earlyAnswers.source === "auto") {
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
const config = processAnswers(combinedAnswers);
installModules(config);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the style questions otherwise...
return inquirer.prompt([
{
type: "list",
name: "indent",
message: "What style of indentation do you use?",
default: "tab",
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"]
}
]).then(answers => {
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
const config = processAnswers(totalAnswers);
installModules(config);
writeFile(config, answers.format);
});
});
});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
const init = {
getConfigForStyleGuide,
hasESLintVersionConflict,
processAnswers,
/* istanbul ignore next */initializeConfig() {
return promptUser();
}
};
module.exports = init;

View File

@@ -0,0 +1,383 @@
/**
* @fileoverview Config file operations. This file must be usable in the browser,
* so no Node-specific code can be here.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const minimatch = require("minimatch"),
path = require("path");
const debug = require("debug")("eslint:config-ops");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
map[value] = index;
return map;
}, {}),
VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/**
* Creates an empty configuration object suitable for merging as a base.
* @returns {Object} A configuration object.
*/
createEmptyConfig() {
return {
globals: {},
env: {},
rules: {},
parserOptions: {}
};
},
/**
* Creates an environment config based on the specified environments.
* @param {Object<string,boolean>} env The environment settings.
* @param {Environments} envContext The environment context.
* @returns {Object} A configuration object with the appropriate rules and globals
* set.
*/
createEnvironmentConfig(env, envContext) {
const envConfig = this.createEmptyConfig();
if (env) {
envConfig.env = env;
Object.keys(env).filter(name => env[name]).forEach(name => {
const environment = envContext.get(name);
if (environment) {
debug(`Creating config for environment ${name}`);
if (environment.globals) {
Object.assign(envConfig.globals, environment.globals);
}
if (environment.parserOptions) {
Object.assign(envConfig.parserOptions, environment.parserOptions);
}
}
});
}
return envConfig;
},
/**
* Given a config with environment settings, applies the globals and
* ecmaFeatures to the configuration and returns the result.
* @param {Object} config The configuration information.
* @param {Environments} envContent env context.
* @returns {Object} The updated configuration information.
*/
applyEnvironments(config, envContent) {
if (config.env && typeof config.env === "object") {
debug("Apply environment settings to config");
return this.merge(this.createEnvironmentConfig(config.env, envContent), config);
}
return config;
},
/**
* Merges two config objects. This will not only add missing keys, but will also modify values to match.
* @param {Object} target config object
* @param {Object} src config object. Overrides in this config object will take priority over base.
* @param {boolean} [combine] Whether to combine arrays or not
* @param {boolean} [isRule] Whether its a rule
* @returns {Object} merged config object.
*/
merge: function deepmerge(target, src, combine, isRule) {
/*
The MIT License (MIT)
Copyright (c) 2012 Nicholas Fisher
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.
*/
/*
* This code is taken from deepmerge repo
* (https://github.com/KyleAMathews/deepmerge)
* and modified to meet our needs.
*/
const array = Array.isArray(src) || Array.isArray(target);
let dst = array && [] || {};
combine = !!combine;
isRule = !!isRule;
if (array) {
target = target || [];
// src could be a string, so check for array
if (isRule && Array.isArray(src) && src.length > 1) {
dst = dst.concat(src);
} else {
dst = dst.concat(target);
}
if (typeof src !== "object" && !Array.isArray(src)) {
src = [src];
}
Object.keys(src).forEach((e, i) => {
e = src[i];
if (typeof dst[i] === "undefined") {
dst[i] = e;
} else if (typeof e === "object") {
if (isRule) {
dst[i] = e;
} else {
dst[i] = deepmerge(target[i], e, combine, isRule);
}
} else {
if (!combine) {
dst[i] = e;
} else {
if (dst.indexOf(e) === -1) {
dst.push(e);
}
}
}
});
} else {
if (target && typeof target === "object") {
Object.keys(target).forEach(key => {
dst[key] = target[key];
});
}
Object.keys(src).forEach(key => {
if (key === "overrides") {
dst[key] = (target[key] || []).concat(src[key] || []);
} else if (Array.isArray(src[key]) || Array.isArray(target[key])) {
dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule);
} else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
dst[key] = src[key];
} else {
dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules");
}
});
}
return dst;
},
/**
* Normalizes the severity value of a rule's configuration to a number
* @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally
* received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0),
* the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array
* whose first element is one of the above values. Strings are matched case-insensitively.
* @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0.
*/
getRuleSeverity(ruleConfig) {
const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
return severityValue;
}
if (typeof severityValue === "string") {
return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
}
return 0;
},
/**
* Converts old-style severity settings (0, 1, 2) into new-style
* severity settings (off, warn, error) for all rules. Assumption is that severity
* values have already been validated as correct.
* @param {Object} config The config object to normalize.
* @returns {void}
*/
normalizeToStrings(config) {
if (config.rules) {
Object.keys(config.rules).forEach(ruleId => {
const ruleConfig = config.rules[ruleId];
if (typeof ruleConfig === "number") {
config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0];
} else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") {
ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0];
}
});
}
},
/**
* Determines if the severity for the given rule configuration represents an error.
* @param {int|string|Array} ruleConfig The configuration for an individual rule.
* @returns {boolean} True if the rule represents an error, false if not.
*/
isErrorSeverity(ruleConfig) {
let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
if (typeof severity === "string") {
severity = RULE_SEVERITY[severity.toLowerCase()] || 0;
}
return (typeof severity === "number" && severity === 2);
},
/**
* Checks whether a given config has valid severity or not.
* @param {number|string|Array} ruleConfig - The configuration for an individual rule.
* @returns {boolean} `true` if the configuration has valid severity.
*/
isValidSeverity(ruleConfig) {
let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
if (typeof severity === "string") {
severity = severity.toLowerCase();
}
return VALID_SEVERITIES.indexOf(severity) !== -1;
},
/**
* Checks whether every rule of a given config has valid severity or not.
* @param {Object} config - The configuration for rules.
* @returns {boolean} `true` if the configuration has valid severity.
*/
isEverySeverityValid(config) {
return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId]));
},
/**
* Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
* file path and a list of subconfig indices that match the current file path. All config data is assumed to be
* cached.
* @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
* @param {Object} configCache the config cache
* @returns {Object} config object
*/
getConfigFromVector(vector, configCache) {
const cachedConfig = configCache.getMergedVectorConfig(vector);
if (cachedConfig) {
return cachedConfig;
}
debug("Using config from partial cache");
const subvector = Array.from(vector);
let nearestCacheIndex = subvector.length - 1,
partialCachedConfig;
while (nearestCacheIndex >= 0) {
partialCachedConfig = configCache.getMergedVectorConfig(subvector);
if (partialCachedConfig) {
break;
}
subvector.pop();
nearestCacheIndex--;
}
if (!partialCachedConfig) {
partialCachedConfig = {};
}
let finalConfig = partialCachedConfig;
// Start from entry immediately following nearest cached config (first uncached entry)
for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache);
configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
}
return finalConfig;
},
/**
* Merges the config options from a single vector entry into the supplied config.
* @param {Object} config the base config to merge the vector entry's options into
* @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
* matching override indices
* @param {Object} configCache the config cache
* @returns {Object} merged config object
*/
mergeVectorEntry(config, vectorEntry, configCache) {
const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
let mergedConfig = Object.assign({}, config),
overrides;
if (vectorEntryConfig.overrides) {
overrides = vectorEntryConfig.overrides.filter(
(override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1
);
} else {
overrides = [];
}
mergedConfig = this.merge(mergedConfig, vectorEntryConfig);
delete mergedConfig.overrides;
mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);
if (mergedConfig.filePath) {
delete mergedConfig.filePath;
delete mergedConfig.baseDirectory;
} else if (mergedConfig.files) {
delete mergedConfig.files;
}
return mergedConfig;
},
/**
* Checks that the specified file path matches all of the supplied glob patterns.
* @param {string} filePath The file path to test patterns against
* @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path
* @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path
* @returns {boolean} True if all the supplied patterns match the file path, false otherwise
*/
pathMatchesGlobs(filePath, patterns, excludedPatterns) {
const patternList = [].concat(patterns);
const excludedPatternList = [].concat(excludedPatterns || []);
patternList.concat(excludedPatternList).forEach(pattern => {
if (path.isAbsolute(pattern) || pattern.includes("..")) {
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
}
});
const opts = { matchBase: true };
return patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
!excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts));
}
};

View File

@@ -0,0 +1,322 @@
/**
* @fileoverview Create configurations for a rule
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Rules = require("../rules"),
loadRules = require("../load-rules");
const rules = new 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((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) {
const res = [];
if (arr1.length === 0) {
return explodeArray(arr2);
}
if (arr2.length === 0) {
return explodeArray(arr1);
}
arr1.forEach(x1 => {
arr2.forEach(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) {
const groupedObj = objects.reduce((accumulator, obj) => {
const prop = Object.keys(obj)[0];
accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
return accumulator;
}, {});
return Object.keys(groupedObj).map(prop => 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) {
const res = [];
if (objArr1.length === 0) {
return objArr2;
}
if (objArr2.length === 0) {
return objArr1;
}
objArr1.forEach(obj1 => {
objArr2.forEach(obj2 => {
const combinedObj = {};
const obj1Props = Object.keys(obj1);
const obj2Props = Object.keys(obj2);
obj1Props.forEach(prop1 => {
combinedObj[prop1] = obj1[prop1];
});
obj2Props.forEach(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"]]
*
* Rule configuration set class
*/
class RuleConfigSet {
/**
* @param {ruleConfig[]} configs Valid rule configurations
*/
constructor(configs) {
/**
* Stored valid rule configurations for this instance
* @type {array}
*/
this.ruleConfigs = configs || [];
}
/**
* 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(severity) {
severity = severity || 2;
this.ruleConfigs = this.ruleConfigs.map(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(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 {boolean} true if at least one schema for the object could be generated, false otherwise
*/
addObject(obj) {
const objectConfigSet = {
objectConfigs: [],
add(property, values) {
for (let idx = 0; idx < values.length; idx++) {
const optionObj = {};
optionObj[property] = values[idx];
this.objectConfigs.push(optionObj);
}
},
combine() {
this.objectConfigs = groupByProperty(this.objectConfigs).reduce((accumulator, objArr) => 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(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));
return true;
}
return false;
}
}
/**
* 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) {
const configSet = new RuleConfigSet();
if (Array.isArray(schema)) {
for (const opt of schema) {
if (opt.enum) {
configSet.addEnums(opt.enum);
} else if (opt.type && opt.type === "object") {
if (!configSet.addObject(opt)) {
break;
}
// TODO (IanVS): support oneOf
} else {
// If we don't know how to fill in this option, don't fill in any of the following options.
break;
}
}
}
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() {
const ruleList = loadRules();
return Object.keys(ruleList).reduce((accumulator, id) => {
const rule = rules.get(id);
const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
accumulator[id] = generateConfigsFromSchema(schema);
return accumulator;
}, {});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
generateConfigsFromSchema,
createCoreRuleConfigs
};

View File

@@ -0,0 +1,242 @@
/**
* @fileoverview Validates configs.
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const ajv = require("../util/ajv"),
lodash = require("lodash"),
configSchema = require("../../conf/config-schema.js"),
util = require("util");
const validators = {
rules: Object.create(null)
};
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
let validateSchema;
/**
* Gets a complete options schema for a rule.
* @param {string} id The rule's unique name.
* @param {Rules} rulesContext Rule context
* @returns {Object} JSON Schema for the rule's options.
*/
function getRuleOptionsSchema(id, rulesContext) {
const rule = rulesContext.get(id),
schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
// Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(schema)) {
if (schema.length) {
return {
type: "array",
items: schema,
minItems: 0,
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
}
// Given a full schema, leave it alone
return schema || null;
}
/**
* Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
* @param {options} options The given options for the rule.
* @returns {number|string} The rule's severity value
*/
function validateRuleSeverity(options) {
const severity = Array.isArray(options) ? options[0] : options;
if (severity !== 0 && severity !== 1 && severity !== 2 && !(typeof severity === "string" && /^(?:off|warn|error)$/i.test(severity))) {
throw new Error(`\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util.inspect(severity).replace(/'/g, "\"").replace(/\n/g, "")}').\n`);
}
return severity;
}
/**
* Validates the non-severity options passed to a rule, based on its schema.
* @param {string} id The rule's unique name
* @param {array} localOptions The options for the rule, excluding severity
* @param {Rules} rulesContext Rule context
* @returns {void}
*/
function validateRuleSchema(id, localOptions, rulesContext) {
const schema = getRuleOptionsSchema(id, rulesContext);
if (!validators.rules[id] && schema) {
validators.rules[id] = ajv.compile(schema);
}
const validateRule = validators.rules[id];
if (validateRule) {
validateRule(localOptions);
if (validateRule.errors) {
throw new Error(validateRule.errors.map(error => `\tValue "${error.data}" ${error.message}.\n`).join(""));
}
}
}
/**
* Validates a rule's options against its schema.
* @param {string} id The rule's unique name.
* @param {array|number} options The given options for the rule.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext Rule context
* @returns {void}
*/
function validateRuleOptions(id, options, source, rulesContext) {
try {
const severity = validateRuleSeverity(options);
if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) {
validateRuleSchema(id, Array.isArray(options) ? options.slice(1) : [], rulesContext);
}
} catch (err) {
throw new Error(`${source}:\n\tConfiguration for rule "${id}" is invalid:\n${err.message}`);
}
}
/**
* Validates an environment object
* @param {Object} environment The environment config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Environments} envContext Env context
* @returns {void}
*/
function validateEnvironment(environment, source, envContext) {
// not having an environment is ok
if (!environment) {
return;
}
Object.keys(environment).forEach(env => {
if (!envContext.get(env)) {
const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`;
throw new Error(message);
}
});
}
/**
* Validates a rules config object
* @param {Object} rulesConfig The rules config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext Rule context
* @returns {void}
*/
function validateRules(rulesConfig, source, rulesContext) {
if (!rulesConfig) {
return;
}
Object.keys(rulesConfig).forEach(id => {
validateRuleOptions(id, rulesConfig[id], source, rulesContext);
});
}
/**
* Formats an array of schema validation errors.
* @param {Array} errors An array of error messages to format.
* @returns {string} Formatted error message
*/
function formatErrors(errors) {
return errors.map(error => {
if (error.keyword === "additionalProperties") {
const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
return `Unexpected top-level property "${formattedPropertyPath}"`;
}
if (error.keyword === "type") {
const formattedField = error.dataPath.slice(1);
const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
const formattedValue = JSON.stringify(error.data);
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
}
const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
}).map(message => `\t- ${message}.\n`).join("");
}
/**
* Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
* for each unique file path, but repeated invocations with the same file path have no effect.
* No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
* @param {string} source The name of the configuration source to report the warning for.
* @returns {void}
*/
const emitEcmaFeaturesWarning = lodash.memoize(source => {
/*
* util.deprecate seems to be the only way to emit a warning in Node 4.x while respecting the --no-warnings flag.
* (In Node 6+, process.emitWarning could be used instead.)
*/
util.deprecate(
() => {},
`[eslint] The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in ${source})`
)();
});
/**
* Validates the top level properties of the config object.
* @param {Object} config The config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @returns {void}
*/
function validateConfigSchema(config, source) {
validateSchema = validateSchema || ajv.compile(configSchema);
if (!validateSchema(config)) {
throw new Error(`ESLint configuration in ${source} is invalid:\n${formatErrors(validateSchema.errors)}`);
}
if (Object.prototype.hasOwnProperty.call(config, "ecmaFeatures")) {
emitEcmaFeaturesWarning(source);
}
}
/**
* Validates an entire config object.
* @param {Object} config The config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Rules} rulesContext The rules context
* @param {Environments} envContext The env context
* @returns {void}
*/
function validate(config, source, rulesContext, envContext) {
validateConfigSchema(config, source);
validateRules(config.rules, source, rulesContext);
validateEnvironment(config.env, source, envContext);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
getRuleOptionsSchema,
validate,
validateRuleOptions
};

View File

@@ -0,0 +1,84 @@
/**
* @fileoverview Environments manager
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const envs = require("../../conf/environments");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class Environments {
/**
* create env context
*/
constructor() {
this._environments = new Map();
this.load();
}
/**
* Loads the default environments.
* @returns {void}
* @private
*/
load() {
Object.keys(envs).forEach(envName => {
this._environments.set(envName, envs[envName]);
});
}
/**
* Gets the environment with the given name.
* @param {string} name The name of the environment to retrieve.
* @returns {Object?} The environment object or null if not found.
*/
get(name) {
return this._environments.get(name) || null;
}
/**
* Gets all the environment present
* @returns {Object} The environment object for each env name
*/
getAll() {
return Array.from(this._environments).reduce((coll, env) => {
coll[env[0]] = env[1];
return coll;
}, {});
}
/**
* Defines an environment.
* @param {string} name The name of the environment.
* @param {Object} env The environment settings.
* @returns {void}
*/
define(name, env) {
this._environments.set(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(plugin, pluginName) {
if (plugin.environments) {
Object.keys(plugin.environments).forEach(envName => {
this.define(`${pluginName}/${envName}`, plugin.environments[envName]);
});
}
}
}
module.exports = Environments;

View File

@@ -0,0 +1,177 @@
/**
* @fileoverview Plugins manager
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint:plugins");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const PLUGIN_NAME_PREFIX = "eslint-plugin-",
NAMESPACE_REGEX = /^@.*\//i;
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Plugin class
*/
class Plugins {
/**
* Creates the plugins context
* @param {Environments} envContext - env context
* @param {Rules} rulesContext - rules context
*/
constructor(envContext, rulesContext) {
this._plugins = Object.create(null);
this._environments = envContext;
this._rules = rulesContext;
}
/**
* Removes the prefix `eslint-plugin-` from a plugin name.
* @param {string} pluginName The name of the plugin which may have the prefix.
* @returns {string} The name of the plugin without prefix.
*/
static removePrefix(pluginName) {
return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.slice(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.
*/
static 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.
*/
static removeNamespace(pluginName) {
return pluginName.replace(NAMESPACE_REGEX, "");
}
/**
* 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(pluginName, plugin) {
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix;
// load up environments and rules
this._plugins[shortName] = plugin;
this._environments.importPlugin(plugin, shortName);
this._rules.importPlugin(plugin, shortName);
}
/**
* 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(pluginName) {
return this._plugins[pluginName] || null;
}
/**
* Returns all plugins that are loaded.
* @returns {Object} The plugins cache.
*/
getAll() {
return this._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(pluginName) {
const pluginNamespace = Plugins.getNamespace(pluginName),
pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix,
longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
let plugin = null;
if (pluginName.match(/\s+/)) {
const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);
whitespaceError.messageTemplate = "whitespace-found";
whitespaceError.messageData = {
pluginName: longName
};
throw whitespaceError;
}
if (!this._plugins[shortName]) {
try {
plugin = require(longName);
} catch (pluginLoadErr) {
try {
// Check whether the plugin exists
require.resolve(longName);
} catch (missingPluginErr) {
// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
debug(`Failed to load plugin ${longName}.`);
missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`;
missingPluginErr.messageTemplate = "plugin-missing";
missingPluginErr.messageData = {
pluginName: longName
};
throw missingPluginErr;
}
// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
throw pluginLoadErr;
}
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.
* @throws {Error} If "plugins" in config is not an array
*/
loadAll(pluginNames) {
// if "plugins" in config is not an array, throw an error so user can fix their config.
if (!Array.isArray(pluginNames)) {
const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
throw new Error(pluginNotArrayMessage);
}
// load each plugin by name
pluginNames.forEach(this.load, this);
}
}
module.exports = Plugins;

View File

@@ -0,0 +1,145 @@
/**
* @fileoverview Util class to find config files.
* @author Aliaksei Shytkin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Get the entries for a directory. Including a try-catch may be detrimental to
* function performance, so move it out here a separate function.
* @param {string} directory The directory to search in.
* @returns {string[]} The entries in the directory or an empty array on error.
* @private
*/
function getDirectoryEntries(directory) {
try {
return fs.readdirSync(directory);
} catch (ex) {
return [];
}
}
/**
* 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) {
const fileHash = {};
entries.forEach(entry => {
if (supportedConfigs.indexOf(entry) >= 0) {
const resolvedEntry = path.resolve(directory, entry);
if (fs.statSync(resolvedEntry).isFile()) {
fileHash[entry] = resolvedEntry;
}
}
});
return fileHash;
}
//------------------------------------------------------------------------------
// API
//------------------------------------------------------------------------------
/**
* FileFinder class
*/
class FileFinder {
/**
* @param {string[]} files The basename(s) of the file(s) to find.
* @param {stirng} cwd Current working directory
*/
constructor(files, cwd) {
this.fileNames = Array.isArray(files) ? files : [files];
this.cwd = cwd || process.cwd();
this.cache = {};
}
/**
* Find all instances of files with the specified file names, in directory and
* parent directories. Cache the results.
* Does not check if a matching directory entry is a file.
* Searches for all the file names in this.fileNames.
* Is currently used by lib/config.js to find .eslintrc and package.json files.
* @param {string} directory The directory to start the search from.
* @returns {GeneratorFunction} to iterate the file paths found
*/
*findAllInDirectoryAndParents(directory) {
const cache = this.cache;
if (directory) {
directory = path.resolve(this.cwd, directory);
} else {
directory = this.cwd;
}
if (cache.hasOwnProperty(directory)) {
yield* cache[directory];
return; // to avoid doing the normal loop afterwards
}
const dirs = [];
const fileNames = this.fileNames;
let searched = 0;
do {
dirs[searched++] = directory;
cache[directory] = [];
const filesMap = normalizeDirectoryEntries(getDirectoryEntries(directory), directory, fileNames);
if (Object.keys(filesMap).length) {
for (let k = 0; k < fileNames.length; k++) {
if (filesMap[fileNames[k]]) {
const filePath = filesMap[fileNames[k]];
// Add the file path to the cache of each directory searched.
for (let j = 0; j < searched; j++) {
cache[dirs[j]].push(filePath);
}
yield filePath;
break;
}
}
}
const child = directory;
// Assign parent directory to directory.
directory = path.dirname(directory);
if (directory === child) {
return;
}
} while (!cache.hasOwnProperty(directory));
// Add what has been cached previously to the cache of each directory searched.
for (let i = 0; i < searched; i++) {
dirs.push.apply(cache[dirs[i]], cache[directory]);
}
yield* cache[dirs[0]];
}
}
module.exports = FileFinder;

View File

@@ -0,0 +1,60 @@
/**
* @fileoverview CheckStyle XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
//------------------------------------------------------------------------------
// 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";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<checkstyle version=\"4.3\">";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${xmlEscape(result.filePath)}">`;
messages.forEach(message => {
output += [
`<error line="${xmlEscape(message.line)}"`,
`column="${xmlEscape(message.column)}"`,
`severity="${xmlEscape(getMessageType(message))}"`,
`message="${xmlEscape(message.message)}${message.ruleId ? ` (${message.ruleId})` : ""}"`,
`source="${message.ruleId ? xmlEscape(`eslint.rules.${message.ruleId}`) : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</checkstyle>";
return output;
};

View File

@@ -0,0 +1,138 @@
/**
* @fileoverview Codeframe reporter
* @author Vitor Balocco
*/
"use strict";
const chalk = require("chalk");
const codeFrame = require("babel-code-frame");
const path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {number} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
/**
* Gets a formatted relative file path from an absolute path and a line/column in the file.
* @param {string} filePath The absolute file path to format.
* @param {number} line The line from the file to use for formatting.
* @param {number} column The column from the file to use for formatting.
* @returns {string} The formatted file path.
*/
function formatFilePath(filePath, line, column) {
let relPath = path.relative(process.cwd(), filePath);
if (line && column) {
relPath += `:${line}:${column}`;
}
return chalk.green(relPath);
}
/**
* Gets the formatted output for a given message.
* @param {Object} message The object that represents this message.
* @param {Object} parentResult The result object that this message belongs to.
* @returns {string} The formatted output.
*/
function formatMessage(message, parentResult) {
const type = (message.fatal || message.severity === 2) ? chalk.red("error") : chalk.yellow("warning");
const msg = `${chalk.bold(message.message.replace(/([^ ])\.$/, "$1"))}`;
const ruleId = message.fatal ? "" : chalk.dim(`(${message.ruleId})`);
const filePath = formatFilePath(parentResult.filePath, message.line, message.column);
const sourceCode = parentResult.output ? parentResult.output : parentResult.source;
const firstLine = [
`${type}:`,
`${msg}`,
ruleId ? `${ruleId}` : "",
sourceCode ? `at ${filePath}:` : `at ${filePath}`
].filter(String).join(" ");
const result = [firstLine];
if (sourceCode) {
result.push(
codeFrame(sourceCode, message.line, message.column, { highlightCode: false })
);
}
return result.join("\n");
}
/**
* Gets the formatted output summary for a given number of errors and warnings.
* @param {number} errors The number of errors.
* @param {number} warnings The number of warnings.
* @param {number} fixableErrors The number of fixable errors.
* @param {number} fixableWarnings The number of fixable warnings.
* @returns {string} The formatted output summary.
*/
function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
const summaryColor = errors > 0 ? "red" : "yellow";
const summary = [];
const fixablesSummary = [];
if (errors > 0) {
summary.push(`${errors} ${pluralize("error", errors)}`);
}
if (warnings > 0) {
summary.push(`${warnings} ${pluralize("warning", warnings)}`);
}
if (fixableErrors > 0) {
fixablesSummary.push(`${fixableErrors} ${pluralize("error", fixableErrors)}`);
}
if (fixableWarnings > 0) {
fixablesSummary.push(`${fixableWarnings} ${pluralize("warning", fixableWarnings)}`);
}
let output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
if (fixableErrors || fixableWarnings) {
output += chalk[summaryColor].bold(`\n${fixablesSummary.join(" and ")} potentially fixable with the \`--fix\` option.`);
}
return output;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let errors = 0;
let warnings = 0;
let fixableErrors = 0;
let fixableWarnings = 0;
const resultsWithMessages = results.filter(result => result.messages.length > 0);
let output = resultsWithMessages.reduce((resultsOutput, result) => {
const messages = result.messages.map(message => `${formatMessage(message, result)}\n\n`);
errors += result.errorCount;
warnings += result.warningCount;
fixableErrors += result.fixableErrorCount;
fixableWarnings += result.fixableWarningCount;
return resultsOutput.concat(messages);
}, []).join("\n");
output += "\n";
output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
return (errors + warnings) > 0 ? output : "";
};

View File

@@ -0,0 +1,60 @@
/**
* @fileoverview Compact reporter
* @author Nicholas C. Zakas
*/
"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";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}: `;
output += `line ${message.line || 0}`;
output += `, col ${message.column || 0}`;
output += `, ${getMessageType(message)}`;
output += ` - ${message.message}`;
output += message.ruleId ? ` (${message.ruleId})` : "";
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};

View File

@@ -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="https://eslint.org/docs/rules/<%= ruleId %>" target="_blank"><%= ruleId %></a>
</td>
</tr>

View File

@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<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:medium;
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>

View File

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

View File

@@ -0,0 +1,127 @@
/**
* @fileoverview HTML reporter
* @author Julian Laval
*/
"use strict";
const lodash = require("lodash");
const fs = require("fs");
const path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
const 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.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
/**
* Renders text along the template of x problems (x errors, x warnings)
* @param {string} totalErrors Total errors
* @param {string} totalWarnings Total warnings
* @returns {string} The formatted string, pluralized where necessary
*/
function renderSummary(totalErrors, totalWarnings) {
const totalProblems = totalErrors + totalWarnings;
let 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...
* @param {string} totalErrors Total errors
* @param {string} totalWarnings Total warnings
* @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
*/
function renderColor(totalErrors, totalWarnings) {
if (totalErrors !== 0) {
return 2;
}
if (totalWarnings !== 0) {
return 1;
}
return 0;
}
/**
* 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.
*/
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, message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
return messageTemplate({
parentIndex,
lineNumber,
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, (result, index) => resultTemplate({
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) {
let totalErrors,
totalWarnings;
totalErrors = 0;
totalWarnings = 0;
// Iterate over results to get totals
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
});
return pageTemplate({
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results)
});
};

View File

@@ -0,0 +1,41 @@
/**
* @fileoverview JSLint XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<jslint>";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${result.filePath}">`;
messages.forEach(message => {
output += [
`<issue line="${message.line}"`,
`char="${message.column}"`,
`evidence="${xmlEscape(message.source || "")}"`,
`reason="${xmlEscape(message.message || "")}${message.ruleId ? ` (${message.ruleId})` : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</jslint>";
return output;
};

View File

@@ -0,0 +1,13 @@
/**
* @fileoverview JSON reporter
* @author Burak Yigit Kaya aka BYK
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
return JSON.stringify(results);
};

View File

@@ -0,0 +1,64 @@
/**
* @fileoverview jUnit Reporter
* @author Jamund Ferguson
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
//------------------------------------------------------------------------------
// 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";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
output += "<testsuites>\n";
results.forEach(result => {
const messages = result.messages;
output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
messages.forEach(message => {
const type = message.fatal ? "error" : "failure";
output += `<testcase time="0" name="org.eslint.${message.ruleId || "unknown"}">`;
output += `<${type} message="${xmlEscape(message.message || "")}">`;
output += "<![CDATA[";
output += `line ${message.line || 0}, col `;
output += `${message.column || 0}, ${getMessageType(message)}`;
output += ` - ${xmlEscape(message.message || "")}`;
output += (message.ruleId ? ` (${message.ruleId})` : "");
output += "]]>";
output += `</${type}>`;
output += "</testcase>\n";
});
output += "</testsuite>\n";
});
output += "</testsuites>\n";
return output;
};

View File

@@ -0,0 +1,100 @@
/**
* @fileoverview Stylish reporter
* @author Sindre Sorhus
*/
"use strict";
const chalk = require("chalk"),
stripAnsi = require("strip-ansi"),
table = require("text-table");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return (count === 1 ? word : `${word}s`);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "\n",
errorCount = 0,
warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow";
results.forEach(result => {
const messages = result.messages;
if (messages.length === 0) {
return;
}
errorCount += result.errorCount;
warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
output += `${chalk.underline(result.filePath)}\n`;
output += `${table(
messages.map(message => {
let messageType;
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
summaryColor = "red";
} else {
messageType = chalk.yellow("warning");
}
return [
"",
message.line || 0,
message.column || 0,
messageType,
message.message.replace(/([^ ])\.$/, "$1"),
chalk.dim(message.ruleId || "")
];
}),
{
align: ["", "r", "l"],
stringLength(str) {
return stripAnsi(str).length;
}
}
).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
});
const total = errorCount + warningCount;
if (total > 0) {
output += chalk[summaryColor].bold([
"\u2716 ", total, pluralize(" problem", total),
" (", errorCount, pluralize(" error", errorCount), ", ",
warningCount, pluralize(" warning", warningCount), ")\n"
].join(""));
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
output += chalk[summaryColor].bold([
" ", fixableErrorCount, pluralize(" error", fixableErrorCount), ", ",
fixableWarningCount, pluralize(" warning", fixableWarningCount),
" potentially fixable with the `--fix` option.\n"
].join(""));
}
}
return total > 0 ? output : "";
};

View File

@@ -0,0 +1,150 @@
/**
* @fileoverview "table reporter.
* @author Gajus Kuizinas <gajus@gajus.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const chalk = require("chalk"),
table = require("table").table,
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) {
const 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(message => {
let 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(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) {
let files;
files = results.map(result => {
if (!result.messages.length) {
return "";
}
return `\n${result.filePath}\n\n${drawTable(result.messages)}`;
});
files = files.filter(content => content.trim());
return files.join("");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(report) {
let result,
errorCount,
warningCount;
result = "";
errorCount = 0;
warningCount = 0;
report.forEach(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() {
return true;
}
})}`;
return result;
};

View File

@@ -0,0 +1,90 @@
/**
* @fileoverview TAP reporter
* @author Jonathan Kingston
*/
"use strict";
const yaml = require("js-yaml");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
/**
* Takes in a JavaScript object and outputs a TAP diagnostics string
* @param {Object} diagnostic JavaScript object to be embedded as YAML into output.
* @returns {string} diagnostics string with YAML embedded - TAP version 13 compliant
*/
function outputDiagnostics(diagnostic) {
const prefix = " ";
let output = `${prefix}---\n`;
output += prefix + yaml.safeDump(diagnostic).split("\n").join(`\n${prefix}`);
output += "...\n";
return output;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = `TAP version 13\n1..${results.length}\n`;
results.forEach((result, id) => {
const messages = result.messages;
let testResult = "ok";
let diagnostics = {};
if (messages.length > 0) {
testResult = "not ok";
messages.forEach(message => {
const diagnostic = {
message: message.message,
severity: getMessageType(message),
data: {
line: message.line || 0,
column: message.column || 0,
ruleId: message.ruleId || ""
}
};
// If we have multiple messages place them under a messages key
// The first error will be logged as message key
// This is to adhere to TAP 13 loosely defined specification of having a message key
if ("message" in diagnostics) {
if (typeof diagnostics.messages === "undefined") {
diagnostics.messages = [];
}
diagnostics.messages.push(diagnostic);
} else {
diagnostics = diagnostic;
}
});
}
output += `${testResult} ${id + 1} - ${result.filePath}\n`;
// If we have an error include diagnostics
if (messages.length > 0) {
output += outputDiagnostics(diagnostics);
}
});
return output;
};

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview unix-style formatter.
* @author oshi-shinobu
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}:`;
output += `${message.line || 0}:`;
output += `${message.column || 0}:`;
output += ` ${message.message} `;
output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`;
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};

View File

@@ -0,0 +1,63 @@
/**
* @fileoverview Visual Studio compatible formatter
* @author Ronald Pijnacker
*/
"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";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(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;
};

View File

@@ -0,0 +1,287 @@
/**
* @fileoverview Responsible for loading ignore config files and managing ignore patterns
* @author Jonathan Rajavuori
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
ignore = require("ignore"),
pathUtil = require("./util/path-util");
const debug = require("debug")("eslint:ignored-paths");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const ESLINT_IGNORE_FILENAME = ".eslintignore";
/**
* Adds `"*"` at the end of `"node_modules/"`,
* so that subtle directories could be re-included by .gitignore patterns
* such as `"!node_modules/should_not_ignored"`
*/
const DEFAULT_IGNORE_DIRS = [
"/node_modules/*",
"/bower_components/*"
];
const DEFAULT_OPTIONS = {
dotfiles: false,
cwd: process.cwd()
};
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Find a file in the current directory.
* @param {string} cwd Current working directory
* @param {string} name File name
* @returns {string} Path of ignore file or an empty string.
*/
function findFile(cwd, name) {
const ignoreFilePath = path.resolve(cwd, name);
return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
}
/**
* Find an ignore file in the current directory.
* @param {string} cwd Current working directory
* @returns {string} Path of ignore file or an empty string.
*/
function findIgnoreFile(cwd) {
return findFile(cwd, ESLINT_IGNORE_FILENAME);
}
/**
* Find an package.json file in the current directory.
* @param {string} cwd Current working directory
* @returns {string} Path of package.json file or an empty string.
*/
function findPackageJSONFile(cwd) {
return findFile(cwd, "package.json");
}
/**
* Merge options with defaults
* @param {Object} options Options to merge with DEFAULT_OPTIONS constant
* @returns {Object} Merged options
*/
function mergeDefaultOptions(options) {
options = (options || {});
return Object.assign({}, DEFAULT_OPTIONS, options);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* IgnoredPaths class
*/
class IgnoredPaths {
/**
* @param {Object} options object containing 'ignore', 'ignorePath' and 'patterns' properties
*/
constructor(options) {
options = mergeDefaultOptions(options);
this.cache = {};
/**
* 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);
}
this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []);
this.baseDir = options.cwd;
this.ig = {
custom: ignore(),
default: ignore()
};
// Add a way to keep track of ignored files. This was present in node-ignore
// 2.x, but dropped for now as of 3.0.10.
this.ig.custom.ignoreFiles = [];
this.ig.default.ignoreFiles = [];
if (options.dotfiles !== true) {
/*
* ignore files beginning with a dot, but not files in a parent or
* ancestor directory (which in relative format will begin with `../`).
*/
addPattern(this.ig.default, [".*", "!../"]);
}
addPattern(this.ig.default, this.defaultPatterns);
if (options.ignore !== false) {
let ignorePath;
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(path.resolve(options.cwd, ignorePath));
this.addIgnoreFile(this.ig.custom, ignorePath);
this.addIgnoreFile(this.ig.default, ignorePath);
} else {
try {
// if the ignoreFile does not exist, check package.json for eslintIgnore
const packageJSONPath = findPackageJSONFile(options.cwd);
if (packageJSONPath) {
let packageJSONOptions;
try {
packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"));
} catch (e) {
debug("Could not read package.json file to check eslintIgnore property");
throw e;
}
if (packageJSONOptions.eslintIgnore) {
if (Array.isArray(packageJSONOptions.eslintIgnore)) {
packageJSONOptions.eslintIgnore.forEach(pattern => {
addPattern(this.ig.custom, pattern);
addPattern(this.ig.default, pattern);
});
} else {
throw new TypeError("Package.json eslintIgnore property requires an array of paths");
}
}
}
} catch (e) {
debug("Could not find package.json to check eslintIgnore property");
throw e;
}
}
if (options.ignorePattern) {
addPattern(this.ig.custom, options.ignorePattern);
addPattern(this.ig.default, options.ignorePattern);
}
}
this.options = options;
}
/**
* read ignore filepath
* @param {string} filePath, file to add to ig
* @returns {array} raw ignore rules
*/
readIgnoreFile(filePath) {
if (typeof this.cache[filePath] === "undefined") {
this.cache[filePath] = fs.readFileSync(filePath, "utf8");
}
return this.cache[filePath];
}
/**
* 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
*/
addIgnoreFile(ig, filePath) {
ig.ignoreFiles.push(filePath);
return ig.add(this.readIgnoreFile(filePath));
}
/**
* 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
*/
contains(filepath, category) {
let result = false;
const absolutePath = path.resolve(this.options.cwd, filepath);
const relativePath = pathUtil.getRelativePath(absolutePath, this.baseDir);
if ((typeof category === "undefined") || (category === "default")) {
result = result || (this.ig.default.filter([relativePath]).length === 0);
}
if ((typeof category === "undefined") || (category === "custom")) {
result = result || (this.ig.custom.filter([relativePath]).length === 0);
}
return result;
}
/**
* Returns a list of dir patterns for glob to ignore
* @returns {function()} method to check whether a folder should be ignored by glob.
*/
getIgnoredFoldersGlobChecker() {
const ig = ignore().add(DEFAULT_IGNORE_DIRS);
if (this.options.dotfiles !== true) {
// Ignore hidden folders. (This cannot be ".*", or else it's not possible to unignore hidden files)
ig.add([".*/*", "!../"]);
}
if (this.options.ignore) {
ig.add(this.ig.custom);
}
const filter = ig.createFilter();
const base = this.baseDir;
return function(absolutePath) {
const relative = pathUtil.getRelativePath(absolutePath, base);
if (!relative) {
return false;
}
return !filter(relative);
};
}
}
module.exports = IgnoredPaths;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
/**
* @fileoverview Module for loading rules from files and directories.
* @author Michael Ficarra
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path");
const rulesDirCache = {};
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Load all rule modules from specified directory.
* @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, cwd) {
if (!rulesDir) {
rulesDir = path.join(__dirname, "rules");
} else {
rulesDir = path.resolve(cwd, rulesDir);
}
// cache will help performance as IO operation are expensive
if (rulesDirCache[rulesDir]) {
return rulesDirCache[rulesDir];
}
const rules = Object.create(null);
fs.readdirSync(rulesDir).forEach(file => {
if (path.extname(file) !== ".js") {
return;
}
rules[file.slice(0, -3)] = path.join(rulesDir, file);
});
rulesDirCache[rulesDir] = rules;
return rules;
};

View File

@@ -0,0 +1,28 @@
/**
* @fileoverview Handle logging for ESLint
* @author Gyandeep Singh
*/
"use strict";
/* eslint no-console: "off" */
/* istanbul ignore next */
module.exports = {
/**
* Cover for console.log
* @returns {void}
*/
info() {
console.log.apply(console, arguments);
},
/**
* Cover for console.error
* @returns {void}
*/
error() {
console.error.apply(console, arguments);
}
};

View File

@@ -0,0 +1,235 @@
/**
* @fileoverview Options configuration for optionator.
* @author George Zahariev
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const optionator = require("optionator");
//------------------------------------------------------------------------------
// Initialization and Public Interface
//------------------------------------------------------------------------------
// exports "parse(args)", "generateHelp()", and "generateHelpForOption(optionName)"
module.exports = optionator({
prepend: "eslint [options] file.js [file.js] [dir]",
defaults: {
concatRepeatedArrays: true,
mergeRepeatedObjects: true
},
options: [
{
heading: "Basic configuration"
},
{
option: "config",
alias: "c",
type: "path::String",
description: "Use configuration from this file or shareable config"
},
{
option: "eslintrc",
type: "Boolean",
default: "true",
description: "Disable use of configuration from .eslintrc"
},
{
option: "env",
type: "[String]",
description: "Specify environments"
},
{
option: "ext",
type: "[String]",
default: ".js",
description: "Specify JavaScript file extensions"
},
{
option: "global",
type: "[String]",
description: "Define global variables"
},
{
option: "parser",
type: "String",
description: "Specify the parser to be used"
},
{
option: "parser-options",
type: "Object",
description: "Specify parser options"
},
{
heading: "Caching"
},
{
option: "cache",
type: "Boolean",
default: "false",
description: "Only check changed files"
},
{
option: "cache-file",
type: "path::String",
default: ".eslintcache",
description: "Path to the cache file. Deprecated: use --cache-location"
},
{
option: "cache-location",
type: "path::String",
description: "Path to the cache file or directory"
},
{
heading: "Specifying rules and plugins"
},
{
option: "rulesdir",
type: "[path::String]",
description: "Use additional rules from this directory"
},
{
option: "plugin",
type: "[String]",
description: "Specify plugins"
},
{
option: "rule",
type: "Object",
description: "Specify rules"
},
{
heading: "Ignoring files"
},
{
option: "ignore-path",
type: "path::String",
description: "Specify path of ignore file"
},
{
option: "ignore",
type: "Boolean",
default: "true",
description: "Disable use of ignore files and patterns"
},
{
option: "ignore-pattern",
type: "[String]",
description: "Pattern of files to ignore (in addition to those in .eslintignore)",
concatRepeatedArrays: [true, {
oneValuePerFlag: true
}]
},
{
heading: "Using stdin"
},
{
option: "stdin",
type: "Boolean",
default: "false",
description: "Lint code provided on <STDIN>"
},
{
option: "stdin-filename",
type: "String",
description: "Specify filename to process STDIN as"
},
{
heading: "Handling warnings"
},
{
option: "quiet",
type: "Boolean",
default: "false",
description: "Report errors only"
},
{
option: "max-warnings",
type: "Int",
default: "-1",
description: "Number of warnings to trigger nonzero exit code"
},
{
heading: "Output"
},
{
option: "output-file",
alias: "o",
type: "path::String",
description: "Specify file to write report to"
},
{
option: "format",
alias: "f",
type: "String",
default: "stylish",
description: "Use a specific output format"
},
{
option: "color",
type: "Boolean",
alias: "no-color",
description: "Force enabling/disabling of color"
},
{
heading: "Miscellaneous"
},
{
option: "init",
type: "Boolean",
default: "false",
description: "Run config initialization wizard"
},
{
option: "fix",
type: "Boolean",
default: false,
description: "Automatically fix problems"
},
{
option: "fix-dry-run",
type: "Boolean",
default: false,
description: "Automatically fix problems without saving the changes to the file system"
},
{
option: "debug",
type: "Boolean",
default: false,
description: "Output debugging information"
},
{
option: "help",
alias: "h",
type: "Boolean",
description: "Show help"
},
{
option: "version",
alias: "v",
type: "Boolean",
description: "Output the version number"
},
{
option: "inline-config",
type: "Boolean",
default: "true",
description: "Prevent comments from changing config or rules"
},
{
option: "report-unused-disable-directives",
type: "Boolean",
default: false,
description: "Adds reported errors for unused eslint-disable directives"
},
{
option: "print-config",
type: "path::String",
description: "Print the configuration for the given file"
}
]
});

View File

@@ -0,0 +1,274 @@
/**
* @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert");
const ruleFixer = require("./util/rule-fixer");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* An error message description
* @typedef {Object} MessageDescriptor
* @property {ASTNode} [node] The reported node
* @property {Location} loc The location of the problem.
* @property {string} message The problem message.
* @property {Object} [data] Optional data to use to fill in placeholders in the
* message.
* @property {Function} [fix] The function to call that creates a fix command.
*/
//------------------------------------------------------------------------------
// Module Definition
//------------------------------------------------------------------------------
/**
* Translates a multi-argument context.report() call into a single object argument call
* @param {...*} arguments A list of arguments passed to `context.report`
* @returns {MessageDescriptor} A normalized object containing report information
*/
function normalizeMultiArgReportCall() {
// If there is one argument, it is considered to be a new-style call already.
if (arguments.length === 1) {
return arguments[0];
}
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
if (typeof arguments[1] === "string") {
return {
node: arguments[0],
message: arguments[1],
data: arguments[2],
fix: arguments[3]
};
}
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
return {
node: arguments[0],
loc: arguments[1],
message: arguments[2],
data: arguments[3],
fix: arguments[4]
};
}
/**
* Asserts that either a loc or a node was provided, and the node is valid if it was provided.
* @param {MessageDescriptor} descriptor A descriptor to validate
* @returns {void}
* @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
*/
function assertValidNodeInfo(descriptor) {
if (descriptor.node) {
assert(typeof descriptor.node === "object", "Node must be an object");
} else {
assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
}
}
/**
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
*/
function normalizeReportLoc(descriptor) {
if (descriptor.loc) {
if (descriptor.loc.start) {
return descriptor.loc;
}
return { start: descriptor.loc, end: null };
}
return descriptor.node.loc;
}
/**
* Interpolates data placeholders in report messages
* @param {MessageDescriptor} descriptor The report message descriptor.
* @returns {string} The interpolated message for the descriptor
*/
function normalizeMessagePlaceholders(descriptor) {
if (!descriptor.data) {
return descriptor.message;
}
return descriptor.message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
if (term in descriptor.data) {
return descriptor.data[term];
}
return fullMatch;
});
}
/**
* Compares items in a fixes array by range.
* @param {Fix} a The first message.
* @param {Fix} b The second message.
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @private
*/
function compareFixesByRange(a, b) {
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
}
/**
* Merges the given fixes array into one.
* @param {Fix[]} fixes The fixes to merge.
* @param {SourceCode} sourceCode The source code object to get the text between fixes.
* @returns {{text: string, range: [number, number]}} The merged fixes
*/
function mergeFixes(fixes, sourceCode) {
if (fixes.length === 0) {
return null;
}
if (fixes.length === 1) {
return fixes[0];
}
fixes.sort(compareFixesByRange);
const originalText = sourceCode.text;
const start = fixes[0].range[0];
const end = fixes[fixes.length - 1].range[1];
let text = "";
let lastPos = Number.MIN_SAFE_INTEGER;
for (const fix of fixes) {
assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
if (fix.range[0] >= 0) {
text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
}
text += fix.text;
lastPos = fix.range[1];
}
text += originalText.slice(Math.max(0, start, lastPos), end);
return { range: [start, end], text };
}
/**
* Gets one fix object from the given descriptor.
* If the descriptor retrieves multiple fixes, this merges those to one.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {SourceCode} sourceCode The source code object to get text between fixes.
* @returns {({text: string, range: [number, number]}|null)} The fix for the descriptor
*/
function normalizeFixes(descriptor, sourceCode) {
if (typeof descriptor.fix !== "function") {
return null;
}
// @type {null | Fix | Fix[] | IterableIterator<Fix>}
const fix = descriptor.fix(ruleFixer);
// Merge to one.
if (fix && Symbol.iterator in fix) {
return mergeFixes(Array.from(fix), sourceCode);
}
return fix;
}
/**
* Creates information about the report from a descriptor
* @param {{
* ruleId: string,
* severity: (0|1|2),
* node: (ASTNode|null),
* message: string,
* loc: {start: SourceLocation, end: (SourceLocation|null)},
* fix: ({text: string, range: [number, number]}|null),
* sourceLines: string[]
* }} options Information about the problem
* @returns {function(...args): {
* ruleId: string,
* severity: (0|1|2),
* message: string,
* line: number,
* column: number,
* endLine: (number|undefined),
* endColumn: (number|undefined),
* nodeType: (string|null),
* source: string,
* fix: ({text: string, range: [number, number]}|null)
* }} Information about the report
*/
function createProblem(options) {
const problem = {
ruleId: options.ruleId,
severity: options.severity,
message: options.message,
line: options.loc.start.line,
column: options.loc.start.column + 1,
nodeType: options.node && options.node.type || null,
source: options.sourceLines[options.loc.start.line - 1] || ""
};
if (options.loc.end) {
problem.endLine = options.loc.end.line;
problem.endColumn = options.loc.end.column + 1;
}
if (options.fix) {
problem.fix = options.fix;
}
return problem;
}
/**
* Returns a function that converts the arguments of a `context.report` call from a rule into a reported
* problem for the Node.js API.
* @param {{ruleId: string, severity: number, sourceCode: SourceCode}} metadata Metadata for the reported problem
* @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
* @returns {function(...args): {
* ruleId: string,
* severity: (0|1|2),
* message: string,
* line: number,
* column: number,
* endLine: (number|undefined),
* endColumn: (number|undefined),
* nodeType: (string|null),
* source: string,
* fix: ({text: string, range: [number, number]}|null)
* }}
* Information about the report
*/
module.exports = function createReportTranslator(metadata) {
/*
* `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
* The report translator itself (i.e. the function that `createReportTranslator` returns) gets
* called every time a rule reports a problem, which happens much less frequently (usually, the vast
* majority of rules don't report any problems for a given file).
*/
return function() {
const descriptor = normalizeMultiArgReportCall.apply(null, arguments);
assertValidNodeInfo(descriptor);
return createProblem({
ruleId: metadata.ruleId,
severity: metadata.severity,
node: descriptor.node,
message: normalizeMessagePlaceholders(descriptor),
loc: normalizeReportLoc(descriptor),
fix: normalizeFixes(descriptor, metadata.sourceCode),
sourceLines: metadata.sourceCode.lines
});
};
};

View File

@@ -0,0 +1,140 @@
/**
* @fileoverview Defines a storage for rules.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const loadRules = require("./load-rules");
const ruleReplacements = require("../conf/replacements").rules;
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Creates a stub rule that gets used when a rule with a given ID is not found.
* @param {string} ruleId The ID of the missing rule
* @returns {{create: function(RuleContext): Object}} A rule that reports an error at the first location
* in the program. The report has the message `Definition for rule '${ruleId}' was not found` if the rule is unknown,
* or `Rule '${ruleId}' was removed and replaced by: ${replacements.join(", ")}` if the rule is known to have been
* replaced.
*/
const createMissingRule = lodash.memoize(ruleId => {
const message = Object.prototype.hasOwnProperty.call(ruleReplacements, ruleId)
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements[ruleId].join(", ")}`
: `Definition for rule '${ruleId}' was not found`;
return {
create: context => ({
Program() {
context.report({
loc: { line: 1, column: 0 },
message
});
}
})
};
});
/**
* Normalizes a rule module to the new-style API
* @param {(Function|{create: Function})} rule A rule object, which can either be a function
* ("old-style") or an object with a `create` method ("new-style")
* @returns {{create: Function}} A new-style rule.
*/
function normalizeRule(rule) {
return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class Rules {
constructor() {
this._rules = Object.create(null);
this.load();
}
/**
* Registers a rule module for rule id in storage.
* @param {string} ruleId Rule id (file name).
* @param {Function} ruleModule Rule handler.
* @returns {void}
*/
define(ruleId, ruleModule) {
this._rules[ruleId] = normalizeRule(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} cwd Current working directory
* @returns {void}
*/
load(rulesDir, cwd) {
const newRules = loadRules(rulesDir, cwd);
Object.keys(newRules).forEach(ruleId => {
this.define(ruleId, newRules[ruleId]);
});
}
/**
* Registers all given rules of a plugin.
* @param {Object} plugin The plugin object to import.
* @param {string} pluginName The name of the plugin without prefix (`eslint-plugin-`).
* @returns {void}
*/
importPlugin(plugin, pluginName) {
if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleId => {
const qualifiedRuleId = `${pluginName}/${ruleId}`,
rule = plugin.rules[ruleId];
this.define(qualifiedRuleId, rule);
});
}
}
/**
* Access rule handler by id (file name).
* @param {string} ruleId Rule id (file name).
* @returns {{create: Function, schema: JsonSchema[]}}
* A rule. This is normalized to always have the new-style shape with a `create` method.
*/
get(ruleId) {
if (!Object.prototype.hasOwnProperty.call(this._rules, ruleId)) {
return createMissingRule(ruleId);
}
if (typeof this._rules[ruleId] === "string") {
return normalizeRule(require(this._rules[ruleId]));
}
return this._rules[ruleId];
}
/**
* Get an object with all currently loaded rules
* @returns {Map} All loaded rules
*/
getAllLoadedRules() {
const allRules = new Map();
Object.keys(this._rules).forEach(name => {
const rule = this.get(name);
allRules.set(name, rule);
});
return allRules;
}
}
module.exports = Rules;

View File

@@ -0,0 +1,3 @@
rules:
internal-no-invalid-meta: "error"
internal-consistent-docs-description: "error"

View File

@@ -0,0 +1,156 @@
/**
* @fileoverview Rule to flag wrapping non-iife in parens
* @author Gyandeep Singh
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node - A node to check.
* @param {string} name - An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks whether or not a given node is an argument of a specified method call.
* @param {ASTNode} node - A node to check.
* @param {number} index - An expected index of the node in arguments.
* @param {string} object - An expected name of the object of the method.
* @param {string} property - An expected name of the method.
* @returns {boolean} `true` if the node is an argument of the specified method call.
*/
function isArgumentOfMethodCall(node, index, object, property) {
const parent = node.parent;
return (
parent.type === "CallExpression" &&
parent.callee.type === "MemberExpression" &&
parent.callee.computed === false &&
isIdentifier(parent.callee.object, object) &&
isIdentifier(parent.callee.property, property) &&
parent.arguments[index] === node
);
}
/**
* Checks whether or not a given node is a property descriptor.
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is a property descriptor.
*/
function isPropertyDescriptor(node) {
// Object.defineProperty(obj, "foo", {set: ...})
if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
) {
return true;
}
/*
* Object.defineProperties(obj, {foo: {set: ...}})
* Object.create(proto, {foo: {set: ...}})
*/
node = node.parent.parent;
return node.type === "ObjectExpression" && (
isArgumentOfMethodCall(node, 1, "Object", "create") ||
isArgumentOfMethodCall(node, 1, "Object", "defineProperties")
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce getter and setter pairs in objects",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
getWithoutSet: {
type: "boolean"
},
setWithoutGet: {
type: "boolean"
}
},
additionalProperties: false
}]
},
create(context) {
const config = context.options[0] || {};
const checkGetWithoutSet = config.getWithoutSet === true;
const 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) {
let isSetPresent = false;
let isGetPresent = false;
const isDescriptor = isPropertyDescriptor(node);
for (let i = 0, end = node.properties.length; i < end; i++) {
const property = node.properties[i];
let propToCheck = "";
if (property.kind === "init") {
if (isDescriptor && !property.computed) {
propToCheck = property.key.name;
}
} else {
propToCheck = property.kind;
}
switch (propToCheck) {
case "set":
isSetPresent = true;
break;
case "get":
isGetPresent = true;
break;
default:
// Do nothing
}
if (isSetPresent && isGetPresent) {
break;
}
}
if (checkSetWithoutGet && isSetPresent && !isGetPresent) {
context.report({ node, message: "Getter is not present." });
} else if (checkGetWithoutSet && isGetPresent && !isSetPresent) {
context.report({ node, message: "Setter is not present." });
}
}
return {
ObjectExpression(node) {
if (checkSetWithoutGet || checkGetWithoutSet) {
checkLonelySetGet(node);
}
}
};
}
};

View File

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

View File

@@ -0,0 +1,229 @@
/**
* @fileoverview Disallows or enforces spaces inside of array brackets.
* @author Jamund Ferguson
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent 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(context) {
const 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;
}
const options = {
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,
loc: token.loc.start,
message: "There should be no space after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
const nextToken = sourceCode.getTokenAfter(token);
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/**
* 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,
loc: token.loc.start,
message: "There should be no space before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(token);
return fixer.removeRange([previousToken.range[1], token.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,
loc: token.loc.start,
message: "A space is required after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* 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,
loc: token.loc.start,
message: "A space is required before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextBefore(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");
}
/**
* 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");
}
/**
* 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;
}
const first = sourceCode.getFirstToken(node),
second = sourceCode.getFirstToken(node, 1),
last = node.typeAnnotation
? sourceCode.getTokenBefore(node.typeAnnotation)
: sourceCode.getLastToken(node),
penultimate = sourceCode.getTokenBefore(last),
firstElement = node.elements[0],
lastElement = node.elements[node.elements.length - 1];
const openingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(firstElement) ||
options.arraysInArraysException && isArrayType(firstElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
const closingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(lastElement) ||
options.arraysInArraysException && isArrayType(lastElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
if (astUtils.isTokenOnSameLine(first, second)) {
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
}
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
}
if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) {
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
}
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ArrayPattern: validateArraySpacing,
ArrayExpression: validateArraySpacing
};
}
};

View File

@@ -0,0 +1,228 @@
/**
* @fileoverview Rule to enforce return statements in callbacks of array's methods
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
const 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;
}
/**
* 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(astUtils.getStaticPropertyName(node) || "")
);
}
/**
* 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) {
const 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": {
const 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 = {
meta: {
docs: {
description: "enforce `return` statements in callbacks of array methods",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
let funcInfo = {
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false,
node: null
};
/**
* 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,
loc: getLocation(node, context.getSourceCode()).loc.start,
message: funcInfo.hasReturn
? "Expected to return a value at the end of {{name}}."
: "Expected to return a value in {{name}}.",
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node)
}
});
}
}
return {
// Stacks this function's information.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
shouldCheck:
TARGET_NODE_TYPE.test(node.type) &&
node.body.type === "BlockStatement" &&
isCallbackOfArrayMethod(node) &&
!node.async &&
!node.generator,
node
};
},
// Pops this function's information.
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Checks the return statement is valid.
ReturnStatement(node) {
if (funcInfo.shouldCheck) {
funcInfo.hasReturn = true;
if (!node.argument) {
context.report({
node,
message: "{{name}} expected a return value.",
data: {
name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
}
});
}
}
},
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};

View File

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

View File

@@ -0,0 +1,209 @@
/**
* @fileoverview Rule to require braces in arrow function body.
* @author Alberto Rodríguez
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require braces around arrow function bodies",
category: "ECMAScript 6",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always", "never"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["as-needed"]
},
{
type: "object",
properties: {
requireReturnForObjectLiteral: { type: "boolean" }
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
},
fixable: "code"
},
create(context) {
const options = context.options;
const always = options[0] === "always";
const asNeeded = !options[0] || options[0] === "as-needed";
const never = options[0] === "never";
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
const sourceCode = context.getSourceCode();
/**
* Checks whether the given node has ASI problem or not.
* @param {Token} token The token to check.
* @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
*/
function hasASIProblem(token) {
return token && token.type === "Punctuator" && /^[([/`+-]/.test(token.value);
}
/**
* Gets the closing parenthesis which is the pair of the given opening parenthesis.
* @param {Token} token The opening parenthesis token to get.
* @returns {Token} The found closing parenthesis token.
*/
function findClosingParen(token) {
let node = sourceCode.getNodeByRangeIndex(token.range[1]);
while (!astUtils.isParenthesised(sourceCode, node)) {
node = node.parent;
}
return sourceCode.getTokenAfter(node);
}
/**
* Determines whether a arrow function body needs braces
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function validate(node) {
const arrowBody = node.body;
if (arrowBody.type === "BlockStatement") {
const blockBody = arrowBody.body;
if (blockBody.length !== 1 && !never) {
return;
}
if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" &&
blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") {
return;
}
if (never || asNeeded && blockBody[0].type === "ReturnStatement") {
context.report({
node,
loc: arrowBody.loc.start,
message: "Unexpected block statement surrounding arrow body.",
fix(fixer) {
const fixes = [];
if (blockBody.length !== 1 ||
blockBody[0].type !== "ReturnStatement" ||
!blockBody[0].argument ||
hasASIProblem(sourceCode.getTokenAfter(arrowBody))
) {
return fixes;
}
const openingBrace = sourceCode.getFirstToken(arrowBody);
const closingBrace = sourceCode.getLastToken(arrowBody);
const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1);
const lastValueToken = sourceCode.getLastToken(blockBody[0]);
const commentsExist =
sourceCode.commentsExistBetween(openingBrace, firstValueToken) ||
sourceCode.commentsExistBetween(lastValueToken, closingBrace);
// Remove tokens around the return value.
// If comments don't exist, remove extra spaces as well.
if (commentsExist) {
fixes.push(
fixer.remove(openingBrace),
fixer.remove(closingBrace),
fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword
);
} else {
fixes.push(
fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
);
}
// If the first token of the reutrn value is `{`,
// enclose the return value by parentheses to avoid syntax error.
if (astUtils.isOpeningBraceToken(firstValueToken)) {
fixes.push(
fixer.insertTextBefore(firstValueToken, "("),
fixer.insertTextAfter(lastValueToken, ")")
);
}
// If the last token of the return statement is semicolon, remove it.
// Non-block arrow body is an expression, not a statement.
if (astUtils.isSemicolonToken(lastValueToken)) {
fixes.push(fixer.remove(lastValueToken));
}
return fixes;
}
});
}
} else {
if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) {
context.report({
node,
loc: arrowBody.loc.start,
message: "Expected block statement surrounding arrow body.",
fix(fixer) {
const fixes = [];
const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
const lastBodyToken = sourceCode.getLastToken(node);
const isParenthesisedObjectLiteral =
astUtils.isOpeningParenToken(firstBodyToken) &&
astUtils.isOpeningBraceToken(sourceCode.getTokenAfter(firstBodyToken));
// Wrap the value by a block and a return statement.
fixes.push(
fixer.insertTextBefore(firstBodyToken, "{return "),
fixer.insertTextAfter(lastBodyToken, "}")
);
// If the value is object literal, remove parentheses which were forced by syntax.
if (isParenthesisedObjectLiteral) {
fixes.push(
fixer.remove(firstBodyToken),
fixer.remove(findClosingParen(firstBodyToken))
);
}
return fixes;
}
});
}
}
}
return {
"ArrowFunctionExpression:exit": validate
};
}
};

View File

@@ -0,0 +1,154 @@
/**
* @fileoverview Rule to require parens in arrow function arguments.
* @author Jxck
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require parentheses around arrow function arguments",
category: "ECMAScript 6",
recommended: false
},
fixable: "code",
schema: [
{
enum: ["always", "as-needed"]
},
{
type: "object",
properties: {
requireForBlockBody: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const message = "Expected parentheses around arrow function argument.";
const asNeededMessage = "Unexpected parentheses around single function argument.";
const asNeeded = context.options[0] === "as-needed";
const requireForBlockBodyMessage = "Unexpected parentheses around single function argument having a body with no curly braces";
const requireForBlockBodyNoParensMessage = "Expected parentheses around arrow function argument having a body with curly braces.";
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;
const sourceCode = context.getSourceCode();
/**
* Determines whether a arrow function argument end with `)`
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function parens(node) {
const isAsync = node.async;
const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0);
/**
* Remove the parenthesis around a parameter
* @param {Fixer} fixer Fixer
* @returns {string} fixed parameter
*/
function fixParamsWithParenthesis(fixer) {
const paramToken = sourceCode.getTokenAfter(firstTokenOfParam);
// ES8 allows Trailing commas in function parameter lists and calls
// https://github.com/eslint/eslint/issues/8834
const closingParenToken = sourceCode.getTokenAfter(paramToken, astUtils.isClosingParenToken);
const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null;
const shouldAddSpaceForAsync = asyncToken && (asyncToken.range[1] === firstTokenOfParam.range[0]);
return fixer.replaceTextRange([
firstTokenOfParam.range[0],
closingParenToken.range[1]
], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`);
}
// "as-needed", { "requireForBlockBody": true }: x => x
if (
requireForBlockBody &&
node.params.length === 1 &&
node.params[0].type === "Identifier" &&
!node.params[0].typeAnnotation &&
node.body.type !== "BlockStatement" &&
!node.returnType
) {
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: requireForBlockBodyMessage,
fix: fixParamsWithParenthesis
});
}
return;
}
if (
requireForBlockBody &&
node.body.type === "BlockStatement"
) {
if (!astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: requireForBlockBodyNoParensMessage,
fix(fixer) {
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
}
});
}
return;
}
// "as-needed": x => x
if (asNeeded &&
node.params.length === 1 &&
node.params[0].type === "Identifier" &&
!node.params[0].typeAnnotation &&
!node.returnType
) {
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
context.report({
node,
message: asNeededMessage,
fix: fixParamsWithParenthesis
});
}
return;
}
if (firstTokenOfParam.type === "Identifier") {
const after = sourceCode.getTokenAfter(firstTokenOfParam);
// (x) => x
if (after.value !== ")") {
context.report({
node,
message,
fix(fixer) {
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
}
});
}
}
}
return {
ArrowFunctionExpression: parens
};
}
};

View File

@@ -0,0 +1,149 @@
/**
* @fileoverview Rule to define spacing before/after arrow function's arrow.
* @author Jxck
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing before and after the arrow in arrow functions",
category: "ECMAScript 6",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: {
type: "boolean"
},
after: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
// merge rules with default
const rule = { before: true, after: true },
option = context.options[0] || {};
rule.before = option.before !== false;
rule.after = option.after !== false;
const sourceCode = context.getSourceCode();
/**
* Get tokens of arrow(`=>`) and before/after arrow.
* @param {ASTNode} node The arrow function node.
* @returns {Object} Tokens of arrow and before/after arrow.
*/
function getTokens(node) {
const arrow = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
return {
before: sourceCode.getTokenBefore(arrow),
arrow,
after: sourceCode.getTokenAfter(arrow)
};
}
/**
* Count spaces before/after arrow(`=>`) token.
* @param {Object} tokens Tokens before/after arrow.
* @returns {Object} count of space before/after arrow.
*/
function countSpaces(tokens) {
const before = tokens.arrow.range[0] - tokens.before.range[1];
const after = tokens.after.range[0] - tokens.arrow.range[1];
return { before, after };
}
/**
* Determines whether space(s) before after arrow(`=>`) is satisfy rule.
* if before/after value is `true`, there should be space(s).
* if before/after value is `false`, there should be no space.
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function spaces(node) {
const tokens = getTokens(node);
const countSpace = countSpaces(tokens);
if (rule.before) {
// should be space(s) before arrow
if (countSpace.before === 0) {
context.report({
node: tokens.before,
message: "Missing space before =>.",
fix(fixer) {
return fixer.insertTextBefore(tokens.arrow, " ");
}
});
}
} else {
// should be no space before arrow
if (countSpace.before > 0) {
context.report({
node: tokens.before,
message: "Unexpected space before =>.",
fix(fixer) {
return fixer.removeRange([tokens.before.range[1], tokens.arrow.range[0]]);
}
});
}
}
if (rule.after) {
// should be space(s) after arrow
if (countSpace.after === 0) {
context.report({
node: tokens.after,
message: "Missing space after =>.",
fix(fixer) {
return fixer.insertTextAfter(tokens.arrow, " ");
}
});
}
} else {
// should be no space after arrow
if (countSpace.after > 0) {
context.report({
node: tokens.after,
message: "Unexpected space after =>.",
fix(fixer) {
return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]);
}
});
}
}
}
return {
ArrowFunctionExpression: spaces
};
}
};

View File

@@ -0,0 +1,115 @@
/**
* @fileoverview Rule to check for "block scoped" variables by binding context
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce the use of variables within the scope they are defined",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
let stack = [];
/**
* Makes a block scope.
* @param {ASTNode} node - A node of a scope.
* @returns {void}
*/
function enterScope(node) {
stack.push(node.range);
}
/**
* Pops the last block scope.
* @returns {void}
*/
function exitScope() {
stack.pop();
}
/**
* Reports a given reference.
* @param {eslint-scope.Reference} reference - A reference to report.
* @returns {void}
*/
function report(reference) {
const identifier = reference.identifier;
context.report({ node: identifier, message: "'{{name}}' used outside of binding context.", data: { name: identifier.name } });
}
/**
* Finds and reports references which are outside of valid scopes.
* @param {ASTNode} node - A node to get variables.
* @returns {void}
*/
function checkForVariables(node) {
if (node.kind !== "var") {
return;
}
// Defines a predicate to check whether or not a given reference is outside of valid scope.
const scopeRange = stack[stack.length - 1];
/**
* Check if a reference is out of scope
* @param {ASTNode} reference node to examine
* @returns {boolean} True is its outside the scope
* @private
*/
function isOutsideOfScope(reference) {
const idRange = reference.identifier.range;
return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1];
}
// Gets declared variables, and checks its references.
const variables = context.getDeclaredVariables(node);
for (let i = 0; i < variables.length; ++i) {
// Reports.
variables[i]
.references
.filter(isOutsideOfScope)
.forEach(report);
}
}
return {
Program(node) {
stack = [node.range];
},
// Manages scopes.
BlockStatement: enterScope,
"BlockStatement:exit": exitScope,
ForStatement: enterScope,
"ForStatement:exit": exitScope,
ForInStatement: enterScope,
"ForInStatement:exit": exitScope,
ForOfStatement: enterScope,
"ForOfStatement:exit": exitScope,
SwitchStatement: enterScope,
"SwitchStatement:exit": exitScope,
CatchClause: enterScope,
"CatchClause:exit": exitScope,
// Finds and reports references which are outside of valid scope.
VariableDeclaration: checkForVariables
};
}
};

View File

@@ -0,0 +1,137 @@
/**
* @fileoverview A rule to disallow or enforce spaces inside of single line blocks.
* @author Toru Nagashima
*/
"use strict";
const util = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow or enforce spaces inside of blocks after opening block and before closing block",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{ enum: ["always", "never"] }
]
},
create(context) {
const always = (context.options[0] !== "never"),
message = always ? "Requires a space" : "Unexpected space(s)",
sourceCode = context.getSourceCode();
/**
* Gets the open brace token from a given node.
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
* @returns {Token} The token of the open brace.
*/
function getOpenBrace(node) {
if (node.type === "SwitchStatement") {
if (node.cases.length > 0) {
return sourceCode.getTokenBefore(node.cases[0]);
}
return sourceCode.getLastToken(node, 1);
}
return sourceCode.getFirstToken(node);
}
/**
* Checks whether or not:
* - given tokens are on same line.
* - there is/isn't a space between given tokens.
* @param {Token} left - A token to check.
* @param {Token} right - The token which is next to `left`.
* @returns {boolean}
* When the option is `"always"`, `true` if there are one or more spaces between given tokens.
* When the option is `"never"`, `true` if there are not any spaces between given tokens.
* If given tokens are not on same line, it's always `true`.
*/
function isValid(left, right) {
return (
!util.isTokenOnSameLine(left, right) ||
sourceCode.isSpaceBetweenTokens(left, right) === always
);
}
/**
* Reports invalid spacing style inside braces.
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
* @returns {void}
*/
function checkSpacingInsideBraces(node) {
// Gets braces and the first/last token of content.
const openBrace = getOpenBrace(node);
const closeBrace = sourceCode.getLastToken(node);
const firstToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
const lastToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
// Skip if the node is invalid or empty.
if (openBrace.type !== "Punctuator" ||
openBrace.value !== "{" ||
closeBrace.type !== "Punctuator" ||
closeBrace.value !== "}" ||
firstToken === closeBrace
) {
return;
}
// Skip line comments for option never
if (!always && firstToken.type === "Line") {
return;
}
// Check.
if (!isValid(openBrace, firstToken)) {
context.report({
node,
loc: openBrace.loc.start,
message: "{{message}} after '{'.",
data: {
message
},
fix(fixer) {
if (always) {
return fixer.insertTextBefore(firstToken, " ");
}
return fixer.removeRange([openBrace.range[1], firstToken.range[0]]);
}
});
}
if (!isValid(lastToken, closeBrace)) {
context.report({
node,
loc: closeBrace.loc.start,
message: "{{message}} before '}'.",
data: {
message
},
fix(fixer) {
if (always) {
return fixer.insertTextAfter(lastToken, " ");
}
return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]);
}
});
}
}
return {
BlockStatement: checkSpacingInsideBraces,
SwitchStatement: checkSpacingInsideBraces
};
}
};

View File

@@ -0,0 +1,182 @@
/**
* @fileoverview Rule to flag block statements that do not use the one true brace style
* @author Ian Christian Myers
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent brace style for blocks",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["1tbs", "stroustrup", "allman"]
},
{
type: "object",
properties: {
allowSingleLine: {
type: "boolean"
}
},
additionalProperties: false
}
],
fixable: "whitespace"
},
create(context) {
const style = context.options[0] || "1tbs",
params = context.options[1] || {},
sourceCode = context.getSourceCode();
const 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.",
BODY_MESSAGE = "Statement inside of curly braces should be on next line.",
CLOSE_MESSAGE = "Closing curly brace does not appear on the same line as the subsequent block.",
CLOSE_MESSAGE_SINGLE = "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
CLOSE_MESSAGE_STROUSTRUP_ALLMAN = "Closing curly brace appears on the same line as the subsequent block.";
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Fixes a place where a newline unexpectedly appears
* @param {Token} firstToken The token before the unexpected newline
* @param {Token} secondToken The token after the unexpected newline
* @returns {Function} A fixer function to remove the newlines between the tokens
*/
function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]];
const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
// Don't do a fix if there is a comment between the tokens
if (textBetween.trim()) {
return null;
}
return fixer => fixer.replaceTextRange(textRange, " ");
}
/**
* Validates a pair of curly brackets based on the user's config
* @param {Token} openingCurly The opening curly bracket
* @param {Token} closingCurly The closing curly bracket
* @returns {void}
*/
function validateCurlyPair(openingCurly, closingCurly) {
const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly);
const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly);
const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly);
const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly);
if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) {
context.report({
node: openingCurly,
message: OPEN_MESSAGE,
fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
});
}
if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
context.report({
node: openingCurly,
message: OPEN_MESSAGE_ALLMAN,
fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
});
}
if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
context.report({
node: openingCurly,
message: BODY_MESSAGE,
fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
});
}
if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
context.report({
node: closingCurly,
message: CLOSE_MESSAGE_SINGLE,
fix: fixer => fixer.insertTextBefore(closingCurly, "\n")
});
}
}
/**
* Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
* @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
* @returns {void}
*/
function validateCurlyBeforeKeyword(curlyToken) {
const keywordToken = sourceCode.getTokenAfter(curlyToken);
if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
message: CLOSE_MESSAGE,
fix: removeNewlineBetween(curlyToken, keywordToken)
});
}
if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
message: CLOSE_MESSAGE_STROUSTRUP_ALLMAN,
fix: fixer => fixer.insertTextAfter(curlyToken, "\n")
});
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
BlockStatement(node) {
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
}
},
ClassBody(node) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
},
SwitchStatement(node) {
const closingCurly = sourceCode.getLastToken(node);
const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly);
validateCurlyPair(openingCurly, closingCurly);
},
IfStatement(node) {
if (node.consequent.type === "BlockStatement" && node.alternate) {
// Handle the keyword after the `if` block (before `else`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent));
}
},
TryStatement(node) {
// Handle the keyword after the `try` block (before `catch` or `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
if (node.handler && node.finalizer) {
// Handle the keyword after the `catch` block (before `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body));
}
}
};
}
};

View File

@@ -0,0 +1,175 @@
/**
* @fileoverview Enforce return after a callback.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `return` statements after callbacks",
category: "Node.js and CommonJS",
recommended: false
},
schema: [{
type: "array",
items: { type: "string" }
}]
},
create(context) {
const callbacks = context.options[0] || ["callback", "cb", "next"],
sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null;
}
if (types.indexOf(node.parent.type) === -1) {
return findClosestParentOfType(node.parent, types);
}
return node.parent;
}
/**
* Check to see if a node contains only identifers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true;
}
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true;
}
if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object);
}
}
return false;
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1;
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @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;
}
// cb()
if (parentNode.expression === node) {
return true;
}
// special case for cb && cb() and similar
if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
if (parentNode.expression.right === node) {
return true;
}
}
return false;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
CallExpression(node) {
// if we're not a callback we can return
if (!isCallback(node)) {
return;
}
// find the closest block, return or loop
const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {};
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return;
}
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return;
}
// block statements are part of functions and most if statements
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem = closestBlock.body[closestBlock.body.length - 1];
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type;
// but only if the block is part of a function
if (parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return;
}
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
return;
}
}
}
// as long as you're the child of a function at this point you should be asked to return
if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
context.report({ node, message: "Expected return with your callback function." });
}
}
};
}
};

View File

@@ -0,0 +1,143 @@
/**
* @fileoverview Rule to flag non-camelcased identifiers
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce camelcase naming convention",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
properties: {
enum: ["always", "never"]
}
},
additionalProperties: false
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
const reported = [];
const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
/**
* Checks if a string contains an underscore and isn't all upper-case
* @param {string} name The string to check.
* @returns {boolean} if the string is underscored
* @private
*/
function isUnderscored(name) {
// if there's an underscore, it might be A_CONSTANT, which is okay
return name.indexOf("_") > -1 && name !== name.toUpperCase();
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
if (reported.indexOf(node) < 0) {
reported.push(node);
context.report({ node, message: "Identifier '{{name}}' is not in camel case.", data: { name: node.name } });
}
}
const options = context.options[0] || {};
let properties = options.properties || "";
if (properties !== "always" && properties !== "never") {
properties = "always";
}
return {
Identifier(node) {
/*
* Leading and trailing underscores are commonly used to flag
* private/protected identifiers, strip them
*/
const name = node.name.replace(/^_+|_+$/g, ""),
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
// MemberExpressions get special rules
if (node.parent.type === "MemberExpression") {
// "never" check properties
if (properties === "never") {
return;
}
// Always report underscored object names
if (node.parent.object.type === "Identifier" &&
node.parent.object.name === node.name &&
isUnderscored(name)) {
report(node);
// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" &&
isUnderscored(name) &&
(effectiveParent.right.type !== "MemberExpression" ||
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === node.name)) {
report(node);
}
// 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) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
report(node);
}
// Check if it's an import specifier
} else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].indexOf(node.parent.type) >= 0) {
// Report only if the local imported identifier is underscored
if (node.parent.local && node.parent.local.name === node.name && isUnderscored(name)) {
report(node);
}
// Report anything that is underscored that isn't a CallExpression
} else if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
report(node);
}
}
};
}
};

View File

@@ -0,0 +1,303 @@
/**
* @fileoverview enforce or disallow capitalization of the first letter of a comment
* @author Kevin Partington
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("../util/patterns/letters");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const ALWAYS_MESSAGE = "Comments should not begin with a lowercase character",
NEVER_MESSAGE = "Comments should not begin with an uppercase character",
DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/g,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
DEFAULTS = {
ignorePattern: null,
ignoreInlineComments: false,
ignoreConsecutiveComments: false
};
/*
* Base schema body for defining the basic capitalization rule, ignorePattern,
* and ignoreInlineComments values.
* This can be used in a few different ways in the actual schema.
*/
const SCHEMA_BODY = {
type: "object",
properties: {
ignorePattern: {
type: "string"
},
ignoreInlineComments: {
type: "boolean"
},
ignoreConsecutiveComments: {
type: "boolean"
}
},
additionalProperties: false
};
/**
* Get normalized options for either block or line comments from the given
* user-provided options.
* - If the user-provided options is just a string, returns a normalized
* set of options using default values for all other options.
* - If the user-provided options is an object, then a normalized option
* set is returned. Options specified in overrides will take priority
* over options specified in the main options object, which will in
* turn take priority over the rule's defaults.
*
* @param {Object|string} rawOptions The user-provided options.
* @param {string} which Either "line" or "block".
* @returns {Object} The normalized options.
*/
function getNormalizedOptions(rawOptions, which) {
if (!rawOptions) {
return Object.assign({}, DEFAULTS);
}
return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
}
/**
* Get normalized options for block and line comments.
*
* @param {Object|string} rawOptions The user-provided options.
* @returns {Object} An object with "Line" and "Block" keys and corresponding
* normalized options objects.
*/
function getAllNormalizedOptions(rawOptions) {
return {
Line: getNormalizedOptions(rawOptions, "line"),
Block: getNormalizedOptions(rawOptions, "block")
};
}
/**
* Creates a regular expression for each ignorePattern defined in the rule
* options.
*
* This is done in order to avoid invoking the RegExp constructor repeatedly.
*
* @param {Object} normalizedOptions The normalized rule options.
* @returns {void}
*/
function createRegExpForIgnorePatterns(normalizedOptions) {
Object.keys(normalizedOptions).forEach(key => {
const ignorePatternStr = normalizedOptions[key].ignorePattern;
if (ignorePatternStr) {
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`);
normalizedOptions[key].ignorePatternRegExp = regExp;
}
});
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce or disallow capitalization of the first letter of a comment",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: [
{ enum: ["always", "never"] },
{
oneOf: [
SCHEMA_BODY,
{
type: "object",
properties: {
line: SCHEMA_BODY,
block: SCHEMA_BODY
},
additionalProperties: false
}
]
}
]
},
create(context) {
const capitalize = context.options[0] || "always",
normalizedOptions = getAllNormalizedOptions(context.options[1]),
sourceCode = context.getSourceCode();
createRegExpForIgnorePatterns(normalizedOptions);
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Checks whether a comment is an inline comment.
*
* For the purpose of this rule, a comment is inline if:
* 1. The comment is preceded by a token on the same line; and
* 2. The command is followed by a token on the same line.
*
* Note that the comment itself need not be single-line!
*
* Also, it follows from this definition that only block comments can
* be considered as possibly inline. This is because line comments
* would consume any following tokens on the same line as the comment.
*
* @param {ASTNode} comment The comment node to check.
* @returns {boolean} True if the comment is an inline comment, false
* otherwise.
*/
function isInlineComment(comment) {
const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
return Boolean(
previousToken &&
nextToken &&
comment.loc.start.line === previousToken.loc.end.line &&
comment.loc.end.line === nextToken.loc.start.line
);
}
/**
* Determine if a comment follows another comment.
*
* @param {ASTNode} comment The comment to check.
* @returns {boolean} True if the comment follows a valid comment.
*/
function isConsecutiveComment(comment) {
const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
return Boolean(
previousTokenOrComment &&
["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
);
}
/**
* Check a comment to determine if it is valid for this rule.
*
* @param {ASTNode} comment The comment node to process.
* @param {Object} options The options for checking this comment.
* @returns {boolean} True if the comment is valid, false otherwise.
*/
function isCommentValid(comment, options) {
// 1. Check for default ignore pattern.
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
return true;
}
// 2. Check for custom ignore pattern.
const commentWithoutAsterisks = comment.value
.replace(/\*/g, "");
if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
return true;
}
// 3. Check for inline comments.
if (options.ignoreInlineComments && isInlineComment(comment)) {
return true;
}
// 4. Is this a consecutive comment (and are we tolerating those)?
if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
return true;
}
// 5. Does the comment start with a possible URL?
if (MAYBE_URL.test(commentWithoutAsterisks)) {
return true;
}
// 6. Is the initial word character a letter?
const commentWordCharsOnly = commentWithoutAsterisks
.replace(WHITESPACE, "");
if (commentWordCharsOnly.length === 0) {
return true;
}
const firstWordChar = commentWordCharsOnly[0];
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
}
// 7. Check the case of the initial word character.
const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
if (capitalize === "always" && isLowercase) {
return false;
}
if (capitalize === "never" && isUppercase) {
return false;
}
return true;
}
/**
* Process a comment to determine if it needs to be reported.
*
* @param {ASTNode} comment The comment node to process.
* @returns {void}
*/
function processComment(comment) {
const options = normalizedOptions[comment.type],
commentValid = isCommentValid(comment, options);
if (!commentValid) {
const message = capitalize === "always"
? ALWAYS_MESSAGE
: NEVER_MESSAGE;
context.report({
node: null, // Intentionally using loc instead
loc: comment.loc,
message,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
return fixer.replaceTextRange(
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
);
}
});
}
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
Program() {
const comments = sourceCode.getAllComments();
comments.filter(token => token.type !== "Shebang").forEach(processComment);
}
};
}
};

View File

@@ -0,0 +1,110 @@
/**
* @fileoverview Rule to enforce that all class methods use 'this'.
* @author Patrick Williams
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce that class methods utilize `this`",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
exceptMethods: {
type: "array",
items: {
type: "string"
}
}
},
additionalProperties: false
}]
},
create(context) {
const config = context.options[0] ? Object.assign({}, context.options[0]) : {};
const exceptMethods = new Set(config.exceptMethods || []);
const stack = [];
/**
* Initializes the current context to false and pushes it onto the stack.
* These booleans represent whether 'this' has been used in the context.
* @returns {void}
* @private
*/
function enterFunction() {
stack.push(false);
}
/**
* Check if the node is an instance method
* @param {ASTNode} node - node to check
* @returns {boolean} True if its an instance method
* @private
*/
function isInstanceMethod(node) {
return !node.static && node.kind !== "constructor" && node.type === "MethodDefinition";
}
/**
* Check if the node is an instance method not excluded by config
* @param {ASTNode} node - node to check
* @returns {boolean} True if it is an instance method, and not excluded by config
* @private
*/
function isIncludedInstanceMethod(node) {
return isInstanceMethod(node) && !exceptMethods.has(node.key.name);
}
/**
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
* Static methods and the constructor are exempt.
* Then pops the context off the stack.
* @param {ASTNode} node - A function node that was entered.
* @returns {void}
* @private
*/
function exitFunction(node) {
const methodUsesThis = stack.pop();
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
context.report({
node,
message: "Expected 'this' to be used by class method '{{classMethod}}'.",
data: {
classMethod: node.parent.key.name
}
});
}
}
/**
* Mark the current context as having used 'this'.
* @returns {void}
* @private
*/
function markThisUsed() {
if (stack.length) {
stack[stack.length - 1] = true;
}
}
return {
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
ThisExpression: markThisUsed,
Super: markThisUsed
};
}
};

View File

@@ -0,0 +1,337 @@
/**
* @fileoverview Rule to forbid or enforce dangling commas.
* @author Ian Christian Myers
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_OPTIONS = Object.freeze({
arrays: "never",
objects: "never",
imports: "never",
exports: "never",
functions: "ignore"
});
/**
* Checks whether or not a trailing comma is allowed in a given node.
* If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
*
* @param {ASTNode} lastItem - The node of the last element in the given node.
* @returns {boolean} `true` if a trailing comma is allowed.
*/
function isTrailingCommaAllowed(lastItem) {
return !(
lastItem.type === "RestElement" ||
lastItem.type === "RestProperty" ||
lastItem.type === "ExperimentalRestProperty"
);
}
/**
* Normalize option value.
*
* @param {string|Object|undefined} optionValue - The 1st option value to normalize.
* @returns {Object} The normalized option value.
*/
function normalizeOptions(optionValue) {
if (typeof optionValue === "string") {
return {
arrays: optionValue,
objects: optionValue,
imports: optionValue,
exports: optionValue,
// For backward compatibility, always ignore functions.
functions: "ignore"
};
}
if (typeof optionValue === "object" && optionValue !== null) {
return {
arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
objects: optionValue.objects || DEFAULT_OPTIONS.objects,
imports: optionValue.imports || DEFAULT_OPTIONS.imports,
exports: optionValue.exports || DEFAULT_OPTIONS.exports,
functions: optionValue.functions || DEFAULT_OPTIONS.functions
};
}
return DEFAULT_OPTIONS;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow trailing commas",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: {
definitions: {
value: {
enum: [
"always-multiline",
"always",
"never",
"only-multiline"
]
},
valueWithIgnore: {
enum: [
"always-multiline",
"always",
"ignore",
"never",
"only-multiline"
]
}
},
type: "array",
items: [
{
oneOf: [
{
$ref: "#/definitions/value"
},
{
type: "object",
properties: {
arrays: { $ref: "#/definitions/valueWithIgnore" },
objects: { $ref: "#/definitions/valueWithIgnore" },
imports: { $ref: "#/definitions/valueWithIgnore" },
exports: { $ref: "#/definitions/valueWithIgnore" },
functions: { $ref: "#/definitions/valueWithIgnore" }
},
additionalProperties: false
}
]
}
]
}
},
create(context) {
const options = normalizeOptions(context.options[0]);
const sourceCode = context.getSourceCode();
const UNEXPECTED_MESSAGE = "Unexpected trailing comma.";
const MISSING_MESSAGE = "Missing trailing comma.";
/**
* Gets the last item of the given node.
* @param {ASTNode} node - The node to get.
* @returns {ASTNode|null} The last node or null.
*/
function getLastItem(node) {
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
return lodash.last(node.properties);
case "ArrayExpression":
case "ArrayPattern":
return lodash.last(node.elements);
case "ImportDeclaration":
case "ExportNamedDeclaration":
return lodash.last(node.specifiers);
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
return lodash.last(node.params);
case "CallExpression":
case "NewExpression":
return lodash.last(node.arguments);
default:
return null;
}
}
/**
* Gets the trailing comma token of the given node.
* If the trailing comma does not exist, this returns the token which is
* the insertion point of the trailing comma token.
*
* @param {ASTNode} node - The node to get.
* @param {ASTNode} lastItem - The last item of the node.
* @returns {Token} The trailing comma token or the insertion point.
*/
function getTrailingToken(node, lastItem) {
switch (node.type) {
case "ObjectExpression":
case "ArrayExpression":
case "CallExpression":
case "NewExpression":
return sourceCode.getLastToken(node, 1);
default: {
const nextToken = sourceCode.getTokenAfter(lastItem);
if (astUtils.isCommaToken(nextToken)) {
return nextToken;
}
return sourceCode.getLastToken(lastItem);
}
}
}
/**
* Checks whether or not a given node is multiline.
* 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 node to check.
* @returns {boolean} `true` if the node is multiline.
*/
function isMultiline(node) {
const lastItem = getLastItem(node);
if (!lastItem) {
return false;
}
const penultimateToken = getTrailingToken(node, lastItem);
const lastToken = sourceCode.getTokenAfter(penultimateToken);
return lastToken.loc.end.line !== penultimateToken.loc.end.line;
}
/**
* 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 forbidTrailingComma(node) {
const lastItem = getLastItem(node);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
const trailingToken = getTrailingToken(node, lastItem);
if (astUtils.isCommaToken(trailingToken)) {
context.report({
node: lastItem,
loc: trailingToken.loc.start,
message: UNEXPECTED_MESSAGE,
fix(fixer) {
return fixer.remove(trailingToken);
}
});
}
}
/**
* Reports the last element of a given node if it does not have a trailing
* comma.
*
* If a given node is `ArrayPattern` which has `RestElement`, the trailing
* comma is disallowed, so report 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 forceTrailingComma(node) {
const lastItem = getLastItem(node);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
if (!isTrailingCommaAllowed(lastItem)) {
forbidTrailingComma(node);
return;
}
const trailingToken = getTrailingToken(node, lastItem);
if (trailingToken.value !== ",") {
context.report({
node: lastItem,
loc: trailingToken.loc.end,
message: MISSING_MESSAGE,
fix(fixer) {
return fixer.insertTextAfter(trailingToken, ",");
}
});
}
}
/**
* If a given node is 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 forceTrailingCommaIfMultiline(node) {
if (isMultiline(node)) {
forceTrailingComma(node);
} else {
forbidTrailingComma(node);
}
}
/**
* 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);
}
}
const predicate = {
always: forceTrailingComma,
"always-multiline": forceTrailingCommaIfMultiline,
"only-multiline": allowTrailingCommaIfMultiline,
never: forbidTrailingComma,
ignore: lodash.noop
};
return {
ObjectExpression: predicate[options.objects],
ObjectPattern: predicate[options.objects],
ArrayExpression: predicate[options.arrays],
ArrayPattern: predicate[options.arrays],
ImportDeclaration: predicate[options.imports],
ExportNamedDeclaration: predicate[options.exports],
FunctionDeclaration: predicate[options.functions],
FunctionExpression: predicate[options.functions],
ArrowFunctionExpression: predicate[options.functions],
CallExpression: predicate[options.functions],
NewExpression: predicate[options.functions]
};
}
};

View File

@@ -0,0 +1,183 @@
/**
* @fileoverview Comma spacing - validates spacing before and after comma
* @author Vignesh Anand aka vegetableman.
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing before and after commas",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: {
type: "boolean"
},
after: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
const tokensAndComments = sourceCode.tokensAndComments;
const options = {
before: context.options[0] ? !!context.options[0].before : false,
after: context.options[0] ? !!context.options[0].after : true
};
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// list of comma tokens to ignore for the check of leading whitespace
const commaTokensToIgnore = [];
/**
* Reports a spacing error with an appropriate message.
* @param {ASTNode} node The binary expression node to report.
* @param {string} dir Is the error "before" or "after" the comma?
* @param {ASTNode} otherNode The node at the left or right of `node`
* @returns {void}
* @private
*/
function report(node, dir, otherNode) {
context.report({
node,
fix(fixer) {
if (options[dir]) {
if (dir === "before") {
return fixer.insertTextBefore(node, " ");
}
return fixer.insertTextAfter(node, " ");
}
let start, end;
const newText = "";
if (dir === "before") {
start = otherNode.range[1];
end = node.range[0];
} else {
start = node.range[1];
end = otherNode.range[0];
}
return fixer.replaceTextRange([start, end], newText);
},
message: options[dir]
? "A space is required {{dir}} ','."
: "There should be no space {{dir}} ','.",
data: {
dir
}
});
}
/**
* Validates the spacing around a comma token.
* @param {Object} tokens - The tokens to be validated.
* @param {Token} tokens.comma The token representing the comma.
* @param {Token} [tokens.left] The last token before the comma.
* @param {Token} [tokens.right] The first token after the comma.
* @param {Token|ASTNode} reportItem The item to use when reporting an error.
* @returns {void}
* @private
*/
function validateCommaItemSpacing(tokens, reportItem) {
if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) &&
(options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma))
) {
report(reportItem, "before", tokens.left);
}
if (tokens.right && !options.after && tokens.right.type === "Line") {
return;
}
if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&
(options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right))
) {
report(reportItem, "after", tokens.right);
}
}
/**
* Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list.
* @param {ASTNode} node An ArrayExpression or ArrayPattern node.
* @returns {void}
*/
function addNullElementsToIgnoreList(node) {
let previousToken = sourceCode.getFirstToken(node);
node.elements.forEach(element => {
let token;
if (element === null) {
token = sourceCode.getTokenAfter(previousToken);
if (astUtils.isCommaToken(token)) {
commaTokensToIgnore.push(token);
}
} else {
token = sourceCode.getTokenAfter(element);
}
previousToken = token;
});
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
"Program:exit"() {
tokensAndComments.forEach((token, i) => {
if (!astUtils.isCommaToken(token)) {
return;
}
if (token && token.type === "JSXText") {
return;
}
const previousToken = tokensAndComments[i - 1];
const nextToken = tokensAndComments[i + 1];
validateCommaItemSpacing({
comma: token,
left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
right: astUtils.isCommaToken(nextToken) ? null : nextToken
}, token);
});
},
ArrayExpression: addNullElementsToIgnoreList,
ArrayPattern: addNullElementsToIgnoreList
};
}
};

View File

@@ -0,0 +1,299 @@
/**
* @fileoverview Comma style - enforces comma styles of two types: last and first
* @author Vignesh Anand aka vegetableman
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent comma style",
category: "Stylistic Issues",
recommended: false
},
fixable: "code",
schema: [
{
enum: ["first", "last"]
},
{
type: "object",
properties: {
exceptions: {
type: "object",
additionalProperties: {
type: "boolean"
}
}
},
additionalProperties: false
}
]
},
create(context) {
const style = context.options[0] || "last",
sourceCode = context.getSourceCode();
const exceptions = {
ArrayPattern: true,
ArrowFunctionExpression: true,
CallExpression: true,
FunctionDeclaration: true,
FunctionExpression: true,
ImportDeclaration: true,
ObjectPattern: true
};
if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) {
const keys = Object.keys(context.options[1].exceptions);
for (let i = 0; i < keys.length; i++) {
exceptions[keys[i]] = context.options[1].exceptions[keys[i]];
}
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Modified text based on the style
* @param {string} styleType Style type
* @param {string} text Source code text
* @returns {string} modified text
* @private
*/
function getReplacedText(styleType, text) {
switch (styleType) {
case "between":
return `,${text.replace("\n", "")}`;
case "first":
return `${text},`;
case "last":
return `,${text}`;
default:
return "";
}
}
/**
* Determines the fixer function for a given style.
* @param {string} styleType comma style
* @param {ASTNode} previousItemToken The token to check.
* @param {ASTNode} commaToken The token to check.
* @param {ASTNode} currentItemToken The token to check.
* @returns {Function} Fixer function
* @private
*/
function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) {
const text =
sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) +
sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]);
const range = [previousItemToken.range[1], currentItemToken.range[0]];
return function(fixer) {
return fixer.replaceTextRange(range, getReplacedText(styleType, text));
};
}
/**
* Validates the spacing around single items in lists.
* @param {Token} previousItemToken The last token from the previous item.
* @param {Token} commaToken The token representing the comma.
* @param {Token} currentItemToken The first token of the current item.
* @param {Token} reportItem The item to use when reporting an error.
* @returns {void}
* @private
*/
function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
// if single line
if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
// do nothing.
} else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
!astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
// lone comma
context.report({
node: reportItem,
loc: {
line: commaToken.loc.end.line,
column: commaToken.loc.start.column
},
message: "Bad line breaking before and after ','.",
fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken)
});
} else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({
node: reportItem,
message: "',' should be placed first.",
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
});
} else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({
node: reportItem,
loc: {
line: commaToken.loc.end.line,
column: commaToken.loc.end.column
},
message: "',' should be placed last.",
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
});
}
}
/**
* Checks the comma placement with regards to a declaration/property/element
* @param {ASTNode} node The binary expression node to check
* @param {string} property The property of the node containing child nodes.
* @private
* @returns {void}
*/
function validateComma(node, property) {
const items = node[property],
arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern");
if (items.length > 1 || arrayLiteral) {
// seed as opening [
let previousItemToken = sourceCode.getFirstToken(node);
items.forEach(item => {
const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken,
currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken),
reportItem = item || currentItemToken,
tokenBeforeComma = sourceCode.getTokenBefore(commaToken);
// Check if previous token is wrapped in parentheses
if (tokenBeforeComma && astUtils.isClosingParenToken(tokenBeforeComma)) {
previousItemToken = tokenBeforeComma;
}
/*
* This works by comparing three token locations:
* - previousItemToken is the last token of the previous item
* - commaToken is the location of the comma before the current item
* - currentItemToken is the first token of the current item
*
* These values get switched around if item is undefined.
* previousItemToken will refer to the last token not belonging
* to the current item, which could be a comma or an opening
* square bracket. currentItemToken could be a comma.
*
* All comparisons are done based on these tokens directly, so
* they are always valid regardless of an undefined item.
*/
if (astUtils.isCommaToken(commaToken)) {
validateCommaItemSpacing(previousItemToken, commaToken,
currentItemToken, reportItem);
}
if (item) {
const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
previousItemToken = tokenAfterItem
? sourceCode.getTokenBefore(tokenAfterItem)
: sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
}
});
/*
* Special case for array literals that have empty last items, such
* as [ 1, 2, ]. These arrays only have two items show up in the
* AST, so we need to look at the token to verify that there's no
* dangling comma.
*/
if (arrayLiteral) {
const lastToken = sourceCode.getLastToken(node),
nextToLastToken = sourceCode.getTokenBefore(lastToken);
if (astUtils.isCommaToken(nextToLastToken)) {
validateCommaItemSpacing(
sourceCode.getTokenBefore(nextToLastToken),
nextToLastToken,
lastToken,
lastToken
);
}
}
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
const nodes = {};
if (!exceptions.VariableDeclaration) {
nodes.VariableDeclaration = function(node) {
validateComma(node, "declarations");
};
}
if (!exceptions.ObjectExpression) {
nodes.ObjectExpression = function(node) {
validateComma(node, "properties");
};
}
if (!exceptions.ObjectPattern) {
nodes.ObjectPattern = function(node) {
validateComma(node, "properties");
};
}
if (!exceptions.ArrayExpression) {
nodes.ArrayExpression = function(node) {
validateComma(node, "elements");
};
}
if (!exceptions.ArrayPattern) {
nodes.ArrayPattern = function(node) {
validateComma(node, "elements");
};
}
if (!exceptions.FunctionDeclaration) {
nodes.FunctionDeclaration = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.FunctionExpression) {
nodes.FunctionExpression = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.ArrowFunctionExpression) {
nodes.ArrowFunctionExpression = function(node) {
validateComma(node, "params");
};
}
if (!exceptions.CallExpression) {
nodes.CallExpression = function(node) {
validateComma(node, "arguments");
};
}
if (!exceptions.ImportDeclaration) {
nodes.ImportDeclaration = function(node) {
validateComma(node, "specifiers");
};
}
return nodes;
}
};

View File

@@ -0,0 +1,168 @@
/**
* @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity.
* Counts the number of if, conditional, for, whilte, try, switch/case,
* @author Patrick Brosset
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce a maximum cyclomatic complexity allowed in a program",
category: "Best Practices",
recommended: false
},
schema: [
{
oneOf: [
{
type: "integer",
minimum: 0
},
{
type: "object",
properties: {
maximum: {
type: "integer",
minimum: 0
},
max: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const option = context.options[0];
let THRESHOLD = 20;
if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") {
THRESHOLD = option.maximum;
}
if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") {
THRESHOLD = option.max;
}
if (typeof option === "number") {
THRESHOLD = option;
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// Using a stack to store complexity (handling nested functions)
const fns = [];
/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
fns.push(1);
}
/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function endFunction(node) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
const complexity = fns.pop();
if (complexity > THRESHOLD) {
context.report({
node,
message: "{{name}} has a complexity of {{complexity}}.",
data: { name, complexity }
});
}
}
/**
* Increase the complexity of the function in context
* @returns {void}
* @private
*/
function increaseComplexity() {
if (fns.length) {
fns[fns.length - 1]++;
}
}
/**
* Increase the switch complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseSwitchComplexity(node) {
// Avoiding `default`
if (node.test) {
increaseComplexity();
}
}
/**
* Increase the logical path complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseLogicalComplexity(node) {
// Avoiding &&
if (node.operator === "||") {
increaseComplexity();
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,
CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseLogicalComplexity,
ForStatement: increaseComplexity,
ForInStatement: increaseComplexity,
ForOfStatement: increaseComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity
};
}
};

View File

@@ -0,0 +1,176 @@
/**
* @fileoverview Disallows or enforces spaces inside computed properties.
* @author Jamund Ferguson
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing inside computed property brackets",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never"]
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never"
//--------------------------------------------------------------------------
// 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.
* @param {Token} tokenAfter - The token after `token`.
* @returns {void}
*/
function reportNoBeginningSpace(node, token, tokenAfter) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([token.range[1], tokenAfter.range[0]]);
}
});
}
/**
* 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.
* @param {Token} tokenBefore - The token before `token`.
* @returns {void}
*/
function reportNoEndingSpace(node, token, tokenBefore) {
context.report({
node,
loc: token.loc.start,
message: "There should be no space before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([tokenBefore.range[1], token.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,
loc: token.loc.start,
message: "A space is required after '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* 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,
loc: token.loc.start,
message: "A space is required before '{{tokenValue}}'.",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/**
* Returns a function that checks the spacing of a node on the property name
* that was passed in.
* @param {string} propertyName The property on the node to check for spacing
* @returns {Function} A function that will check spacing on a node
*/
function checkSpacing(propertyName) {
return function(node) {
if (!node.computed) {
return;
}
const property = node[propertyName];
const before = sourceCode.getTokenBefore(property),
first = sourceCode.getFirstToken(property),
last = sourceCode.getLastToken(property),
after = sourceCode.getTokenAfter(property);
if (astUtils.isTokenOnSameLine(before, first)) {
if (propertyNameMustBeSpaced) {
if (!sourceCode.isSpaceBetweenTokens(before, first) && astUtils.isTokenOnSameLine(before, first)) {
reportRequiredBeginningSpace(node, before);
}
} else {
if (sourceCode.isSpaceBetweenTokens(before, first)) {
reportNoBeginningSpace(node, before, first);
}
}
}
if (astUtils.isTokenOnSameLine(last, after)) {
if (propertyNameMustBeSpaced) {
if (!sourceCode.isSpaceBetweenTokens(last, after) && astUtils.isTokenOnSameLine(last, after)) {
reportRequiredEndingSpace(node, after);
}
} else {
if (sourceCode.isSpaceBetweenTokens(last, after)) {
reportNoEndingSpace(node, after, last);
}
}
}
};
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Property: checkSpacing("key"),
MemberExpression: checkSpacing("property")
};
}
};

View File

@@ -0,0 +1,188 @@
/**
* @fileoverview Rule to flag consistent return values
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node - A node to check.
* @param {string} name - An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* 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;
}
/**
* Checks whether a given node is a `constructor` method in an ES6 class
* @param {ASTNode} node A node to check
* @returns {boolean} `true` if the node is a `constructor` method
*/
function isClassConstructor(node) {
return node.type === "FunctionExpression" &&
node.parent &&
node.parent.type === "MethodDefinition" &&
node.parent.kind === "constructor";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `return` statements to either always or never specify values",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
treatUndefinedAsUnspecified: {
type: "boolean"
}
},
additionalProperties: false
}]
},
create(context) {
const options = context.options[0] || {};
const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true;
let funcInfo = null;
/**
* 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}
*/
function checkLastSegment(node) {
let loc, name;
/*
* 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) ||
astUtils.isES5Constructor(node) ||
isClassConstructor(node)
) {
return;
}
// Adjust a location and a message.
if (node.type === "Program") {
// The head of program.
loc = { line: 1, column: 0 };
name = "program";
} else if (node.type === "ArrowFunctionExpression") {
// `=>` token
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start;
} else if (
node.parent.type === "MethodDefinition" ||
(node.parent.type === "Property" && node.parent.method)
) {
// Method name.
loc = node.parent.key.loc.start;
} else {
// Function name or `function` keyword.
loc = (node.id || node).loc.start;
}
if (!name) {
name = astUtils.getFunctionNameWithKind(node);
}
// Reports.
context.report({
node,
loc,
message: "Expected to return a value at the end of {{name}}.",
data: { name }
});
}
return {
// Initializes/Disposes state of each code path.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
hasReturnValue: false,
message: "",
node
};
},
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Reports a given return statement if it's inconsistent.
ReturnStatement(node) {
const argument = node.argument;
let hasReturnValue = Boolean(argument);
if (treatUndefinedAsUnspecified && hasReturnValue) {
hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void";
}
if (!funcInfo.hasReturn) {
funcInfo.hasReturn = true;
funcInfo.hasReturnValue = hasReturnValue;
funcInfo.message = "{{name}} expected {{which}} return value.";
funcInfo.data = {
name: funcInfo.node.type === "Program"
? "Program"
: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)),
which: hasReturnValue ? "a" : "no"
};
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
context.report({
node,
message: funcInfo.message,
data: funcInfo.data
});
}
},
// Reports a given program/function if the implicit returning is not consistent.
"Program:exit": checkLastSegment,
"FunctionDeclaration:exit": checkLastSegment,
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};

View File

@@ -0,0 +1,141 @@
/**
* @fileoverview Rule to enforce consistent naming of "this" context variables
* @author Raphael Pigulla
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent naming when capturing the current execution context",
category: "Stylistic Issues",
recommended: false
},
schema: {
type: "array",
items: {
type: "string",
minLength: 1
},
uniqueItems: true
}
},
create(context) {
let 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, alias) {
context.report({ node, message: "Designated alias '{{alias}}' is not assigned to 'this'.", data: { alias } });
}
/**
* Checks that an assignment to an identifier only assigns 'this' to the
* appropriate alias, and the alias is only assigned to 'this'.
* @param {ASTNode} node - The assigning node.
* @param {Identifier} name - The name of the variable assigned to.
* @param {Expression} value - The value of the assignment.
* @returns {void}
*/
function checkAssignment(node, name, value) {
const isThis = value.type === "ThisExpression";
if (aliases.indexOf(name) !== -1) {
if (!isThis || node.operator && node.operator !== "=") {
reportBadAssignment(node, name);
}
} else if (isThis) {
context.report({ node, message: "Unexpected alias '{{name}}' for 'this'.", data: { name } });
}
}
/**
* 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 checkWasAssigned(alias, scope) {
const variable = scope.set.get(alias);
if (!variable) {
return;
}
if (variable.defs.some(def => def.node.type === "VariableDeclarator" &&
def.node.init !== null)) {
return;
}
// The alias has been declared and not assigned: check it was
// assigned later in the same scope.
if (!variable.references.some(reference => {
const write = reference.writeExpr;
return (
reference.from === scope &&
write && write.type === "ThisExpression" &&
write.parent.operator === "="
);
})) {
variable.defs.map(def => def.node).forEach(node => {
reportBadAssignment(node, alias);
});
}
}
/**
* Check each alias to ensure that is was assinged to the correct value.
* @returns {void}
*/
function ensureWasAssigned() {
const scope = context.getScope();
aliases.forEach(alias => {
checkWasAssigned(alias, scope);
});
}
return {
"Program:exit": ensureWasAssigned,
"FunctionExpression:exit": ensureWasAssigned,
"FunctionDeclaration:exit": ensureWasAssigned,
VariableDeclarator(node) {
const id = node.id;
const isDestructuring =
id.type === "ArrayPattern" || id.type === "ObjectPattern";
if (node.init !== null && !isDestructuring) {
checkAssignment(node, id.name, node.init);
}
},
AssignmentExpression(node) {
if (node.left.type === "Identifier") {
checkAssignment(node, node.left.name, node.right);
}
}
};
}
};

View File

@@ -0,0 +1,385 @@
/**
* @fileoverview A rule to verify `super()` callings in constructor.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether a given code path segment is reachable or not.
*
* @param {CodePathSegment} segment - A code path segment to check.
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
}
/**
* 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"
);
}
/**
* Checks whether a given node can be a constructor or not.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node can be a constructor.
*/
function isPossibleConstructor(node) {
if (!node) {
return false;
}
switch (node.type) {
case "ClassExpression":
case "FunctionExpression":
case "ThisExpression":
case "MemberExpression":
case "CallExpression":
case "NewExpression":
case "YieldExpression":
case "TaggedTemplateExpression":
case "MetaProperty":
return true;
case "Identifier":
return node.name !== "undefined";
case "AssignmentExpression":
return isPossibleConstructor(node.right);
case "LogicalExpression":
return (
isPossibleConstructor(node.left) ||
isPossibleConstructor(node.right)
);
case "ConditionalExpression":
return (
isPossibleConstructor(node.alternate) ||
isPossibleConstructor(node.consequent)
);
case "SequenceExpression": {
const lastExpression = node.expressions[node.expressions.length - 1];
return isPossibleConstructor(lastExpression);
}
default:
return false;
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `super()` calls in constructors",
category: "ECMAScript 6",
recommended: true
},
schema: []
},
create(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.
*/
let funcInfo = 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.
* - validNodes:
*/
let segInfoMap = Object.create(null);
/**
* 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 isCalledInSomePath(segment) {
return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
}
/**
* 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 isCalledInEveryPath(segment) {
/*
* If specific segment is the looped segment of the current segment,
* skip the segment.
* If not skipped, this never becomes true after a loop.
*/
if (segment.nextSegments.length === 1 &&
segment.nextSegments[0].isLoopedPrevSegment(segment)
) {
return true;
}
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
}
return {
/**
* Stacks a constructor information.
* @param {CodePath} codePath - A code path which was started.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathStart(codePath, node) {
if (isConstructorFunction(node)) {
// Class > ClassBody > MethodDefinition > FunctionExpression
const classNode = node.parent.parent.parent;
const superClass = classNode.superClass;
funcInfo = {
upper: funcInfo,
isConstructor: true,
hasExtends: Boolean(superClass),
superIsConstructor: isPossibleConstructor(superClass),
codePath
};
} else {
funcInfo = {
upper: funcInfo,
isConstructor: false,
hasExtends: false,
superIsConstructor: false,
codePath
};
}
},
/**
* 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}
*/
onCodePathEnd(codePath, node) {
const hasExtends = funcInfo.hasExtends;
// Pop.
funcInfo = funcInfo.upper;
if (!hasExtends) {
return;
}
// Reports if `super()` lacked.
const segments = codePath.returnedSegments;
const calledInEveryPaths = segments.every(isCalledInEveryPath);
const 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
});
}
},
/**
* Initialize information of a given code path segment.
* @param {CodePathSegment} segment - A code path segment to initialize.
* @returns {void}
*/
onCodePathSegmentStart(segment) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Initialize info.
const info = segInfoMap[segment.id] = {
calledInSomePaths: false,
calledInEveryPaths: false,
validNodes: []
};
// When there are previous segments, aggregates these.
const prevSegments = segment.prevSegments;
if (prevSegments.length > 0) {
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
}
},
/**
* Update information of the code path segment when a code path was
* looped.
* @param {CodePathSegment} fromSegment - The code path segment of the
* end of a loop.
* @param {CodePathSegment} toSegment - A code path segment of the head
* of a loop.
* @returns {void}
*/
onCodePathSegmentLoop(fromSegment, toSegment) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Update information inside of the loop.
const isRealLoop = toSegment.prevSegments.length >= 2;
funcInfo.codePath.traverseSegments(
{ first: toSegment, last: fromSegment },
segment => {
const info = segInfoMap[segment.id];
const prevSegments = segment.prevSegments;
// Updates flags.
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
// If flags become true anew, reports the valid nodes.
if (info.calledInSomePaths || isRealLoop) {
const nodes = info.validNodes;
info.validNodes = [];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
context.report({
message: "Unexpected duplicate 'super()'.",
node
});
}
}
}
);
},
/**
* Checks for a call of `super()`.
* @param {ASTNode} node - A CallExpression node to check.
* @returns {void}
*/
"CallExpression:exit"(node) {
if (!(funcInfo && funcInfo.isConstructor)) {
return;
}
// Skips except `super()`.
if (node.callee.type !== "Super") {
return;
}
// Reports if needed.
if (funcInfo.hasExtends) {
const segments = funcInfo.codePath.currentSegments;
let duplicate = false;
let info = null;
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
if (segment.reachable) {
info = segInfoMap[segment.id];
duplicate = duplicate || info.calledInSomePaths;
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
if (info) {
if (duplicate) {
context.report({
message: "Unexpected duplicate 'super()'.",
node
});
} else if (!funcInfo.superIsConstructor) {
context.report({
message: "Unexpected 'super()' because 'super' is not a constructor.",
node
});
} else {
info.validNodes.push(node);
}
}
} else if (funcInfo.codePath.currentSegments.some(isReachable)) {
context.report({
message: "Unexpected 'super()'.",
node
});
}
},
/**
* Set the mark to the returned path as `super()` was called.
* @param {ASTNode} node - A ReturnStatement node to check.
* @returns {void}
*/
ReturnStatement(node) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Skips if no argument.
if (!node.argument) {
return;
}
// Returning argument is a substitute of 'super()'.
const segments = funcInfo.codePath.currentSegments;
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
if (segment.reachable) {
const info = segInfoMap[segment.id];
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
},
/**
* Resets state.
* @returns {void}
*/
"Program:exit"() {
segInfoMap = Object.create(null);
}
};
}
};

View File

@@ -0,0 +1,395 @@
/**
* @fileoverview Rule to flag statements without curly braces
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent brace style for all control statements",
category: "Best Practices",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["all"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["multi", "multi-line", "multi-or-nest"]
},
{
enum: ["consistent"]
}
],
minItems: 0,
maxItems: 2
}
]
},
fixable: "code"
},
create(context) {
const multiOnly = (context.options[0] === "multi");
const multiLine = (context.options[0] === "multi-line");
const multiOrNest = (context.options[0] === "multi-or-nest");
const consistent = (context.options[1] === "consistent");
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Determines if a given node is a one-liner that's on the same line as it's preceding code.
* @param {ASTNode} node The node to check.
* @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
* @private
*/
function isCollapsedOneLiner(node) {
const before = sourceCode.getTokenBefore(node);
const last = sourceCode.getLastToken(node);
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
return before.loc.start.line === lastExcludingSemicolon.loc.end.line;
}
/**
* Determines if a given node is a one-liner.
* @param {ASTNode} node The node to check.
* @returns {boolean} True if the node is a one-liner.
* @private
*/
function isOneLiner(node) {
const first = sourceCode.getFirstToken(node),
last = sourceCode.getLastToken(node);
return first.loc.start.line === last.loc.end.line;
}
/**
* Checks if the given token is an `else` token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is an `else` token.
*/
function isElseKeywordToken(token) {
return token.value === "else" && token.type === "Keyword";
}
/**
* Gets the `else` keyword token of a given `IfStatement` node.
* @param {ASTNode} node - A `IfStatement` node to get.
* @returns {Token} The `else` keyword token.
*/
function getElseKeyword(node) {
return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken);
}
/**
* Checks a given IfStatement node requires braces of the consequent chunk.
* This returns `true` when below:
*
* 1. The given node has the `alternate` node.
* 2. There is a `IfStatement` which doesn't have `alternate` node in the
* trailing statement chain of the `consequent` node.
*
* @param {ASTNode} node - A IfStatement node to check.
* @returns {boolean} `true` if the node requires braces of the consequent chunk.
*/
function requiresBraceOfConsequent(node) {
if (node.alternate && node.consequent.type === "BlockStatement") {
if (node.consequent.body.length >= 2) {
return true;
}
node = node.consequent.body[0];
while (node) {
if (node.type === "IfStatement" && !node.alternate) {
return true;
}
node = astUtils.getTrailingStatement(node);
}
}
return false;
}
/**
* Reports "Expected { after ..." error
* @param {ASTNode} node The node to report.
* @param {ASTNode} bodyNode The body node that is incorrectly missing curly brackets
* @param {string} name The name to report.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {void}
* @private
*/
function reportExpectedBraceError(node, bodyNode, name, suffix) {
context.report({
node,
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
message: "Expected { after '{{name}}'{{suffix}}.",
data: {
name,
suffix: (suffix ? ` ${suffix}` : "")
},
fix: fixer => fixer.replaceText(bodyNode, `{${sourceCode.getText(bodyNode)}}`)
});
}
/**
* Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
* @param {Token} closingBracket The } token
* @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
*/
function needsSemicolon(closingBracket) {
const tokenBefore = sourceCode.getTokenBefore(closingBracket);
const tokenAfter = sourceCode.getTokenAfter(closingBracket);
const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
if (astUtils.isSemicolonToken(tokenBefore)) {
// If the last statement already has a semicolon, don't add another one.
return false;
}
if (!tokenAfter) {
// If there are no statements after this block, there is no need to add a semicolon.
return false;
}
if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") {
// If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
// don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
// a SyntaxError if it was followed by `else`.
return false;
}
if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
// If the next token is on the same line, insert a semicolon.
return true;
}
if (/^[([/`+-]/.test(tokenAfter.value)) {
// If the next token starts with a character that would disrupt ASI, insert a semicolon.
return true;
}
if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) {
// If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
return true;
}
// Otherwise, do not insert a semicolon.
return false;
}
/**
* Reports "Unnecessary { after ..." error
* @param {ASTNode} node The node to report.
* @param {ASTNode} bodyNode The block statement that is incorrectly surrounded by parens
* @param {string} name The name to report.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {void}
* @private
*/
function reportUnnecessaryBraceError(node, bodyNode, name, suffix) {
context.report({
node,
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
message: "Unnecessary { after '{{name}}'{{suffix}}.",
data: {
name,
suffix: (suffix ? ` ${suffix}` : "")
},
fix(fixer) {
// `do while` expressions sometimes need a space to be inserted after `do`.
// e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
const needsPrecedingSpace = node.type === "DoWhileStatement" &&
sourceCode.getTokenBefore(bodyNode).range[1] === bodyNode.range[0] &&
!astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(bodyNode, { skip: 1 }));
const openingBracket = sourceCode.getFirstToken(bodyNode);
const closingBracket = sourceCode.getLastToken(bodyNode);
const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket);
if (needsSemicolon(closingBracket)) {
/*
* If removing braces would cause a SyntaxError due to multiple statements on the same line (or
* change the semantics of the code due to ASI), don't perform a fix.
*/
return null;
}
const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) +
sourceCode.getText(lastTokenInBlock) +
sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]);
return fixer.replaceText(bodyNode, (needsPrecedingSpace ? " " : "") + resultingBodyText);
}
});
}
/**
* Prepares to check the body of a node to see if it's a block statement.
* @param {ASTNode} node The node to report if there's a problem.
* @param {ASTNode} body The body node to check for blocks.
* @param {string} name The name to report if there's a problem.
* @param {string} suffix Additional string to add to the end of a report.
* @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
* "actual" will be `true` or `false` whether the body is already a block statement.
* "expected" will be `true` or `false` if the body should be a block statement or not, or
* `null` if it doesn't matter, depending on the rule options. It can be modified to change
* the final behavior of "check".
* "check" will be a function reporting appropriate problems depending on the other
* properties.
*/
function prepareCheck(node, body, name, suffix) {
const hasBlock = (body.type === "BlockStatement");
let expected = null;
if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
expected = true;
} else if (multiOnly) {
if (hasBlock && body.body.length === 1) {
expected = false;
}
} else if (multiLine) {
if (!isCollapsedOneLiner(body)) {
expected = true;
}
} else if (multiOrNest) {
if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
const leadingComments = sourceCode.getCommentsBefore(body.body[0]);
expected = leadingComments.length > 0;
} else if (!isOneLiner(body)) {
expected = true;
}
} else {
expected = true;
}
return {
actual: hasBlock,
expected,
check() {
if (this.expected !== null && this.expected !== this.actual) {
if (this.expected) {
reportExpectedBraceError(node, body, name, suffix);
} else {
reportUnnecessaryBraceError(node, body, name, suffix);
}
}
}
};
}
/**
* Prepares to check the bodies of a "if", "else if" and "else" chain.
* @param {ASTNode} node The first IfStatement node of the chain.
* @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
* information.
*/
function prepareIfChecks(node) {
const preparedChecks = [];
do {
preparedChecks.push(prepareCheck(node, node.consequent, "if", "condition"));
if (node.alternate && node.alternate.type !== "IfStatement") {
preparedChecks.push(prepareCheck(node, node.alternate, "else"));
break;
}
node = node.alternate;
} while (node);
if (consistent) {
/*
* If any node should have or already have braces, make sure they
* all have braces.
* If all nodes shouldn't have braces, make sure they don't.
*/
const expected = preparedChecks.some(preparedCheck => {
if (preparedCheck.expected !== null) {
return preparedCheck.expected;
}
return preparedCheck.actual;
});
preparedChecks.forEach(preparedCheck => {
preparedCheck.expected = expected;
});
}
return preparedChecks;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
IfStatement(node) {
if (node.parent.type !== "IfStatement") {
prepareIfChecks(node).forEach(preparedCheck => {
preparedCheck.check();
});
}
},
WhileStatement(node) {
prepareCheck(node, node.body, "while", "condition").check();
},
DoWhileStatement(node) {
prepareCheck(node, node.body, "do").check();
},
ForStatement(node) {
prepareCheck(node, node.body, "for", "condition").check();
},
ForInStatement(node) {
prepareCheck(node, node.body, "for-in").check();
},
ForOfStatement(node) {
prepareCheck(node, node.body, "for-of").check();
}
};
}
};

View File

@@ -0,0 +1,90 @@
/**
* @fileoverview require default case in switch statements
* @author Aliaksei Shytkin
*/
"use strict";
const DEFAULT_COMMENT_PATTERN = /^no default$/i;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `default` cases in `switch` statements",
category: "Best Practices",
recommended: false
},
schema: [{
type: "object",
properties: {
commentPattern: {
type: "string"
}
},
additionalProperties: false
}]
},
create(context) {
const options = context.options[0] || {};
const commentPattern = options.commentPattern
? new RegExp(options.commentPattern)
: DEFAULT_COMMENT_PATTERN;
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Shortcut to get last element of array
* @param {*[]} collection Array
* @returns {*} Last element
*/
function last(collection) {
return collection[collection.length - 1];
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
SwitchStatement(node) {
if (!node.cases.length) {
/*
* skip check of empty switch because there is no easy way
* to extract comments inside it now
*/
return;
}
const hasDefault = node.cases.some(v => v.test === null);
if (!hasDefault) {
let comment;
const lastCase = last(node.cases);
const comments = sourceCode.getCommentsAfter(lastCase);
if (comments.length) {
comment = last(comments);
}
if (!comment || !commentPattern.test(comment.value.trim())) {
context.report({ node, message: "Expected a default case." });
}
}
}
};
}
};

View File

@@ -0,0 +1,88 @@
/**
* @fileoverview Validates newlines before and after dots
* @author Greg Cochard
*/
"use strict";
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent newlines before and after dots",
category: "Best Practices",
recommended: false
},
schema: [
{
enum: ["object", "property"]
}
],
fixable: "code"
},
create(context) {
const config = context.options[0];
// default to onObject if no preference is passed
const onObject = config === "object" || !config;
const sourceCode = context.getSourceCode();
/**
* Reports if the dot between object and property is on the correct loccation.
* @param {ASTNode} obj The object owning the property.
* @param {ASTNode} prop The property of the object.
* @param {ASTNode} node The corresponding node of the token.
* @returns {void}
*/
function checkDotLocation(obj, prop, node) {
const dot = sourceCode.getTokenBefore(prop);
const textBeforeDot = sourceCode.getText().slice(obj.range[1], dot.range[0]);
const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);
if (dot.type === "Punctuator" && dot.value === ".") {
if (onObject) {
if (!astUtils.isTokenOnSameLine(obj, dot)) {
const neededTextAfterObj = astUtils.isDecimalInteger(obj) ? " " : "";
context.report({
node,
loc: dot.loc.start,
message: "Expected dot to be on same line as object.",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`)
});
}
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
context.report({
node,
loc: dot.loc.start,
message: "Expected dot to be on same line as property.",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
});
}
}
}
/**
* Checks the spacing of the dot within a member expression.
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkNode(node) {
checkDotLocation(node.object, node.property, node);
}
return {
MemberExpression: checkNode
};
}
};

View File

@@ -0,0 +1,159 @@
/**
* @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible.
* @author Josh Perez
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
const keywords = require("../util/keywords");
module.exports = {
meta: {
docs: {
description: "enforce dot notation whenever possible",
category: "Best Practices",
recommended: false
},
schema: [
{
type: "object",
properties: {
allowKeywords: {
type: "boolean"
},
allowPattern: {
type: "string"
}
},
additionalProperties: false
}
],
fixable: "code"
},
create(context) {
const options = context.options[0] || {};
const allowKeywords = options.allowKeywords === void 0 || !!options.allowKeywords;
const sourceCode = context.getSourceCode();
let allowPattern;
if (options.allowPattern) {
allowPattern = new RegExp(options.allowPattern);
}
/**
* Check if the property is valid dot notation
* @param {ASTNode} node The dot notation node
* @param {string} value Value which is to be checked
* @returns {void}
*/
function checkComputedProperty(node, value) {
if (
validIdentifier.test(value) &&
(allowKeywords || keywords.indexOf(String(value)) === -1) &&
!(allowPattern && allowPattern.test(value))
) {
const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``;
context.report({
node: node.property,
message: "[{{propertyValue}}] is better written in dot notation.",
data: {
propertyValue: formattedValue
},
fix(fixer) {
const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
const rightBracket = sourceCode.getLastToken(node);
if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) {
// Don't perform any fixes if there are comments inside the brackets.
return null;
}
const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket);
const needsSpaceAfterProperty = tokenAfterProperty &&
rightBracket.range[1] === tokenAfterProperty.range[0] &&
!astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty);
const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
const textAfterProperty = needsSpaceAfterProperty ? " " : "";
return fixer.replaceTextRange(
[leftBracket.range[0], rightBracket.range[1]],
`${textBeforeDot}.${value}${textAfterProperty}`
);
}
});
}
}
return {
MemberExpression(node) {
if (
node.computed &&
node.property.type === "Literal"
) {
checkComputedProperty(node, node.property.value);
}
if (
node.computed &&
node.property.type === "TemplateLiteral" &&
node.property.expressions.length === 0
) {
checkComputedProperty(node, node.property.quasis[0].value.cooked);
}
if (
!allowKeywords &&
!node.computed &&
keywords.indexOf(String(node.property.name)) !== -1
) {
context.report({
node: node.property,
message: ".{{propertyName}} is a syntax error.",
data: {
propertyName: node.property.name
},
fix(fixer) {
const dot = sourceCode.getTokenBefore(node.property);
const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]);
if (textAfterDot.trim()) {
// Don't perform any fixes if there are comments between the dot and the property name.
return null;
}
if (node.object.type === "Identifier" && node.object.name === "let") {
/*
* A statement that starts with `let[` is parsed as a destructuring variable declaration, not
* a MemberExpression.
*/
return null;
}
return fixer.replaceTextRange(
[dot.range[0], node.property.range[1]],
`[${textAfterDot}"${node.property.name}"]`
);
}
});
}
}
};
}
};

View File

@@ -0,0 +1,94 @@
/**
* @fileoverview Require or disallow newline at the end of files
* @author Nodeca Team <https://github.com/nodeca>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow newline at the end of files",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
enum: ["always", "never", "unix", "windows"]
}
]
},
create(context) {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
Program: function checkBadEOF(node) {
const sourceCode = context.getSourceCode(),
src = sourceCode.getText(),
location = {
column: lodash.last(sourceCode.lines).length,
line: sourceCode.lines.length
},
LF = "\n",
CRLF = `\r${LF}`,
endsWithNewline = lodash.endsWith(src, LF);
let mode = context.options[0] || "always",
appendCRLF = false;
if (mode === "unix") {
// `"unix"` should behave exactly as `"always"`
mode = "always";
}
if (mode === "windows") {
// `"windows"` should behave exactly as `"always"`, but append CRLF in the fixer for backwards compatibility
mode = "always";
appendCRLF = true;
}
if (mode === "always" && !endsWithNewline) {
// File is not newline-terminated, but should be
context.report({
node,
loc: location,
message: "Newline required at end of file but not found.",
fix(fixer) {
return fixer.insertTextAfterRange([0, src.length], appendCRLF ? CRLF : LF);
}
});
} else if (mode === "never" && endsWithNewline) {
// File is newline-terminated, but shouldn't be
context.report({
node,
loc: location,
message: "Newline not allowed at end of file.",
fix(fixer) {
const finalEOLs = /(?:\r?\n)+$/,
match = finalEOLs.exec(sourceCode.text),
start = match.index,
end = sourceCode.text.length;
return fixer.replaceTextRange([start, end], "");
}
});
}
}
};
}
};

View File

@@ -0,0 +1,180 @@
/**
* @fileoverview Rule to flag statements that use != and == instead of !== and ===
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require the use of `===` and `!==`",
category: "Best Practices",
recommended: false
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always"]
},
{
type: "object",
properties: {
null: {
enum: ["always", "never", "ignore"]
}
},
additionalProperties: false
}
],
additionalItems: false
},
{
type: "array",
items: [
{
enum: ["smart", "allow-null"]
}
],
additionalItems: false
}
]
},
fixable: "code"
},
create(context) {
const config = context.options[0] || "always";
const options = context.options[1] || {};
const sourceCode = context.getSourceCode();
const nullOption = (config === "always")
? options.null || "always"
: "ignore";
const enforceRuleForNull = (nullOption === "always");
const enforceInverseRuleForNull = (nullOption === "never");
/**
* Checks if an expression is a typeof expression
* @param {ASTNode} node The node to check
* @returns {boolean} if the node is a typeof expression
*/
function isTypeOf(node) {
return node.type === "UnaryExpression" && node.operator === "typeof";
}
/**
* Checks if either operand of a binary expression is a typeof operation
* @param {ASTNode} node The node to check
* @returns {boolean} if one of the operands is typeof
* @private
*/
function isTypeOfBinary(node) {
return isTypeOf(node.left) || isTypeOf(node.right);
}
/**
* Checks if operands are literals of the same type (via typeof)
* @param {ASTNode} node The node to check
* @returns {boolean} if operands are of same type
* @private
*/
function areLiteralsAndSameType(node) {
return node.left.type === "Literal" && node.right.type === "Literal" &&
typeof node.left.value === typeof node.right.value;
}
/**
* Checks if one of the operands is a literal null
* @param {ASTNode} node The node to check
* @returns {boolean} if operands are null
* @private
*/
function isNullCheck(node) {
return astUtils.isNullLiteral(node.right) || astUtils.isNullLiteral(node.left);
}
/**
* Gets the location (line and column) of the binary expression's operator
* @param {ASTNode} node The binary expression node to check
* @param {string} operator The operator to find
* @returns {Object} { line, column } location of operator
* @private
*/
function getOperatorLocation(node) {
const opToken = sourceCode.getTokenAfter(node.left);
return { line: opToken.loc.start.line, column: opToken.loc.start.column };
}
/**
* Reports a message for this rule.
* @param {ASTNode} node The binary expression node that was checked
* @param {string} expectedOperator The operator that was expected (either '==', '!=', '===', or '!==')
* @returns {void}
* @private
*/
function report(node, expectedOperator) {
context.report({
node,
loc: getOperatorLocation(node),
message: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'.",
data: { expectedOperator, actualOperator: node.operator },
fix(fixer) {
// If the comparison is a `typeof` comparison or both sides are literals with the same type, then it's safe to fix.
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
const operatorToken = sourceCode.getFirstTokenBetween(
node.left,
node.right,
token => token.value === node.operator
);
return fixer.replaceText(operatorToken, expectedOperator);
}
return null;
}
});
}
return {
BinaryExpression(node) {
const isNull = isNullCheck(node);
if (node.operator !== "==" && node.operator !== "!=") {
if (enforceInverseRuleForNull && isNull) {
report(node, node.operator.slice(0, -1));
}
return;
}
if (config === "smart" && (isTypeOfBinary(node) ||
areLiteralsAndSameType(node) || isNull)) {
return;
}
if (!enforceRuleForNull && isNull) {
return;
}
report(node, `${node.operator}=`);
}
};
}
};

View File

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

View File

@@ -0,0 +1,157 @@
/**
* @fileoverview Rule to control spacing within function calls
* @author Matt DuVall <http://www.mattduvall.com>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow spacing between function identifiers and their invocations",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["never"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["always"]
},
{
type: "object",
properties: {
allowNewlines: {
type: "boolean"
}
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
}
},
create(context) {
const never = context.options[0] !== "always";
const allowNewlines = !never && context.options[1] && context.options[1].allowNewlines;
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();
/**
* Check if open space is present in a function name
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkSpacing(node) {
const lastToken = sourceCode.getLastToken(node);
const lastCalleeToken = sourceCode.getLastToken(node.callee);
const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken);
const prevToken = parenToken && sourceCode.getTokenBefore(parenToken);
// Parens in NewExpression are optional
if (!(parenToken && parenToken.range[1] < node.range[1])) {
return;
}
const textBetweenTokens = text.slice(prevToken.range[1], parenToken.range[0]).replace(/\/\*.*?\*\//g, "");
const hasWhitespace = /\s/.test(textBetweenTokens);
const hasNewline = hasWhitespace && astUtils.LINEBREAK_MATCHER.test(textBetweenTokens);
/*
* never allowNewlines hasWhitespace hasNewline message
* F F F F Missing space between function name and paren.
* F F F T (Invalid `!hasWhitespace && hasNewline`)
* F F T T Unexpected newline between function name and paren.
* F F T F (OK)
* F T T F (OK)
* F T T T (OK)
* F T F T (Invalid `!hasWhitespace && hasNewline`)
* F T F F Missing space between function name and paren.
* T T F F (Invalid `never && allowNewlines`)
* T T F T (Invalid `!hasWhitespace && hasNewline`)
* T T T T (Invalid `never && allowNewlines`)
* T T T F (Invalid `never && allowNewlines`)
* T F T F Unexpected space between function name and paren.
* T F T T Unexpected space between function name and paren.
* T F F T (Invalid `!hasWhitespace && hasNewline`)
* T F F F (OK)
*
* T T Unexpected space between function name and paren.
* F F Missing space between function name and paren.
* F F T Unexpected newline between function name and paren.
*/
if (never && hasWhitespace) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Unexpected space between function name and paren.",
fix(fixer) {
// Only autofix if there is no newline
// https://github.com/eslint/eslint/issues/7787
if (!hasNewline) {
return fixer.removeRange([prevToken.range[1], parenToken.range[0]]);
}
return null;
}
});
} else if (!never && !hasWhitespace) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Missing space between function name and paren.",
fix(fixer) {
return fixer.insertTextBefore(parenToken, " ");
}
});
} else if (!never && !allowNewlines && hasNewline) {
context.report({
node,
loc: lastCalleeToken.loc.start,
message: "Unexpected newline between function name and paren.",
fix(fixer) {
return fixer.replaceTextRange([prevToken.range[1], parenToken.range[0]], " ");
}
});
}
}
return {
CallExpression: checkSpacing,
NewExpression: checkSpacing
};
}
};

View File

@@ -0,0 +1,193 @@
/**
* @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned.
* @author Annie Zhang, Pavel Strashkin
*/
"use strict";
//--------------------------------------------------------------------------
// Requirements
//--------------------------------------------------------------------------
const astUtils = require("../ast-utils");
const esutils = require("esutils");
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Determines if a pattern is `module.exports` or `module["exports"]`
* @param {ASTNode} pattern The left side of the AssignmentExpression
* @returns {boolean} True if the pattern is `module.exports` or `module["exports"]`
*/
function isModuleExports(pattern) {
if (pattern.type === "MemberExpression" && pattern.object.type === "Identifier" && pattern.object.name === "module") {
// module.exports
if (pattern.property.type === "Identifier" && pattern.property.name === "exports") {
return true;
}
// module["exports"]
if (pattern.property.type === "Literal" && pattern.property.value === "exports") {
return true;
}
}
return false;
}
/**
* Determines if a string name is a valid identifier
* @param {string} name The string to be checked
* @param {int} ecmaVersion The ECMAScript version if specified in the parserOptions config
* @returns {boolean} True if the string is a valid identifier
*/
function isIdentifier(name, ecmaVersion) {
if (ecmaVersion >= 6) {
return esutils.keyword.isIdentifierES6(name);
}
return esutils.keyword.isIdentifierES5(name);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const alwaysOrNever = { enum: ["always", "never"] };
const optionsObject = {
type: "object",
properties: {
includeCommonJSModuleExports: {
type: "boolean"
}
},
additionalProperties: false
};
module.exports = {
meta: {
docs: {
description: "require function names to match the name of the variable or property to which they are assigned",
category: "Stylistic Issues",
recommended: false
},
schema: {
anyOf: [{
type: "array",
additionalItems: false,
items: [alwaysOrNever, optionsObject]
}, {
type: "array",
additionalItems: false,
items: [optionsObject]
}]
}
},
create(context) {
const options = (typeof context.options[0] === "object" ? context.options[0] : context.options[1]) || {};
const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always";
const includeModuleExports = options.includeCommonJSModuleExports;
const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5;
/**
* Compares identifiers based on the nameMatches option
* @param {string} x the first identifier
* @param {string} y the second identifier
* @returns {boolean} whether the two identifiers should warn.
*/
function shouldWarn(x, y) {
return (nameMatches === "always" && x !== y) || (nameMatches === "never" && x === y);
}
/**
* Reports
* @param {ASTNode} node The node to report
* @param {string} name The variable or property name
* @param {string} funcName The function name
* @param {boolean} isProp True if the reported node is a property assignment
* @returns {void}
*/
function report(node, name, funcName, isProp) {
let message;
if (nameMatches === "always" && isProp) {
message = "Function name `{{funcName}}` should match property name `{{name}}`";
} else if (nameMatches === "always") {
message = "Function name `{{funcName}}` should match variable name `{{name}}`";
} else if (isProp) {
message = "Function name `{{funcName}}` should not match property name `{{name}}`";
} else {
message = "Function name `{{funcName}}` should not match variable name `{{name}}`";
}
context.report({
node,
message,
data: {
name,
funcName
}
});
}
/**
* Determines whether a given node is a string literal
* @param {ASTNode} node The node to check
* @returns {boolean} `true` if the node is a string literal
*/
function isStringLiteral(node) {
return node.type === "Literal" && typeof node.value === "string";
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
VariableDeclarator(node) {
if (!node.init || node.init.type !== "FunctionExpression" || node.id.type !== "Identifier") {
return;
}
if (node.init.id && shouldWarn(node.id.name, node.init.id.name)) {
report(node, node.id.name, node.init.id.name, false);
}
},
AssignmentExpression(node) {
if (
node.right.type !== "FunctionExpression" ||
(node.left.computed && node.left.property.type !== "Literal") ||
(!includeModuleExports && isModuleExports(node.left)) ||
(node.left.type !== "Identifier" && node.left.type !== "MemberExpression")
) {
return;
}
const isProp = node.left.type === "MemberExpression";
const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name;
if (node.right.id && isIdentifier(name) && shouldWarn(name, node.right.id.name)) {
report(node, name, node.right.id.name, isProp);
}
},
Property(node) {
if (node.value.type !== "FunctionExpression" || !node.value.id || node.computed && !isStringLiteral(node.key)) {
return;
}
if (node.key.type === "Identifier" && shouldWarn(node.key.name, node.value.id.name)) {
report(node, node.key.name, node.value.id.name, true);
} else if (
isStringLiteral(node.key) &&
isIdentifier(node.key.value, ecmaVersion) &&
shouldWarn(node.key.value, node.value.id.name)
) {
report(node, node.key.value, node.value.id.name, true);
}
}
};
}
};

View File

@@ -0,0 +1,114 @@
/**
* @fileoverview Rule to warn when a function expression does not have a name.
* @author Kyle T. Nunery
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
/**
* Checks whether or not a given variable is a function name.
* @param {eslint-scope.Variable} variable - A variable to check.
* @returns {boolean} `true` if the variable is a function name.
*/
function isFunctionName(variable) {
return variable && variable.defs[0].type === "FunctionName";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow named `function` expressions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["always", "as-needed", "never"]
}
]
},
create(context) {
const never = context.options[0] === "never";
const asNeeded = context.options[0] === "as-needed";
/**
* Determines whether the current FunctionExpression node is a get, set, or
* shorthand method in an object literal or a class.
* @param {ASTNode} node - A node to check.
* @returns {boolean} True if the node is a get, set, or shorthand method.
*/
function isObjectOrClassMethod(node) {
const parent = node.parent;
return (parent.type === "MethodDefinition" || (
parent.type === "Property" && (
parent.method ||
parent.kind === "get" ||
parent.kind === "set"
)
));
}
/**
* Determines whether the current FunctionExpression node has a name that would be
* inferred from context in a conforming ES6 environment.
* @param {ASTNode} node - A node to check.
* @returns {boolean} True if the node would have a name assigned automatically.
*/
function hasInferredName(node) {
const parent = node.parent;
return isObjectOrClassMethod(node) ||
(parent.type === "VariableDeclarator" && parent.id.type === "Identifier" && parent.init === node) ||
(parent.type === "Property" && parent.value === node) ||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier" && parent.right === node) ||
(parent.type === "ExportDefaultDeclaration" && parent.declaration === node) ||
(parent.type === "AssignmentPattern" && parent.right === node);
}
return {
"FunctionExpression:exit"(node) {
// Skip recursive functions.
const nameVar = context.getDeclaredVariables(node)[0];
if (isFunctionName(nameVar) && nameVar.references.length > 0) {
return;
}
const hasName = Boolean(node.id && node.id.name);
const name = astUtils.getFunctionNameWithKind(node);
if (never) {
if (hasName) {
context.report({
node,
message: "Unexpected named {{name}}.",
data: { name }
});
}
} else {
if (!hasName && (asNeeded ? !hasInferredName(node) : !isObjectOrClassMethod(node))) {
context.report({
node,
message: "Unexpected unnamed {{name}}.",
data: { name }
});
}
}
}
};
}
};

View File

@@ -0,0 +1,89 @@
/**
* @fileoverview Rule to enforce a particular function style
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce the consistent use of either `function` declarations or expressions",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
enum: ["declaration", "expression"]
},
{
type: "object",
properties: {
allowArrowFunctions: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const style = context.options[0],
allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions === true,
enforceDeclarations = (style === "declaration"),
stack = [];
const nodesToCheck = {
FunctionDeclaration(node) {
stack.push(false);
if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
context.report({ node, message: "Expected a function expression." });
}
},
"FunctionDeclaration:exit"() {
stack.pop();
},
FunctionExpression(node) {
stack.push(false);
if (enforceDeclarations && node.parent.type === "VariableDeclarator") {
context.report({ node: node.parent, message: "Expected a function declaration." });
}
},
"FunctionExpression:exit"() {
stack.pop();
},
ThisExpression() {
if (stack.length > 0) {
stack[stack.length - 1] = true;
}
}
};
if (!allowArrowFunctions) {
nodesToCheck.ArrowFunctionExpression = function() {
stack.push(false);
};
nodesToCheck["ArrowFunctionExpression:exit"] = function(node) {
const hasThisExpr = stack.pop();
if (enforceDeclarations && !hasThisExpr && node.parent.type === "VariableDeclarator") {
context.report({ node: node.parent, message: "Expected a function declaration." });
}
};
}
return nodesToCheck;
}
};

View File

@@ -0,0 +1,221 @@
/**
* @fileoverview enforce consistent line breaks inside function parentheses
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce consistent line breaks inside function parentheses",
category: "Stylistic Issues",
recommended: false
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["always", "never", "consistent", "multiline"]
},
{
type: "object",
properties: {
minItems: {
type: "integer",
minimum: 0
}
},
additionalProperties: false
}
]
}
]
},
create(context) {
const sourceCode = context.getSourceCode();
const rawOption = context.options[0] || "multiline";
const multilineOption = rawOption === "multiline";
const consistentOption = rawOption === "consistent";
let minItems;
if (typeof rawOption === "object") {
minItems = rawOption.minItems;
} else if (rawOption === "always") {
minItems = 0;
} else if (rawOption === "never") {
minItems = Infinity;
} else {
minItems = null;
}
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Determines whether there should be newlines inside function parens
* @param {ASTNode[]} elements The arguments or parameters in the list
* @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
* @returns {boolean} `true` if there should be newlines inside the function parens
*/
function shouldHaveNewlines(elements, hasLeftNewline) {
if (multilineOption) {
return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
}
if (consistentOption) {
return hasLeftNewline;
}
return elements.length >= minItems;
}
/**
* Validates a list of arguments or parameters
* @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
* @param {ASTNode[]} elements The arguments or parameters in the list
* @returns {void}
*/
function validateParens(parens, elements) {
const leftParen = parens.leftParen;
const rightParen = parens.rightParen;
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
if (hasLeftNewline && !needsNewlines) {
context.report({
node: leftParen,
message: "Unexpected newline after '('.",
fix(fixer) {
return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
// If there is a comment between the ( and the first element, don't do a fix.
? null
: fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
}
});
} else if (!hasLeftNewline && needsNewlines) {
context.report({
node: leftParen,
message: "Expected a newline after '('.",
fix: fixer => fixer.insertTextAfter(leftParen, "\n")
});
}
if (hasRightNewline && !needsNewlines) {
context.report({
node: rightParen,
message: "Unexpected newline before ')'.",
fix(fixer) {
return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
// If there is a comment between the last element and the ), don't do a fix.
? null
: fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
}
});
} else if (!hasRightNewline && needsNewlines) {
context.report({
node: rightParen,
message: "Expected a newline before ')'.",
fix: fixer => fixer.insertTextBefore(rightParen, "\n")
});
}
}
/**
* Gets the left paren and right paren tokens of a node.
* @param {ASTNode} node The node with parens
* @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
* Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
* with a single parameter)
*/
function getParenTokens(node) {
switch (node.type) {
case "NewExpression":
if (!node.arguments.length && !(
astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
astUtils.isClosingParenToken(sourceCode.getLastToken(node))
)) {
// If the NewExpression does not have parens (e.g. `new Foo`), return null.
return null;
}
// falls through
case "CallExpression":
return {
leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
rightParen: sourceCode.getLastToken(node)
};
case "FunctionDeclaration":
case "FunctionExpression": {
const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
const rightParen = node.params.length
? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
: sourceCode.getTokenAfter(leftParen);
return { leftParen, rightParen };
}
case "ArrowFunctionExpression": {
const firstToken = sourceCode.getFirstToken(node);
if (!astUtils.isOpeningParenToken(firstToken)) {
// If the ArrowFunctionExpression has a single param without parens, return null.
return null;
}
return {
leftParen: firstToken,
rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
};
}
default:
throw new TypeError(`unexpected node with type ${node.type}`);
}
}
/**
* Validates the parentheses for a node
* @param {ASTNode} node The node with parens
* @returns {void}
*/
function validateNode(node) {
const parens = getParenTokens(node);
if (parens) {
validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments);
}
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
ArrowFunctionExpression: validateNode,
CallExpression: validateNode,
FunctionDeclaration: validateNode,
FunctionExpression: validateNode,
NewExpression: validateNode
};
}
};

View File

@@ -0,0 +1,199 @@
/**
* @fileoverview Rule to check the spacing around the * in generator functions.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const OVERRIDE_SCHEMA = {
oneOf: [
{
enum: ["before", "after", "both", "neither"]
},
{
type: "object",
properties: {
before: { type: "boolean" },
after: { type: "boolean" }
},
additionalProperties: false
}
]
};
module.exports = {
meta: {
docs: {
description: "enforce consistent spacing around `*` operators in generator functions",
category: "ECMAScript 6",
recommended: false
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["before", "after", "both", "neither"]
},
{
type: "object",
properties: {
before: { type: "boolean" },
after: { type: "boolean" },
named: OVERRIDE_SCHEMA,
anonymous: OVERRIDE_SCHEMA,
method: OVERRIDE_SCHEMA
},
additionalProperties: false
}
]
}
]
},
create(context) {
const optionDefinitions = {
before: { before: true, after: false },
after: { before: false, after: true },
both: { before: true, after: true },
neither: { before: false, after: false }
};
/**
* Returns resolved option definitions based on an option and defaults
*
* @param {any} option - The option object or string value
* @param {Object} defaults - The defaults to use if options are not present
* @returns {Object} the resolved object definition
*/
function optionToDefinition(option, defaults) {
if (!option) {
return defaults;
}
return typeof option === "string"
? optionDefinitions[option]
: Object.assign({}, defaults, option);
}
const modes = (function(option) {
option = option || {};
const defaults = optionToDefinition(option, optionDefinitions.before);
return {
named: optionToDefinition(option.named, defaults),
anonymous: optionToDefinition(option.anonymous, defaults),
method: optionToDefinition(option.method, defaults)
};
}(context.options[0]));
const sourceCode = context.getSourceCode();
/**
* Checks if the given token is a star token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is a star token.
*/
function isStarToken(token) {
return token.value === "*" && token.type === "Punctuator";
}
/**
* Gets the generator star token of the given function node.
*
* @param {ASTNode} node - The function node to get.
* @returns {Token} Found star token.
*/
function getStarToken(node) {
return sourceCode.getFirstToken(
(node.parent.method || node.parent.type === "MethodDefinition") ? node.parent : node,
isStarToken
);
}
/**
* Checks the spacing between two tokens before or after the star token.
*
* @param {string} kind Either "named", "anonymous", or "method"
* @param {string} side Either "before" or "after".
* @param {Token} leftToken `function` keyword token if side is "before", or
* star token if side is "after".
* @param {Token} rightToken Star token if side is "before", or identifier
* token if side is "after".
* @returns {void}
*/
function checkSpacing(kind, side, leftToken, rightToken) {
if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
const after = leftToken.value === "*";
const spaceRequired = modes[kind][side];
const node = after ? leftToken : rightToken;
const type = spaceRequired ? "Missing" : "Unexpected";
const message = "{{type}} space {{side}} *.";
const data = {
type,
side
};
context.report({
node,
message,
data,
fix(fixer) {
if (spaceRequired) {
if (after) {
return fixer.insertTextAfter(node, " ");
}
return fixer.insertTextBefore(node, " ");
}
return fixer.removeRange([leftToken.range[1], rightToken.range[0]]);
}
});
}
}
/**
* Enforces the spacing around the star if node is a generator function.
*
* @param {ASTNode} node A function expression or declaration node.
* @returns {void}
*/
function checkFunction(node) {
if (!node.generator) {
return;
}
const starToken = getStarToken(node);
const prevToken = sourceCode.getTokenBefore(starToken);
const nextToken = sourceCode.getTokenAfter(starToken);
let kind = "named";
if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
kind = "method";
} else if (!node.id) {
kind = "anonymous";
}
// Only check before when preceded by `function`|`static` keyword
if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
checkSpacing(kind, "before", prevToken, starToken);
}
checkSpacing(kind, "after", starToken, nextToken);
}
return {
FunctionDeclaration: checkFunction,
FunctionExpression: checkFunction
};
}
};

View File

@@ -0,0 +1,176 @@
/**
* @fileoverview Enforces that a return statement is present in property getters.
* @author Aladdin-ADD(hh_2013@foxmail.com)
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
/**
* 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.
*
* @param {ASTNode} node - A function node to get.
* @returns {ASTNode|Token} The node or the token of a location.
*/
function getId(node) {
return node.id || node;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce `return` statements in getters",
category: "Possible Errors",
recommended: false
},
fixable: null,
schema: [
{
type: "object",
properties: {
allowImplicit: {
type: "boolean"
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || { allowImplicit: false };
let funcInfo = {
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false,
node: null
};
/**
* 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,
loc: getId(node).loc.start,
message: funcInfo.hasReturn
? "Expected {{name}} to always return a value."
: "Expected to return a value in {{name}}.",
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node)
}
});
}
}
/** Checks whether a node means a getter function.
* @param {ASTNode} node - a node to check.
* @returns {boolean} if node means a getter, return true; else return false.
*/
function isGetter(node) {
const parent = node.parent;
if (TARGET_NODE_TYPE.test(node.type) && node.body.type === "BlockStatement") {
if (parent.kind === "get") {
return true;
}
if (parent.type === "Property" && astUtils.getStaticPropertyName(parent) === "get" && parent.parent.type === "ObjectExpression") {
// Object.defineProperty()
if (parent.parent.parent.type === "CallExpression" &&
astUtils.getStaticPropertyName(parent.parent.parent.callee) === "defineProperty") {
return true;
}
// Object.defineProperties()
if (parent.parent.parent.type === "Property" &&
parent.parent.parent.parent.type === "ObjectExpression" &&
parent.parent.parent.parent.parent.type === "CallExpression" &&
astUtils.getStaticPropertyName(parent.parent.parent.parent.parent.callee) === "defineProperties") {
return true;
}
}
}
return false;
}
return {
// Stacks this function's information.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
shouldCheck: isGetter(node),
node
};
},
// Pops this function's information.
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Checks the return statement is valid.
ReturnStatement(node) {
if (funcInfo.shouldCheck) {
funcInfo.hasReturn = true;
// if allowImplicit: false, should also check node.argument
if (!options.allowImplicit && !node.argument) {
context.report({
node,
message: "Expected to return a value in {{name}}.",
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node)
}
});
}
}
},
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};

View File

@@ -0,0 +1,75 @@
/**
* @fileoverview Rule for disallowing require() outside of the top-level module context
* @author Jamund Ferguson
*/
"use strict";
const ACCEPTABLE_PARENTS = [
"AssignmentExpression",
"VariableDeclarator",
"MemberExpression",
"ExpressionStatement",
"CallExpression",
"ConditionalExpression",
"Program",
"VariableDeclaration"
];
/**
* Finds the eslint-scope reference in the given scope.
* @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node.
* @returns {Reference|null} Returns the found reference or null if none were found.
*/
function findReference(scope, node) {
const references = scope.references.filter(reference => 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];
}
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) {
const reference = findReference(scope, node);
return reference && reference.resolved && reference.resolved.defs.length > 0;
}
module.exports = {
meta: {
docs: {
description: "require `require()` calls to be placed at top-level module scope",
category: "Node.js and CommonJS",
recommended: false
},
schema: []
},
create(context) {
return {
CallExpression(node) {
const currentScope = context.getScope();
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.indexOf(parent.type) > -1);
if (!isGoodRequire) {
context.report({ node, message: "Unexpected require()." });
}
}
}
};
}
};

View File

@@ -0,0 +1,42 @@
/**
* @fileoverview Rule to flag for-in loops without if statements inside
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require `for-in` loops to include an `if` statement",
category: "Best Practices",
recommended: false
},
schema: []
},
create(context) {
return {
ForInStatement(node) {
/*
* If the for-in statement has {}, then the real body is the body
* of the BlockStatement. Otherwise, just use body as provided.
*/
const body = node.body.type === "BlockStatement" ? node.body.body[0] : node.body;
if (body && body.type !== "IfStatement") {
context.report({ node, message: "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype." });
}
}
};
}
};

View File

@@ -0,0 +1,89 @@
/**
* @fileoverview Ensure handling of errors when we know they exist.
* @author Jamund Ferguson
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require error handling in callbacks",
category: "Node.js and CommonJS",
recommended: false
},
schema: [
{
type: "string"
}
]
},
create(context) {
const errorArgument = context.options[0] || "err";
/**
* Checks if the given argument should be interpreted as a regexp pattern.
* @param {string} stringToCheck The string which should be checked.
* @returns {boolean} Whether or not the string should be interpreted as a pattern.
*/
function isPattern(stringToCheck) {
const firstChar = stringToCheck[0];
return firstChar === "^";
}
/**
* Checks if the given name matches the configured error argument.
* @param {string} name The name which should be compared.
* @returns {boolean} Whether or not the given name matches the configured error variable name.
*/
function matchesConfiguredErrorName(name) {
if (isPattern(errorArgument)) {
const regexp = new RegExp(errorArgument);
return regexp.test(name);
}
return name === errorArgument;
}
/**
* Get the parameters of a given function scope.
* @param {Object} scope The function scope.
* @returns {array} All parameters of the given scope.
*/
function getParameters(scope) {
return scope.variables.filter(variable => variable.defs[0] && variable.defs[0].type === "Parameter");
}
/**
* Check to see if we're handling the error object properly.
* @param {ASTNode} node The AST node to check.
* @returns {void}
*/
function checkForError(node) {
const scope = context.getScope(),
parameters = getParameters(scope),
firstParameter = parameters[0];
if (firstParameter && matchesConfiguredErrorName(firstParameter.name)) {
if (firstParameter.references.length === 0) {
context.report({ node, message: "Expected error to be handled." });
}
}
}
return {
FunctionDeclaration: checkForError,
FunctionExpression: checkForError,
ArrowFunctionExpression: checkForError
};
}
};

View File

@@ -0,0 +1,121 @@
/**
* @fileoverview Rule that warns when identifier names that are
* blacklisted in the configuration are used.
* @author Keith Cirkel (http://keithcirkel.co.uk)
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow specified identifiers",
category: "Stylistic Issues",
recommended: false
},
schema: {
type: "array",
items: {
type: "string"
},
uniqueItems: true
}
},
create(context) {
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
const 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,
message: "Identifier '{{name}}' is blacklisted.",
data: {
name: node.name
}
});
}
return {
Identifier(node) {
const 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);
}
}
};
}
};

View File

@@ -0,0 +1,116 @@
/**
* @fileoverview Rule that warns when identifier names are shorter or longer
* than the values provided in configuration.
* @author Burak Yigit Kaya aka BYK
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce minimum and maximum identifier lengths",
category: "Stylistic Issues",
recommended: false
},
schema: [
{
type: "object",
properties: {
min: {
type: "number"
},
max: {
type: "number"
},
exceptions: {
type: "array",
uniqueItems: true,
items: {
type: "string"
}
},
properties: {
enum: ["always", "never"]
}
},
additionalProperties: false
}
]
},
create(context) {
const options = context.options[0] || {};
const minLength = typeof options.min !== "undefined" ? options.min : 2;
const maxLength = typeof options.max !== "undefined" ? options.max : Infinity;
const properties = options.properties !== "never";
const exceptions = (options.exceptions ? options.exceptions : [])
.reduce((obj, item) => {
obj[item] = true;
return obj;
}, {});
const SUPPORTED_EXPRESSIONS = {
MemberExpression: properties && function(parent) {
return !parent.computed && (
// regular property assignment
(parent.parent.left === parent && parent.parent.type === "AssignmentExpression" ||
// or the last identifier in an ObjectPattern destructuring
parent.parent.type === "Property" && parent.parent.value === parent &&
parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent)
);
},
AssignmentPattern(parent, node) {
return parent.left === node;
},
VariableDeclarator(parent, node) {
return parent.id === node;
},
Property: properties && function(parent, node) {
return parent.key === node;
},
ImportDefaultSpecifier: true,
RestElement: true,
FunctionExpression: true,
ArrowFunctionExpression: true,
ClassDeclaration: true,
FunctionDeclaration: true,
MethodDefinition: true,
CatchClause: true
};
return {
Identifier(node) {
const name = node.name;
const parent = node.parent;
const isShort = name.length < minLength;
const isLong = name.length > maxLength;
if (!(isShort || isLong) || exceptions[name]) {
return; // Nothing to report
}
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
if (isValidExpression && (isValidExpression === true || isValidExpression(parent, node))) {
context.report({
node,
message: isShort
? "Identifier name '{{name}}' is too short (< {{min}})."
: "Identifier name '{{name}}' is too long (> {{max}}).",
data: { name, min: minLength, max: maxLength }
});
}
}
};
}
};

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