tools: update eslint

PR-URL: https://github.com/nodejs/node/pull/27670
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Richard Lau <riclau@uk.ibm.com>
Reviewed-By: Refael Ackermann (רפאל פלחי) <refack@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>
This commit is contained in:
Ruben Bridgewater committed 2019-06-11 15:59:17 +02:00
1 parent 6a5376bb94
commit 8845a77f74
516 files changed
+11130 -7768

No files matched your search

+46 -58
View File
@@ -24,15 +24,25 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J
* 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
## Table of Contents
Prerequisites: [Node.js](https://nodejs.org/en/) (>=6.14), npm version 3+.
1. [Installation and Usage](#installation-and-usage)
2. [Configuration](#configuration)
3. [Code of Conduct](#code-of-conduct)
4. [Filing Issues](#filing-issues)
5. [Frequently Asked Questions](#faq)
6. [Releases](#releases)
7. [Semantic Versioning Policy](#semantic-versioning-policy)
8. [License](#license)
9. [Team](#team)
10. [Sponsors](#sponsors)
11. [Technology Sponsors](#technology-sponsors)
There are two ways to install ESLint: globally and locally.
## <a name="installation-and-usage"></a>Installation and Usage
### Local Installation and Usage
Prerequisites: [Node.js](https://nodejs.org/) (`^8.10.0`, `^10.13.0`, or `>=11.10.1`), npm version 3+.
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:
You can install ESLint using npm:
```
$ npm install eslint --save-dev
@@ -50,33 +60,9 @@ 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.
It is also possible to install ESLint globally rather than locally (using `npm install eslint --global`). However, any plugins or shareable configs that you use must be installed locally in either case.
### 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 set up 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
## <a name="configuration"></a>Configuration
After running `eslint --init`, you'll have a `.eslintrc` file in your directory. In it, you'll see some rules configured like this:
@@ -97,11 +83,11 @@ The names `"semi"` and `"quotes"` are the names of [rules](https://eslint.org/do
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)).
## Code of Conduct
## <a name="code-of-conduct"></a>Code of Conduct
ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
## Filing Issues
## <a name="filing-issues"></a>Filing Issues
Before filing an issue, please be sure to read the guidelines for what you're reporting:
@@ -110,13 +96,15 @@ Before filing an issue, please be sure to read the guidelines for what you're re
* [Proposing a Rule Change](https://eslint.org/docs/developer-guide/contributing/rule-changes)
* [Request a Change](https://eslint.org/docs/developer-guide/contributing/changes)
## Frequently Asked Questions
## <a name="faq"></a>Frequently Asked Questions
### I'm using JSCS, should I migrate to 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. There are still [a few issues](https://github.com/eslint/eslint/milestones/JSCS%20Compatibility) pending. We’ll 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.
Yes. [JSCS has reached end of life](https://eslint.org/blog/2016/07/jscs-end-of-life) and is no longer supported.
If you are having issues with JSCS, you can try to move to ESLint. Have a look at our [migration guide](https://eslint.org/docs/user-guide/migrating-from-jscs).
We have prepared a [migration guide](https://eslint.org/docs/user-guide/migrating-from-jscs) to help you convert your JSCS settings to an ESLint configuration.
We are now at or near 100% compatibility with JSCS. If you try ESLint and believe we are not yet compatible with a JSCS rule/configuration, please create an issue (mentioning that it is a JSCS compatibility issue) and we will evaluate it as per our normal process.
### Does Prettier replace ESLint?
@@ -124,16 +112,9 @@ No, ESLint does both traditional linting (looking for problematic patterns) and
### 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 peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has.
* Make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has.
### Does ESLint support JSX?
@@ -155,11 +136,11 @@ Once a language feature has been adopted into the ECMAScript standard (stage 4 a
Join our [Mailing List](https://groups.google.com/group/eslint) or [Chatroom](https://gitter.im/eslint/eslint).
## Releases
## <a name="releases"></a>Releases
We have scheduled releases every two weeks on Friday or Saturday. You can follow a [release issue](https://github.com/eslint/eslint/issues?q=is%3Aopen+is%3Aissue+label%3Arelease) for updates about the scheduling of any particular release.
## Semantic Versioning Policy
## <a name="semantic-versioning-policy"></a>Semantic Versioning Policy
ESLint follows [semantic versioning](https://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:
@@ -185,11 +166,11 @@ ESLint follows [semantic versioning](https://semver.org). However, due to the na
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
## <a name="license"></a>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)
## Team
## <a name="team"></a>Team
These folks keep the project moving and are resources for help.
@@ -243,6 +224,18 @@ Teddy Katz
</td></tr></tbody></table>
### Reviewers
The people who review and implement new features.
<table><tbody><tr><td align="center" valign="top" width="11%">
<a href="https://github.com/aladdin-add">
<img src="https://github.com/aladdin-add.png?s=75" width="75" height="75"><br />
薛定谔的猫
</a>
</td></tr></tbody></table>
### Committers
@@ -250,11 +243,6 @@ Teddy Katz
The people who review and fix bugs and help triage issues.
<table><tbody><tr><td align="center" valign="top" width="11%">
<a href="https://github.com/aladdin-add">
<img src="https://github.com/aladdin-add.png?s=75" width="75" height="75"><br />
薛定谔的猫
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/g-plane">
<img src="https://github.com/g-plane.png?s=75" width="75" height="75"><br />
Pig Fang
@@ -264,19 +252,19 @@ Pig Fang
<!--teamend-->
## Sponsors
## <a name="sponsors"></a>Sponsors
The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://opencollective.com/eslint) to get your logo on our README and website.
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
<!--sponsorsstart-->
<h3>Gold Sponsors</h3>
<p><a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F098e3bd0-4d57-11e8-9324-0f6cc1f92bf1.png&height=96" alt="Airbnb" height="96"></a> <a href="https://code.facebook.com/projects/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fres.cloudinary.com%2Fopencollective%2Fimage%2Fupload%2Fv1508519428%2FS9gk78AS_400x400_fulq2l.jpg&height=96" alt="Facebook Open Source" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2Fbbdb9cc0-3b5d-11e9-9537-ad85092287b8.png&height=96" alt="Badoo" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://www.ampproject.org/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Fopencollective-production.s3-us-west-1.amazonaws.com%2F68ed8b70-ebf2-11e6-9958-cb7e79408c56.png&height=96" alt="AMP Project" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="http://faithlife.com/ref/about"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Ffaithlife.com&height=96" alt="Faithlife" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/proxy/images/?src=https%3A%2F%2Flogo.clearbit.com%2Fjsheroes.io&height=96" alt="JSHeroes " height="32"></a></p>
<p><a href="http://salesforce.com"><img src="https://images.opencollective.com/salesforce/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/logo.png" alt="Airbnb" height="96"></a> <a href="https://code.facebook.com/projects/"><img src="https://images.opencollective.com/fbopensource/logo.png" alt="Facebook Open Source" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/badoo/logo.png" alt="Badoo" height="96"></a> <a href="https://www.shopify.com"><img src="https://images.opencollective.com/shopify/logo.png" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://www.ampproject.org/"><img src="https://images.opencollective.com/amp/logo.png" alt="AMP Project" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://usave.co.uk/utilities/broadband/"><img src="https://images.opencollective.com/usave/logo.png" alt="Usave" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/tekhattan/logo.png" alt="TekHattan" height="32"></a> <a href="https://faithlife.com/ref/about"><img src="https://images.opencollective.com/faithlife/logo.png" alt="Faithlife" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/marfeel/logo.png" alt="Marfeel" height="32"></a> <a href="https://clay.global"><img src="https://images.opencollective.com/clayglobal/logo.png" alt="clay" height="32"></a> <a href="https://discordapp.com"><img src="https://images.opencollective.com/discordapp/logo.png" alt="Discord" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/jsheroes1/logo.png" alt="JSHeroes " height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/logo.png" alt="Fire Stick Tricks" height="32"></a></p>
<!--sponsorsend-->
## Technology Sponsors
## <a name="technology-sponsors"></a>Technology Sponsors
* Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com)
+1 -1
View File
@@ -65,7 +65,7 @@ if (useStdIn) {
process.exitCode = cli.execute(process.argv, fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8"));
} else if (init) {
const configInit = require("../lib/config/config-initializer");
const configInit = require("../lib/init/config-initializer");
configInit.initializeConfig().then(() => {
process.exitCode = 0;
+39 -32
View File
@@ -7,64 +7,71 @@
const baseConfigProperties = {
env: { type: "object" },
extends: { $ref: "#/definitions/stringOrStrings" },
globals: { type: "object" },
overrides: {
type: "array",
items: { $ref: "#/definitions/overrideConfig" },
additionalItems: false
},
parser: { type: ["string", "null"] },
parserOptions: { type: "object" },
plugins: { type: "array" },
processor: { type: "string" },
rules: { type: "object" },
settings: { type: "object" },
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
};
const overrideProperties = Object.assign(
{},
baseConfigProperties,
{
files: {
const configSchema = {
definitions: {
stringOrStrings: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
minItems: 1
additionalItems: false
}
]
},
excludedFiles: {
stringOrStringsRequired: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" }
items: { type: "string" },
additionalItems: false,
minItems: 1
}
]
}
}
);
},
const topLevelConfigProperties = Object.assign(
{},
baseConfigProperties,
{
extends: { type: ["string", "array"] },
root: { type: "boolean" },
overrides: {
type: "array",
items: {
type: "object",
properties: overrideProperties,
required: ["files"],
additionalProperties: false
}
}
}
);
// Config at top-level.
objectConfig: {
type: "object",
properties: {
root: { type: "boolean" },
...baseConfigProperties
},
additionalProperties: false
},
const configSchema = {
type: "object",
properties: topLevelConfigProperties,
additionalProperties: false
// Config in `overrides`.
overrideConfig: {
type: "object",
properties: {
excludedFiles: { $ref: "#/definitions/stringOrStrings" },
files: { $ref: "#/definitions/stringOrStringsRequired" },
...baseConfigProperties
},
required: ["files"],
additionalProperties: false
}
},
$ref: "#/definitions/objectConfig"
};
module.exports = configSchema;
+3 -2
View File
@@ -14,7 +14,8 @@ const globals = require("globals");
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/** @type {Map<string, import("../lib/shared/types").Environment>} */
module.exports = new Map(Object.entries({
builtin: {
globals: globals.es5
},
@@ -106,4 +107,4 @@ module.exports = {
greasemonkey: {
globals: globals.greasemonkey
}
};
}));
+9 -7
View File
@@ -9,21 +9,23 @@
// Requirements
//------------------------------------------------------------------------------
const builtInRules = require("../lib/built-in-rules-index");
const builtInRules = require("../lib/rules");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const enabledRules = Object.keys(builtInRules).reduce((result, ruleId) => {
if (!builtInRules[ruleId].meta.deprecated) {
result[ruleId] = "error";
const allRules = {};
for (const [ruleId, rule] of builtInRules) {
if (!rule.meta.deprecated) {
allRules[ruleId] = "error";
}
return result;
}, {});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { rules: enabledRules };
/** @type {import("../lib/shared/types").ConfigData} */
module.exports = { rules: allRules };
+59 -4
View File
@@ -6,10 +6,65 @@
"use strict";
const builtInRules = require("../lib/built-in-rules-index");
/* eslint sort-keys: ["error", "asc"] */
/** @type {import("../lib/shared/types").ConfigData} */
module.exports = {
rules: Object.assign({}, ...Object.keys(builtInRules).map(ruleId => ({
[ruleId]: builtInRules[ruleId].meta.docs.recommended ? "error" : "off"
})))
rules: {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-extra-semi": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-inner-declarations": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-misleading-character-class": "error",
"no-mixed-spaces-and-tabs": "error",
"no-new-symbol": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-undef": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-labels": "error",
"no-unused-vars": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-atomic-updates": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error"
}
};
+8 -4
View File
@@ -5,15 +5,19 @@
"use strict";
const Linter = require("./linter");
const { CLIEngine } = require("./cli-engine");
const { Linter } = require("./linter");
const { RuleTester } = require("./rule-tester");
const { SourceCode } = require("./source-code");
module.exports = {
Linter,
CLIEngine: require("./cli-engine"),
RuleTester: require("./testers/rule-tester"),
SourceCode: require("./util/source-code")
CLIEngine,
RuleTester,
SourceCode
};
// DOTO: remove deprecated API.
let deprecatedLinterInstance = null;
Object.defineProperty(module.exports, "linter", {
-278
View File
@@ -1,278 +0,0 @@
/**
* @fileoverview Collects the built-in rules into a map structure so that they can be imported all at once and without
* using the file-system directly.
* @author Peter (Somogyvari) Metz
*/
"use strict";
/* eslint sort-keys: ["error", "asc"] */
module.exports = {
"accessor-pairs": require("./rules/accessor-pairs"),
"array-bracket-newline": require("./rules/array-bracket-newline"),
"array-bracket-spacing": require("./rules/array-bracket-spacing"),
"array-callback-return": require("./rules/array-callback-return"),
"array-element-newline": require("./rules/array-element-newline"),
"arrow-body-style": require("./rules/arrow-body-style"),
"arrow-parens": require("./rules/arrow-parens"),
"arrow-spacing": require("./rules/arrow-spacing"),
"block-scoped-var": require("./rules/block-scoped-var"),
"block-spacing": require("./rules/block-spacing"),
"brace-style": require("./rules/brace-style"),
"callback-return": require("./rules/callback-return"),
camelcase: require("./rules/camelcase"),
"capitalized-comments": require("./rules/capitalized-comments"),
"class-methods-use-this": require("./rules/class-methods-use-this"),
"comma-dangle": require("./rules/comma-dangle"),
"comma-spacing": require("./rules/comma-spacing"),
"comma-style": require("./rules/comma-style"),
complexity: require("./rules/complexity"),
"computed-property-spacing": require("./rules/computed-property-spacing"),
"consistent-return": require("./rules/consistent-return"),
"consistent-this": require("./rules/consistent-this"),
"constructor-super": require("./rules/constructor-super"),
curly: require("./rules/curly"),
"default-case": require("./rules/default-case"),
"dot-location": require("./rules/dot-location"),
"dot-notation": require("./rules/dot-notation"),
"eol-last": require("./rules/eol-last"),
eqeqeq: require("./rules/eqeqeq"),
"for-direction": require("./rules/for-direction"),
"func-call-spacing": require("./rules/func-call-spacing"),
"func-name-matching": require("./rules/func-name-matching"),
"func-names": require("./rules/func-names"),
"func-style": require("./rules/func-style"),
"function-paren-newline": require("./rules/function-paren-newline"),
"generator-star-spacing": require("./rules/generator-star-spacing"),
"getter-return": require("./rules/getter-return"),
"global-require": require("./rules/global-require"),
"guard-for-in": require("./rules/guard-for-in"),
"handle-callback-err": require("./rules/handle-callback-err"),
"id-blacklist": require("./rules/id-blacklist"),
"id-length": require("./rules/id-length"),
"id-match": require("./rules/id-match"),
"implicit-arrow-linebreak": require("./rules/implicit-arrow-linebreak"),
indent: require("./rules/indent"),
"indent-legacy": require("./rules/indent-legacy"),
"init-declarations": require("./rules/init-declarations"),
"jsx-quotes": require("./rules/jsx-quotes"),
"key-spacing": require("./rules/key-spacing"),
"keyword-spacing": require("./rules/keyword-spacing"),
"line-comment-position": require("./rules/line-comment-position"),
"linebreak-style": require("./rules/linebreak-style"),
"lines-around-comment": require("./rules/lines-around-comment"),
"lines-around-directive": require("./rules/lines-around-directive"),
"lines-between-class-members": require("./rules/lines-between-class-members"),
"max-classes-per-file": require("./rules/max-classes-per-file"),
"max-depth": require("./rules/max-depth"),
"max-len": require("./rules/max-len"),
"max-lines": require("./rules/max-lines"),
"max-lines-per-function": require("./rules/max-lines-per-function"),
"max-nested-callbacks": require("./rules/max-nested-callbacks"),
"max-params": require("./rules/max-params"),
"max-statements": require("./rules/max-statements"),
"max-statements-per-line": require("./rules/max-statements-per-line"),
"multiline-comment-style": require("./rules/multiline-comment-style"),
"multiline-ternary": require("./rules/multiline-ternary"),
"new-cap": require("./rules/new-cap"),
"new-parens": require("./rules/new-parens"),
"newline-after-var": require("./rules/newline-after-var"),
"newline-before-return": require("./rules/newline-before-return"),
"newline-per-chained-call": require("./rules/newline-per-chained-call"),
"no-alert": require("./rules/no-alert"),
"no-array-constructor": require("./rules/no-array-constructor"),
"no-async-promise-executor": require("./rules/no-async-promise-executor"),
"no-await-in-loop": require("./rules/no-await-in-loop"),
"no-bitwise": require("./rules/no-bitwise"),
"no-buffer-constructor": require("./rules/no-buffer-constructor"),
"no-caller": require("./rules/no-caller"),
"no-case-declarations": require("./rules/no-case-declarations"),
"no-catch-shadow": require("./rules/no-catch-shadow"),
"no-class-assign": require("./rules/no-class-assign"),
"no-compare-neg-zero": require("./rules/no-compare-neg-zero"),
"no-cond-assign": require("./rules/no-cond-assign"),
"no-confusing-arrow": require("./rules/no-confusing-arrow"),
"no-console": require("./rules/no-console"),
"no-const-assign": require("./rules/no-const-assign"),
"no-constant-condition": require("./rules/no-constant-condition"),
"no-continue": require("./rules/no-continue"),
"no-control-regex": require("./rules/no-control-regex"),
"no-debugger": require("./rules/no-debugger"),
"no-delete-var": require("./rules/no-delete-var"),
"no-div-regex": require("./rules/no-div-regex"),
"no-dupe-args": require("./rules/no-dupe-args"),
"no-dupe-class-members": require("./rules/no-dupe-class-members"),
"no-dupe-keys": require("./rules/no-dupe-keys"),
"no-duplicate-case": require("./rules/no-duplicate-case"),
"no-duplicate-imports": require("./rules/no-duplicate-imports"),
"no-else-return": require("./rules/no-else-return"),
"no-empty": require("./rules/no-empty"),
"no-empty-character-class": require("./rules/no-empty-character-class"),
"no-empty-function": require("./rules/no-empty-function"),
"no-empty-pattern": require("./rules/no-empty-pattern"),
"no-eq-null": require("./rules/no-eq-null"),
"no-eval": require("./rules/no-eval"),
"no-ex-assign": require("./rules/no-ex-assign"),
"no-extend-native": require("./rules/no-extend-native"),
"no-extra-bind": require("./rules/no-extra-bind"),
"no-extra-boolean-cast": require("./rules/no-extra-boolean-cast"),
"no-extra-label": require("./rules/no-extra-label"),
"no-extra-parens": require("./rules/no-extra-parens"),
"no-extra-semi": require("./rules/no-extra-semi"),
"no-fallthrough": require("./rules/no-fallthrough"),
"no-floating-decimal": require("./rules/no-floating-decimal"),
"no-func-assign": require("./rules/no-func-assign"),
"no-global-assign": require("./rules/no-global-assign"),
"no-implicit-coercion": require("./rules/no-implicit-coercion"),
"no-implicit-globals": require("./rules/no-implicit-globals"),
"no-implied-eval": require("./rules/no-implied-eval"),
"no-inline-comments": require("./rules/no-inline-comments"),
"no-inner-declarations": require("./rules/no-inner-declarations"),
"no-invalid-regexp": require("./rules/no-invalid-regexp"),
"no-invalid-this": require("./rules/no-invalid-this"),
"no-irregular-whitespace": require("./rules/no-irregular-whitespace"),
"no-iterator": require("./rules/no-iterator"),
"no-label-var": require("./rules/no-label-var"),
"no-labels": require("./rules/no-labels"),
"no-lone-blocks": require("./rules/no-lone-blocks"),
"no-lonely-if": require("./rules/no-lonely-if"),
"no-loop-func": require("./rules/no-loop-func"),
"no-magic-numbers": require("./rules/no-magic-numbers"),
"no-misleading-character-class": require("./rules/no-misleading-character-class"),
"no-mixed-operators": require("./rules/no-mixed-operators"),
"no-mixed-requires": require("./rules/no-mixed-requires"),
"no-mixed-spaces-and-tabs": require("./rules/no-mixed-spaces-and-tabs"),
"no-multi-assign": require("./rules/no-multi-assign"),
"no-multi-spaces": require("./rules/no-multi-spaces"),
"no-multi-str": require("./rules/no-multi-str"),
"no-multiple-empty-lines": require("./rules/no-multiple-empty-lines"),
"no-native-reassign": require("./rules/no-native-reassign"),
"no-negated-condition": require("./rules/no-negated-condition"),
"no-negated-in-lhs": require("./rules/no-negated-in-lhs"),
"no-nested-ternary": require("./rules/no-nested-ternary"),
"no-new": require("./rules/no-new"),
"no-new-func": require("./rules/no-new-func"),
"no-new-object": require("./rules/no-new-object"),
"no-new-require": require("./rules/no-new-require"),
"no-new-symbol": require("./rules/no-new-symbol"),
"no-new-wrappers": require("./rules/no-new-wrappers"),
"no-obj-calls": require("./rules/no-obj-calls"),
"no-octal": require("./rules/no-octal"),
"no-octal-escape": require("./rules/no-octal-escape"),
"no-param-reassign": require("./rules/no-param-reassign"),
"no-path-concat": require("./rules/no-path-concat"),
"no-plusplus": require("./rules/no-plusplus"),
"no-process-env": require("./rules/no-process-env"),
"no-process-exit": require("./rules/no-process-exit"),
"no-proto": require("./rules/no-proto"),
"no-prototype-builtins": require("./rules/no-prototype-builtins"),
"no-redeclare": require("./rules/no-redeclare"),
"no-regex-spaces": require("./rules/no-regex-spaces"),
"no-restricted-globals": require("./rules/no-restricted-globals"),
"no-restricted-imports": require("./rules/no-restricted-imports"),
"no-restricted-modules": require("./rules/no-restricted-modules"),
"no-restricted-properties": require("./rules/no-restricted-properties"),
"no-restricted-syntax": require("./rules/no-restricted-syntax"),
"no-return-assign": require("./rules/no-return-assign"),
"no-return-await": require("./rules/no-return-await"),
"no-script-url": require("./rules/no-script-url"),
"no-self-assign": require("./rules/no-self-assign"),
"no-self-compare": require("./rules/no-self-compare"),
"no-sequences": require("./rules/no-sequences"),
"no-shadow": require("./rules/no-shadow"),
"no-shadow-restricted-names": require("./rules/no-shadow-restricted-names"),
"no-spaced-func": require("./rules/no-spaced-func"),
"no-sparse-arrays": require("./rules/no-sparse-arrays"),
"no-sync": require("./rules/no-sync"),
"no-tabs": require("./rules/no-tabs"),
"no-template-curly-in-string": require("./rules/no-template-curly-in-string"),
"no-ternary": require("./rules/no-ternary"),
"no-this-before-super": require("./rules/no-this-before-super"),
"no-throw-literal": require("./rules/no-throw-literal"),
"no-trailing-spaces": require("./rules/no-trailing-spaces"),
"no-undef": require("./rules/no-undef"),
"no-undef-init": require("./rules/no-undef-init"),
"no-undefined": require("./rules/no-undefined"),
"no-underscore-dangle": require("./rules/no-underscore-dangle"),
"no-unexpected-multiline": require("./rules/no-unexpected-multiline"),
"no-unmodified-loop-condition": require("./rules/no-unmodified-loop-condition"),
"no-unneeded-ternary": require("./rules/no-unneeded-ternary"),
"no-unreachable": require("./rules/no-unreachable"),
"no-unsafe-finally": require("./rules/no-unsafe-finally"),
"no-unsafe-negation": require("./rules/no-unsafe-negation"),
"no-unused-expressions": require("./rules/no-unused-expressions"),
"no-unused-labels": require("./rules/no-unused-labels"),
"no-unused-vars": require("./rules/no-unused-vars"),
"no-use-before-define": require("./rules/no-use-before-define"),
"no-useless-call": require("./rules/no-useless-call"),
"no-useless-catch": require("./rules/no-useless-catch"),
"no-useless-computed-key": require("./rules/no-useless-computed-key"),
"no-useless-concat": require("./rules/no-useless-concat"),
"no-useless-constructor": require("./rules/no-useless-constructor"),
"no-useless-escape": require("./rules/no-useless-escape"),
"no-useless-rename": require("./rules/no-useless-rename"),
"no-useless-return": require("./rules/no-useless-return"),
"no-var": require("./rules/no-var"),
"no-void": require("./rules/no-void"),
"no-warning-comments": require("./rules/no-warning-comments"),
"no-whitespace-before-property": require("./rules/no-whitespace-before-property"),
"no-with": require("./rules/no-with"),
"nonblock-statement-body-position": require("./rules/nonblock-statement-body-position"),
"object-curly-newline": require("./rules/object-curly-newline"),
"object-curly-spacing": require("./rules/object-curly-spacing"),
"object-property-newline": require("./rules/object-property-newline"),
"object-shorthand": require("./rules/object-shorthand"),
"one-var": require("./rules/one-var"),
"one-var-declaration-per-line": require("./rules/one-var-declaration-per-line"),
"operator-assignment": require("./rules/operator-assignment"),
"operator-linebreak": require("./rules/operator-linebreak"),
"padded-blocks": require("./rules/padded-blocks"),
"padding-line-between-statements": require("./rules/padding-line-between-statements"),
"prefer-arrow-callback": require("./rules/prefer-arrow-callback"),
"prefer-const": require("./rules/prefer-const"),
"prefer-destructuring": require("./rules/prefer-destructuring"),
"prefer-named-capture-group": require("./rules/prefer-named-capture-group"),
"prefer-numeric-literals": require("./rules/prefer-numeric-literals"),
"prefer-object-spread": require("./rules/prefer-object-spread"),
"prefer-promise-reject-errors": require("./rules/prefer-promise-reject-errors"),
"prefer-reflect": require("./rules/prefer-reflect"),
"prefer-rest-params": require("./rules/prefer-rest-params"),
"prefer-spread": require("./rules/prefer-spread"),
"prefer-template": require("./rules/prefer-template"),
"quote-props": require("./rules/quote-props"),
quotes: require("./rules/quotes"),
radix: require("./rules/radix"),
"require-atomic-updates": require("./rules/require-atomic-updates"),
"require-await": require("./rules/require-await"),
"require-jsdoc": require("./rules/require-jsdoc"),
"require-unicode-regexp": require("./rules/require-unicode-regexp"),
"require-yield": require("./rules/require-yield"),
"rest-spread-spacing": require("./rules/rest-spread-spacing"),
semi: require("./rules/semi"),
"semi-spacing": require("./rules/semi-spacing"),
"semi-style": require("./rules/semi-style"),
"sort-imports": require("./rules/sort-imports"),
"sort-keys": require("./rules/sort-keys"),
"sort-vars": require("./rules/sort-vars"),
"space-before-blocks": require("./rules/space-before-blocks"),
"space-before-function-paren": require("./rules/space-before-function-paren"),
"space-in-parens": require("./rules/space-in-parens"),
"space-infix-ops": require("./rules/space-infix-ops"),
"space-unary-ops": require("./rules/space-unary-ops"),
"spaced-comment": require("./rules/spaced-comment"),
strict: require("./rules/strict"),
"switch-colon-spacing": require("./rules/switch-colon-spacing"),
"symbol-description": require("./rules/symbol-description"),
"template-curly-spacing": require("./rules/template-curly-spacing"),
"template-tag-spacing": require("./rules/template-tag-spacing"),
"unicode-bom": require("./rules/unicode-bom"),
"use-isnan": require("./rules/use-isnan"),
"valid-jsdoc": require("./rules/valid-jsdoc"),
"valid-typeof": require("./rules/valid-typeof"),
"vars-on-top": require("./rules/vars-on-top"),
"wrap-iife": require("./rules/wrap-iife"),
"wrap-regex": require("./rules/wrap-regex"),
"yield-star-spacing": require("./rules/yield-star-spacing"),
yoda: require("./rules/yoda")
};
-796
View File
@@ -1,796 +0,0 @@
/**
* @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"),
lodash = require("lodash"),
IgnoredPaths = require("./util/ignored-paths"),
Config = require("./config"),
ConfigOps = require("./config/config-ops"),
LintResultCache = require("./util/lint-result-cache"),
globUtils = require("./util/glob-utils"),
validator = require("./config/config-validator"),
hash = require("./util/hash"),
ModuleResolver = require("./util/module-resolver"),
naming = require("./util/naming"),
pkg = require("../package.json"),
loadRules = require("./load-rules");
const debug = require("debug")("eslint:cli-engine");
const resolver = new ModuleResolver();
const validFixTypes = new Set(["problem", "suggestion", "layout"]);
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* The options to configure a CLI engine with.
* @typedef {Object} CLIEngineOptions
* @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
* @property {Object} baseConfig Base config object, extended by all configs used with this CLIEngine instance
* @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[]} fixTypes Array of rule types to apply fixes for.
* @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
//------------------------------------------------------------------------------
/**
* Determines if each fix type in an array is supported by ESLint and throws
* an error if not.
* @param {string[]} fixTypes An array of fix types to check.
* @returns {void}
* @throws {Error} If an invalid fix type is found.
*/
function validateFixTypes(fixTypes) {
for (const fixType of fixTypes) {
if (!validFixTypes.has(fixType)) {
throw new Error(`Invalid fix type "${fixType}" found.`);
}
}
}
/**
* 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 {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
* @private
*/
function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) {
let filePath,
fileExtension,
processor;
if (filename) {
filePath = path.resolve(filename);
fileExtension = path.extname(filename);
}
const effectiveFilename = filename || "<text>";
debug(`Linting ${effectiveFilename}`);
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: effectiveFilename,
allowInlineConfig,
reportUnusedDisableDirectives,
fix: !!autofixingEnabled && fix,
preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)),
postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename))
});
const stats = calculateStatsPerFile(fixedResult.messages);
const result = {
filePath: effectiveFilename,
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, config };
}
/**
* 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 {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
* @private
*/
function processFile(filename, configHelper, options, linter) {
const text = fs.readFileSync(path.resolve(filename), "utf8");
return processText(
text,
configHelper,
filename,
options.fix,
options.allowInlineConfig,
options.reportUnusedDisableDirectives,
linter
);
}
/**
* 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 = /^\./u.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
};
}
/**
* Produces rule warnings (i.e. deprecation) from configured rules
* @param {(Array<string>|Set<string>)} usedRules - Rules configured
* @param {Map} loadedRules - Map of loaded rules
* @returns {Array<Object>} Contains rule warnings
* @private
*/
function createRuleDeprecationWarnings(usedRules, loadedRules) {
const usedDeprecatedRules = [];
usedRules.forEach(name => {
const loadedRule = loadedRules.get(name);
if (loadedRule && loadedRule.meta && loadedRule.meta.deprecated) {
const deprecatedRule = { ruleId: name };
const replacedBy = lodash.get(loadedRule, "meta.replacedBy", []);
if (replacedBy.every(newRule => lodash.isString(newRule))) {
deprecatedRule.replacedBy = replacedBy;
}
usedDeprecatedRules.push(deprecatedRule);
}
});
return usedDeprecatedRules;
}
/**
* 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
*/
const normalizedCacheFile = path.normalize(cacheFile);
const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
const looksLikeADirectory = normalizedCacheFile.slice(-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;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class CLIEngine {
/**
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} providedOptions The options for this instance.
* @constructor
*/
constructor(providedOptions) {
const options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
providedOptions
);
/*
* if an --ignore-path option is provided, ensure that the ignore
* file exists and is not a directory
*/
if (options.ignore && options.ignorePath) {
try {
if (!fs.statSync(options.ignorePath).isFile()) {
throw new Error(`${options.ignorePath} is not a file`);
}
} catch (e) {
e.message = `Error: Could not load file ${options.ignorePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Stored options for this instance
* @type {Object}
*/
this.options = options;
this.linter = new Linter();
// 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.defineRules(loadRules(rulesdir, cwd));
});
}
if (this.options.rules && Object.keys(this.options.rules).length) {
const loadedRules = this.linter.getRules();
// Ajv validator with default schema will mutate original object, so we must clone it recursively.
this.options.rules = lodash.cloneDeep(this.options.rules);
Object.keys(this.options.rules).forEach(name => {
validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
});
}
this.config = new Config(this.options, this.linter);
if (this.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.
* @type {Object}
*/
this._lintResultCache = new LintResultCache(cacheFile, this.config);
}
// setup special filter for fixes
if (this.options.fix && this.options.fixTypes && this.options.fixTypes.length > 0) {
debug(`Using fix types ${this.options.fixTypes}`);
// throw an error if any invalid fix types are found
validateFixTypes(this.options.fixTypes);
// convert to Set for faster lookup
const fixTypes = new Set(this.options.fixTypes);
// save original value of options.fix in case it's a function
const originalFix = (typeof this.options.fix === "function")
? this.options.fix : () => this.options.fix;
// create a cache of rules (but don't populate until needed)
this._rulesCache = null;
this.options.fix = lintResult => {
const rule = this._rulesCache.get(lintResult.ruleId);
const matches = rule.meta && fixTypes.has(rule.meta.type);
return matches && originalFix(lintResult);
};
}
}
getRules() {
return this.linter.getRules();
}
/**
* 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 => Object.prototype.hasOwnProperty.call(result, "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 globUtils.resolveFileGlobPatterns(patterns.filter(Boolean), 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,
lintResultCache = this._lintResultCache,
configHelper = this.config;
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
if (!options.cache && fs.existsSync(cacheFile)) {
fs.unlinkSync(cacheFile);
}
const startTime = Date.now();
const fileList = globUtils.listFilesToProcess(patterns, options);
const allUsedRules = new Set();
const results = fileList.map(fileInfo => {
if (fileInfo.ignored) {
return createIgnoreResult(fileInfo.filename, options.cwd);
}
if (options.cache) {
const cachedLintResults = lintResultCache.getCachedLintResults(fileInfo.filename);
if (cachedLintResults) {
const resultHadMessages = cachedLintResults.messages && cachedLintResults.messages.length;
if (resultHadMessages && options.fix) {
debug(`Reprocessing cached file to allow autofix: ${fileInfo.filename}`);
} else {
debug(`Skipping file since it hasn't changed: ${fileInfo.filename}`);
return cachedLintResults;
}
}
}
// if there's a cache, populate it
if ("_rulesCache" in this) {
this._rulesCache = this.getRules();
}
debug(`Processing ${fileInfo.filename}`);
const { result, config } = processFile(fileInfo.filename, configHelper, options, this.linter);
Object.keys(config.rules)
.filter(ruleId => ConfigOps.getRuleSeverity(config.rules[ruleId]))
.forEach(ruleId => allUsedRules.add(ruleId));
return result;
});
if (options.cache) {
results.forEach(result => {
/*
* Store the lint result in the LintResultCache.
* NOTE: The LintResultCache will remove the file source and any
* other properties that are difficult to serialize, and will
* hydrate those properties back in on future lint runs.
*/
lintResultCache.setCachedLintResults(result.filePath, result);
});
// persist the cache to disk
lintResultCache.reconcile();
}
const stats = calculateStatsPerRun(results);
const usedDeprecatedRules = createRuleDeprecationWarnings(allUsedRules, this.getRules());
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
errorCount: stats.errorCount,
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount,
usedDeprecatedRules
};
}
/**
* 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)
const resolvedFilename = filename && !path.isAbsolute(filename)
? path.resolve(options.cwd, filename)
: filename;
let usedDeprecatedRules;
if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
if (warnIgnored) {
results.push(createIgnoreResult(resolvedFilename, options.cwd));
}
usedDeprecatedRules = [];
} else {
// if there's a cache, populate it
if ("_rulesCache" in this) {
this._rulesCache = this.getRules();
}
const { result, config } = processText(
text,
configHelper,
resolvedFilename,
options.fix,
options.allowInlineConfig,
options.reportUnusedDisableDirectives,
this.linter
);
results.push(result);
usedDeprecatedRules = createRuleDeprecationWarnings(
Object.keys(config.rules).filter(rule => ConfigOps.getRuleSeverity(config.rules[rule])),
this.getRules()
);
}
const stats = calculateStatsPerRun(results);
return {
results,
errorCount: stats.errorCount,
warningCount: stats.warningCount,
fixableErrorCount: stats.fixableErrorCount,
fixableWarningCount: stats.fixableWarningCount,
usedDeprecatedRules
};
}
/**
* 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
const resolvedFormatName = format || "stylish";
// only strings are valid formatters
if (typeof resolvedFormatName === "string") {
// replace \ with / for Windows compatibility
const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/");
const cwd = this.options ? this.options.cwd : process.cwd();
const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
let formatterPath;
// if there's a slash, then it's a file
if (!namespace && normalizedFormatName.indexOf("/") > -1) {
formatterPath = path.resolve(cwd, normalizedFormatName);
} else {
try {
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
} catch (e) {
formatterPath = `./formatters/${normalizedFormatName}`;
}
}
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;
@@ -0,0 +1,405 @@
/**
* @fileoverview `CascadingConfigArrayFactory` class.
*
* `CascadingConfigArrayFactory` class has a responsibility:
*
* 1. Handles cascading of config files.
*
* It provides two methods:
*
* - `getConfigArrayForFile(filePath)`
* Get the corresponded configuration of a given file. This method doesn't
* throw even if the given file didn't exist.
* - `clearCache()`
* Clear the internal cache. You have to call this method when
* `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
* on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const os = require("os");
const path = require("path");
const { validateConfigArray } = require("../shared/config-validator");
const { ConfigArrayFactory } = require("./config-array-factory");
const { ConfigDependency } = require("./config-array");
const loadRules = require("./load-rules");
const debug = require("debug")("eslint:cascading-config-array-factory");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
// Define types for VSCode IntelliSense.
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").Parser} Parser */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
/**
* @typedef {Object} CascadingConfigArrayFactoryOptions
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
* @property {ConfigData} [baseConfig] The config by `baseConfig` option.
* @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
* @property {string} [cwd] The base directory to start lookup.
* @property {string[]} [rulePaths] The value of `--rulesdir` option.
* @property {string} [specificConfigPath] The value of `--config` option.
* @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
*/
/**
* @typedef {Object} CascadingConfigArrayFactoryInternalSlots
* @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
* @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
* @property {ConfigArray} cliConfigArray The config array of CLI options.
* @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
* @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
* @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
* @property {string} cwd The base directory to start lookup.
* @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
* @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
* @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
* @property {boolean} useEslintrc if `false` then it doesn't load config files.
*/
/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Create the config array from `baseConfig` and `rulePaths`.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createBaseConfigArray({
configArrayFactory,
baseConfigData,
rulePaths,
cwd
}) {
const baseConfigArray = configArrayFactory.create(
baseConfigData,
{ name: "BaseConfig" }
);
if (rulePaths && rulePaths.length > 0) {
/*
* Load rules `--rulesdir` option as a pseudo plugin.
* Use a pseudo plugin to define rules of `--rulesdir`, so we can
* validate the rule's options with only information in the config
* array.
*/
baseConfigArray.push({
name: "--rulesdir",
filePath: "",
plugins: {
"": new ConfigDependency({
definition: {
rules: rulePaths.reduce(
(map, rulesPath) => Object.assign(
map,
loadRules(rulesPath, cwd)
),
{}
)
},
filePath: "",
id: "",
importerName: "--rulesdir",
importerPath: ""
})
}
});
}
return baseConfigArray;
}
/**
* Create the config array from CLI options.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createCLIConfigArray({
cliConfigData,
configArrayFactory,
specificConfigPath
}) {
const cliConfigArray = configArrayFactory.create(
cliConfigData,
{ name: "CLIOptions" }
);
if (specificConfigPath) {
cliConfigArray.unshift(
...configArrayFactory.loadFile(
specificConfigPath,
{ name: "--config" }
)
);
}
return cliConfigArray;
}
/**
* The error type when there are files matched by a glob, but all of them have been ignored.
*/
class ConfigurationNotFoundError extends Error {
/**
* @param {string} directoryPath - The directory path.
*/
constructor(directoryPath) {
super(`No ESLint configuration found in ${directoryPath}.`);
this.messageTemplate = "no-config-found";
this.messageData = { directoryPath };
}
}
/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
class CascadingConfigArrayFactory {
/**
* Initialize this enumerator.
* @param {CascadingConfigArrayFactoryOptions} options The options.
*/
constructor({
additionalPluginPool = new Map(),
baseConfig: baseConfigData = null,
cliConfig: cliConfigData = null,
cwd = process.cwd(),
resolvePluginsRelativeTo = cwd,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true
} = {}) {
const configArrayFactory = new ConfigArrayFactory({
additionalPluginPool,
cwd,
resolvePluginsRelativeTo
});
internalSlotsMap.set(this, {
baseConfigArray: createBaseConfigArray({
baseConfigData,
configArrayFactory,
cwd,
rulePaths
}),
baseConfigData,
cliConfigArray: createCLIConfigArray({
cliConfigData,
configArrayFactory,
specificConfigPath
}),
cliConfigData,
configArrayFactory,
configCache: new Map(),
cwd,
finalizeCache: new WeakMap(),
rulePaths,
specificConfigPath,
useEslintrc
});
}
/**
* The path to the current working directory.
* This is used by tests.
* @type {string}
*/
get cwd() {
const { cwd } = internalSlotsMap.get(this);
return cwd;
}
/**
* Get the config array of a given file.
* @param {string} filePath The file path to a file.
* @returns {ConfigArray} The config array of the file.
*/
getConfigArrayForFile(filePath) {
const { cwd } = internalSlotsMap.get(this);
const directoryPath = path.dirname(path.resolve(cwd, filePath));
debug(`Load config files for ${directoryPath}.`);
return this._finalizeConfigArray(
this._loadConfigInAncestors(directoryPath),
directoryPath
);
}
/**
* Clear config cache.
* @returns {void}
*/
clearCache() {
const slots = internalSlotsMap.get(this);
slots.baseConfigArray = createBaseConfigArray(slots);
slots.cliConfigArray = createCLIConfigArray(slots);
slots.configCache.clear();
}
/**
* Load and normalize config files from the ancestor directories.
* @param {string} directoryPath The path to a leaf directory.
* @returns {ConfigArray} The loaded config.
* @private
*/
_loadConfigInAncestors(directoryPath) {
const {
baseConfigArray,
configArrayFactory,
configCache,
cwd,
useEslintrc
} = internalSlotsMap.get(this);
if (!useEslintrc) {
return baseConfigArray;
}
let configArray = configCache.get(directoryPath);
// Hit cache.
if (configArray) {
debug(`Cache hit: ${directoryPath}.`);
return configArray;
}
debug(`No cache found: ${directoryPath}.`);
const homePath = os.homedir();
// Consider this is root.
if (directoryPath === homePath && cwd !== homePath) {
debug("Stop traversing because of considered root.");
return this._cacheConfig(directoryPath, baseConfigArray);
}
// Load the config on this directory.
try {
configArray = configArrayFactory.loadInDirectory(directoryPath);
} catch (error) {
/* istanbul ignore next */
if (error.code === "EACCES") {
debug("Stop traversing because of 'EACCES' error.");
return this._cacheConfig(directoryPath, baseConfigArray);
}
throw error;
}
if (configArray.length > 0 && configArray.isRoot()) {
debug("Stop traversing because of 'root:true'.");
return this._cacheConfig(directoryPath, configArray);
}
// Load from the ancestors and merge it.
const parentPath = path.dirname(directoryPath);
const parentConfigArray = parentPath && parentPath !== directoryPath
? this._loadConfigInAncestors(parentPath)
: baseConfigArray;
if (configArray.length > 0) {
configArray.unshift(...parentConfigArray);
} else {
configArray = parentConfigArray;
}
// Cache and return.
return this._cacheConfig(directoryPath, configArray);
}
/**
* Freeze and cache a given config.
* @param {string} directoryPath The path to a directory as a cache key.
* @param {ConfigArray} configArray The config array as a cache value.
* @returns {ConfigArray} The `configArray` (frozen).
*/
_cacheConfig(directoryPath, configArray) {
const { configCache } = internalSlotsMap.get(this);
Object.freeze(configArray);
configCache.set(directoryPath, configArray);
return configArray;
}
/**
* Finalize a given config array.
* Concatenate `--config` and other CLI options.
* @param {ConfigArray} configArray The parent config array.
* @param {string} directoryPath The path to the leaf directory to find config files.
* @returns {ConfigArray} The loaded config.
* @private
*/
_finalizeConfigArray(configArray, directoryPath) {
const {
cliConfigArray,
configArrayFactory,
finalizeCache,
useEslintrc
} = internalSlotsMap.get(this);
let finalConfigArray = finalizeCache.get(configArray);
if (!finalConfigArray) {
finalConfigArray = configArray;
// Load the personal config if there are no regular config files.
if (
useEslintrc &&
configArray.every(c => !c.filePath) &&
cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
) {
debug("Loading the config file of the home directory.");
finalConfigArray = configArrayFactory.loadInDirectory(
os.homedir(),
{ name: "PersonalConfig", parent: finalConfigArray }
);
}
// Apply CLI options.
if (cliConfigArray.length > 0) {
finalConfigArray = finalConfigArray.concat(cliConfigArray);
}
// Validate rule settings and environments.
validateConfigArray(finalConfigArray);
// Cache it.
Object.freeze(finalConfigArray);
finalizeCache.set(configArray, finalConfigArray);
debug(
"Configuration was determined: %o on %s",
finalConfigArray,
directoryPath
);
}
if (useEslintrc && finalConfigArray.length === 0) {
throw new ConfigurationNotFoundError(directoryPath);
}
return finalConfigArray;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { CascadingConfigArrayFactory };
+988
View File
@@ -0,0 +1,988 @@
/**
* @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");
const path = require("path");
const defaultOptions = require("../../conf/default-cli-options");
const pkg = require("../../package.json");
const ConfigOps = require("../shared/config-ops");
const ModuleResolver = require("../shared/relative-module-resolver");
const { Linter } = require("../linter");
const builtInRules = require("../rules");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const { getUsedExtractedConfigs } = require("./config-array");
const { FileEnumerator } = require("./file-enumerator");
const hash = require("./hash");
const { IgnoredPaths } = require("./ignored-paths");
const LintResultCache = require("./lint-result-cache");
const naming = require("./naming");
const debug = require("debug")("eslint:cli-engine");
const validFixTypes = new Set(["problem", "suggestion", "layout"]);
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
// For VSCode IntelliSense
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("../shared/types").RuleConf} RuleConf */
/** @typedef {import("../shared/types").Rule} Rule */
/** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
/** @typedef {ReturnType<ConfigArray["extractConfig"]>} ExtractedConfig */
/**
* The options to configure a CLI engine with.
* @typedef {Object} CLIEngineOptions
* @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
* @property {ConfigData} baseConfig Base config object, extended by all configs used with this CLIEngine instance
* @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[]} fixTypes Array of rule types to apply fixes for.
* @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 {ParserOptions} parserOptions An object of parserOption settings to use.
* @property {string[]} plugins An array of plugins to load.
* @property {Record<string,RuleConf>} 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
* @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD
*/
/**
* 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.
*/
/**
* Information of deprecated rules.
* @typedef {Object} DeprecatedRuleInfo
* @property {string} ruleId The rule ID.
* @property {string[]} replacedBy The rule IDs that replace this deprecated rule.
*/
/**
* Linting results.
* @typedef {Object} LintReport
* @property {LintResult[]} results All of 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 {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules.
*/
/**
* Private data for CLIEngine.
* @typedef {Object} CLIEngineInternalSlots
* @property {Map<string, Plugin>} additionalPluginPool The map for additional plugins.
* @property {string} cacheFilePath The path to the cache of lint results.
* @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs.
* @property {FileEnumerator} fileEnumerator The file enumerator.
* @property {IgnoredPaths} ignoredPaths The ignored paths.
* @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used.
* @property {LintResultCache|null} lintResultCache The cache of lint results.
* @property {Linter} linter The linter instance which has loaded rules.
* @property {CLIEngineOptions} options The normalized options of this instance.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/** @type {WeakMap<CLIEngine, CLIEngineInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Determines if each fix type in an array is supported by ESLint and throws
* an error if not.
* @param {string[]} fixTypes An array of fix types to check.
* @returns {void}
* @throws {Error} If an invalid fix type is found.
*/
function validateFixTypes(fixTypes) {
for (const fixType of fixTypes) {
if (!validFixTypes.has(fixType)) {
throw new Error(`Invalid fix type "${fixType}" found.`);
}
}
}
/**
* It will calculate the error and warning count for collection of messages per file
* @param {LintMessage[]} 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 {LintResult[]} 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 {Object} config The config object.
* @param {string} config.text The source code to verify.
* @param {string} config.cwd The path to the current working directory.
* @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
* @param {ConfigArray} config.config The config.
* @param {boolean} config.fix If `true` then it does fix.
* @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
* @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments.
* @param {RegExp} config.extensionRegExp The `RegExp` object that tests if a file path has the allowed file extensions.
* @param {Linter} config.linter The linter instance to verify.
* @returns {LintResult} The result of linting.
* @private
*/
function verifyText({
text,
cwd,
filePath: providedFilePath,
config,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp,
linter
}) {
const filePath = providedFilePath || "<text>";
debug(`Lint ${filePath}`);
/*
* Verify.
* `config.extractConfig(filePath)` requires an absolute path, but `linter`
* doesn't know CWD, so it gives `linter` an absolute path always.
*/
const filePathToVerify = filePath === "<text>" ? path.join(cwd, filePath) : filePath;
const { fixed, messages, output } = linter.verifyAndFix(
text,
config,
{
allowInlineConfig,
filename: filePathToVerify,
fix,
reportUnusedDisableDirectives,
/**
* Check if the linter should adopt a given code block or not.
* Currently, the linter adopts code blocks if the name matches `--ext` option.
* In the future, `overrides` in the configuration would affect the adoption (https://github.com/eslint/rfcs/pull/20).
* @param {string} blockFilename The virtual filename of a code block.
* @returns {boolean} `true` if the linter should adopt the code block.
*/
filterCodeBlock(blockFilename) {
return extensionRegExp.test(blockFilename);
}
}
);
// Tweak and return.
const result = {
filePath,
messages,
...calculateStatsPerFile(messages)
};
if (fixed) {
result.output = output;
}
if (
result.errorCount + result.warningCount > 0 &&
typeof result.output === "undefined"
) {
result.source = text;
}
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 = /^\./u.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
};
}
/**
* Get a rule.
* @param {string} ruleId The rule ID to get.
* @param {ConfigArray[]} configArrays The config arrays that have plugin rules.
* @returns {Rule|null} The rule or null.
*/
function getRule(ruleId, configArrays) {
for (const configArray of configArrays) {
const rule = configArray.pluginRules.get(ruleId);
if (rule) {
return rule;
}
}
return builtInRules.get(ruleId) || null;
}
/**
* Collect used deprecated rules.
* @param {ConfigArray[]} usedConfigArrays The config arrays which were used.
* @param {Map<string, Object>} ruleMap The rule definitions which were used (built-ins).
* @returns {IterableIterator<DeprecatedRuleInfo>} Used deprecated rules.
*/
function *iterateRuleDeprecationWarnings(usedConfigArrays) {
const processedRuleIds = new Set();
// Flatten used configs.
/** @type {ExtractedConfig[]} */
const configs = [].concat(
...usedConfigArrays.map(getUsedExtractedConfigs)
);
// Traverse rule configs.
for (const config of configs) {
for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
// Skip if it was processed.
if (processedRuleIds.has(ruleId)) {
continue;
}
processedRuleIds.add(ruleId);
// Skip if it's not used.
if (!ConfigOps.getRuleSeverity(ruleConfig)) {
continue;
}
const rule = getRule(ruleId, usedConfigArrays);
// Skip if it's not deprecated.
if (!(rule && rule.meta && rule.meta.deprecated)) {
continue;
}
// This rule was used and deprecated.
yield {
ruleId,
replacedBy: rule.meta.replacedBy || []
};
}
}
}
/**
* Checks if the given message is an error message.
* @param {LintMessage} 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
*/
const normalizedCacheFile = path.normalize(cacheFile);
const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
const looksLikeADirectory = normalizedCacheFile.slice(-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;
}
/**
* Convert a string array to a boolean map.
* @param {string[]|null} keys The keys to assign true.
* @param {boolean} defaultValue The default value for each property.
* @param {string} displayName The property name which is used in error message.
* @returns {Record<string,boolean>} The boolean map.
*/
function toBooleanMap(keys, defaultValue, displayName) {
if (keys && !Array.isArray(keys)) {
throw new Error(`${displayName} must be an array.`);
}
if (keys && keys.length > 0) {
return keys.reduce((map, def) => {
const [key, value] = def.split(":");
if (key !== "__proto__") {
map[key] = value === void 0
? defaultValue
: value === "true";
}
return map;
}, {});
}
return void 0;
}
/**
* Create a config data from CLI options.
* @param {CLIEngineOptions} options The options
* @returns {ConfigData|null} The created config data.
*/
function createConfigDataFromOptions(options) {
const { parser, parserOptions, plugins, rules } = options;
const env = toBooleanMap(options.envs, true, "envs");
const globals = toBooleanMap(options.globals, false, "globals");
if (
env === void 0 &&
globals === void 0 &&
parser === void 0 &&
parserOptions === void 0 &&
plugins === void 0 &&
rules === void 0
) {
return null;
}
return { env, globals, parser, parserOptions, plugins, rules };
}
/**
* Checks whether a directory exists at the given location
* @param {string} resolvedPath A path from the CWD
* @returns {boolean} `true` if a directory exists
*/
function directoryExists(resolvedPath) {
try {
return fs.statSync(resolvedPath).isDirectory();
} catch (error) {
if (error && error.code === "ENOENT") {
return false;
}
throw error;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class CLIEngine {
/**
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} providedOptions The options for this instance.
* @constructor
*/
constructor(providedOptions) {
const options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
providedOptions
);
if (options.fix === void 0) {
options.fix = false;
}
const additionalPluginPool = new Map();
const cacheFilePath = getCacheFile(
options.cacheLocation || options.cacheFile,
options.cwd
);
const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool,
baseConfig: options.baseConfig || null,
cliConfig: createConfigDataFromOptions(options),
cwd: options.cwd,
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
rulePaths: options.rulePaths,
specificConfigPath: options.configFile,
useEslintrc: options.useEslintrc
});
const ignoredPaths = new IgnoredPaths(options);
const fileEnumerator = new FileEnumerator({
configArrayFactory,
cwd: options.cwd,
extensions: options.extensions,
globInputPaths: options.globInputPaths,
ignore: options.ignore,
ignoredPaths
});
const lintResultCache =
options.cache ? new LintResultCache(cacheFilePath) : null;
const linter = new Linter();
/** @type {ConfigArray[]} */
const lastConfigArrays = [];
// Store private data.
internalSlotsMap.set(this, {
additionalPluginPool,
cacheFilePath,
configArrayFactory,
fileEnumerator,
ignoredPaths,
lastConfigArrays,
lintResultCache,
linter,
options
});
// setup special filter for fixes
if (options.fix && options.fixTypes && options.fixTypes.length > 0) {
debug(`Using fix types ${options.fixTypes}`);
// throw an error if any invalid fix types are found
validateFixTypes(options.fixTypes);
// convert to Set for faster lookup
const fixTypes = new Set(options.fixTypes);
// save original value of options.fix in case it's a function
const originalFix = (typeof options.fix === "function")
? options.fix : () => true;
options.fix = message => {
const rule = message.ruleId && getRule(message.ruleId, lastConfigArrays);
const matches = rule && rule.meta && fixTypes.has(rule.meta.type);
return matches && originalFix(message);
};
}
}
getRules() {
const { lastConfigArrays } = internalSlotsMap.get(this);
return new Map(function *() {
yield* builtInRules;
for (const configArray of lastConfigArrays) {
yield* configArray.pluginRules;
}
}());
}
/**
* 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({
...result,
messages: filteredMessages,
errorCount: filteredMessages.length,
warningCount: 0,
fixableErrorCount: result.fixableErrorCount,
fixableWarningCount: 0
});
}
});
return filtered;
}
/**
* Outputs fixes from the given results to files.
* @param {LintReport} report The report object created by CLIEngine.
* @returns {void}
*/
static outputFixes(report) {
report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
}
/**
* Add a plugin by passing its configuration
* @param {string} name Name of the plugin.
* @param {Plugin} pluginObject Plugin configuration object.
* @returns {void}
*/
addPlugin(name, pluginObject) {
const {
additionalPluginPool,
configArrayFactory
} = internalSlotsMap.get(this);
additionalPluginPool.set(name, pluginObject);
configArrayFactory.clearCache();
}
/**
* 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) {
const { options } = internalSlotsMap.get(this);
if (options.globInputPaths === false) {
return patterns.filter(Boolean);
}
const extensions = options.extensions.map(ext => ext.replace(/^\./u, ""));
const dirSuffix = `/**/*.{${extensions.join(",")}}`;
return patterns.filter(Boolean).map(pathname => {
const resolvedPath = path.resolve(options.cwd, pathname);
const newPath = directoryExists(resolvedPath)
? pathname.replace(/[/\\]$/u, "") + dirSuffix
: pathname;
return path.normalize(newPath).replace(/\\/gu, "/");
});
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
* @returns {LintReport} The results for all files that were linted.
*/
executeOnFiles(patterns) {
const {
cacheFilePath,
fileEnumerator,
lastConfigArrays,
lintResultCache,
linter,
options: {
allowInlineConfig,
cache,
cwd,
fix,
reportUnusedDisableDirectives
}
} = internalSlotsMap.get(this);
const results = [];
const startTime = Date.now();
// Clear the last used config arrays.
lastConfigArrays.length = 0;
// Delete cache file; should this do here?
if (!cache) {
try {
fs.unlinkSync(cacheFilePath);
} catch (error) {
if (!error || error.code !== "ENOENT") {
throw error;
}
}
}
// Iterate source code files.
for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
if (ignored) {
results.push(createIgnoreResult(filePath, cwd));
continue;
}
/*
* Store used configs for:
* - this method uses to collect used deprecated rules.
* - `getRules()` method uses to collect all loaded rules.
* - `--fix-type` option uses to get the loaded rule's meta data.
*/
if (!lastConfigArrays.includes(config)) {
lastConfigArrays.push(config);
}
// Skip if there is cached result.
if (lintResultCache) {
const cachedResult =
lintResultCache.getCachedLintResults(filePath, config);
if (cachedResult) {
const hadMessages =
cachedResult.messages &&
cachedResult.messages.length > 0;
if (hadMessages && fix) {
debug(`Reprocessing cached file to allow autofix: ${filePath}`);
} else {
debug(`Skipping file since it hasn't changed: ${filePath}`);
results.push(cachedResult);
continue;
}
}
}
// Do lint.
const result = verifyText({
text: fs.readFileSync(filePath, "utf8"),
filePath,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp: fileEnumerator.extensionRegExp,
linter
});
results.push(result);
/*
* Store the lint result in the LintResultCache.
* NOTE: The LintResultCache will remove the file source and any
* other properties that are difficult to serialize, and will
* hydrate those properties back in on future lint runs.
*/
if (lintResultCache) {
lintResultCache.setCachedLintResults(filePath, config, result);
}
}
// Persist the cache to disk.
if (lintResultCache) {
lintResultCache.reconcile();
}
// Collect used deprecated rules.
const usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
...calculateStatsPerRun(results),
usedDeprecatedRules
};
}
/**
* 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 {LintReport} The results for the linting.
*/
executeOnText(text, filename, warnIgnored) {
const {
configArrayFactory,
fileEnumerator,
ignoredPaths,
lastConfigArrays,
linter,
options: {
allowInlineConfig,
cwd,
fix,
reportUnusedDisableDirectives
}
} = internalSlotsMap.get(this);
const results = [];
const startTime = Date.now();
const resolvedFilename = filename && path.resolve(cwd, filename);
// Clear the last used config arrays.
lastConfigArrays.length = 0;
if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
if (warnIgnored) {
results.push(createIgnoreResult(resolvedFilename, cwd));
}
} else {
const config = configArrayFactory.getConfigArrayForFile(
resolvedFilename || "__placeholder__.js"
);
/*
* Store used configs for:
* - this method uses to collect used deprecated rules.
* - `getRules()` method uses to collect all loaded rules.
* - `--fix-type` option uses to get the loaded rule's meta data.
*/
lastConfigArrays.push(config);
// Do lint.
results.push(verifyText({
text,
filePath: resolvedFilename,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
extensionRegExp: fileEnumerator.extensionRegExp,
linter
}));
}
// Collect used deprecated rules.
const usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
debug(`Linting complete in: ${Date.now() - startTime}ms`);
return {
results,
...calculateStatsPerRun(results),
usedDeprecatedRules
};
}
/**
* 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 {ConfigData} A configuration object for the file.
*/
getConfigForFile(filePath) {
const { configArrayFactory, options } = internalSlotsMap.get(this);
const absolutePath = path.resolve(options.cwd, filePath);
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent();
}
/**
* 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 { ignoredPaths } = internalSlotsMap.get(this);
return ignoredPaths.contains(filePath);
}
/**
* 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
const resolvedFormatName = format || "stylish";
// only strings are valid formatters
if (typeof resolvedFormatName === "string") {
// replace \ with / for Windows compatibility
const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/");
const slots = internalSlotsMap.get(this);
const cwd = slots ? slots.options.cwd : process.cwd();
const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
let formatterPath;
// if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
if (!namespace && normalizedFormatName.indexOf("/") > -1) {
formatterPath = path.resolve(cwd, normalizedFormatName);
} else {
try {
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js"));
} catch (e) {
formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName);
}
}
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,
/**
* Get the internal slots of a given CLIEngine instance for tests.
* @param {CLIEngine} instance The CLIEngine instance to get.
* @returns {CLIEngineInternalSlots} The internal slots.
*/
getCLIEngineInternalSlots(instance) {
return internalSlotsMap.get(instance);
}
};
+914
View File
@@ -0,0 +1,914 @@
/**
* @fileoverview The factory of `ConfigArray` objects.
*
* This class provides methods to create `ConfigArray` instance.
*
* - `create(configData, options)`
* Create a `ConfigArray` instance from a config data. This is to handle CLI
* options except `--config`.
* - `loadFile(filePath, options)`
* Create a `ConfigArray` instance from a config file. This is to handle
* `--config` option. If the file was not found, throws the following error:
* - If the filename was `*.js`, a `MODULE_NOT_FOUND` error.
* - If the filename was `package.json`, an IO error or an
* `ESLINT_CONFIG_FIELD_NOT_FOUND` error.
* - Otherwise, an IO error such as `ENOENT`.
* - `loadInDirectory(directoryPath, options)`
* Create a `ConfigArray` instance from a config file which is on a given
* directory. This tries to load `.eslintrc.*` or `package.json`. If not
* found, returns an empty `ConfigArray`.
*
* `ConfigArrayFactory` class has the responsibility that loads configuration
* files, including loading `extends`, `parser`, and `plugins`. The created
* `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`.
*
* But this class doesn't handle cascading. `CascadingConfigArrayFactory` class
* handles cascading and hierarchy.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const importFresh = require("import-fresh");
const stripComments = require("strip-json-comments");
const { validateConfigSchema } = require("../shared/config-validator");
const ModuleResolver = require("../shared/relative-module-resolver");
const { ConfigArray, ConfigDependency, OverrideTester } = require("./config-array");
const naming = require("./naming");
const debug = require("debug")("eslint:config-array-factory");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
const configFilenames = [
".eslintrc.js",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
"package.json"
];
// Define types for VSCode IntelliSense.
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").OverrideConfigData} OverrideConfigData */
/** @typedef {import("../shared/types").Parser} Parser */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */
/** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */
/** @typedef {ConfigArray[0]} ConfigArrayElement */
/**
* @typedef {Object} ConfigArrayFactoryOptions
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
* @property {string} [cwd] The path to the current working directory.
* @property {string} [resolvePluginsRelativeTo] A path to the directory that plugins should be resolved from. Defaults to `cwd`.
*/
/**
* @typedef {Object} ConfigArrayFactoryInternalSlots
* @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
* @property {string} cwd The path to the current working directory.
* @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
*/
/** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Check if a given string is a file path.
* @param {string} nameOrPath A module name or file path.
* @returns {boolean} `true` if the `nameOrPath` is a file path.
*/
function isFilePath(nameOrPath) {
return (
/^\.{1,2}[/\\]/u.test(nameOrPath) ||
path.isAbsolute(nameOrPath)
);
}
/**
* 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/u, "");
}
/**
* Loads a YAML configuration from a file.
* @param {string} filePath The filename to load.
* @returns {ConfigData} 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 {ConfigData} 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}`;
e.messageTemplate = "failed-to-read-json";
e.messageData = {
path: filePath,
message: e.message
};
throw e;
}
}
/**
* Loads a legacy (.eslintrc) configuration from a file.
* @param {string} filePath The filename to load.
* @returns {ConfigData} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadLegacyConfigFile(filePath) {
debug(`Loading legacy 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: %s\n%o", filePath, e);
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 {ConfigData} 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 importFresh(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 {ConfigData} 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 {
const packageData = loadJSONConfigFile(filePath);
if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) {
throw Object.assign(
new Error("package.json file doesn't have 'eslintConfig' field."),
{ code: "ESLINT_CONFIG_FIELD_NOT_FOUND" }
);
}
return packageData.eslintConfig;
} 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) {
return Object.assign(
new Error(`Failed to load config "${configName}" to extend from.`),
{
messageTemplate: "extend-config-missing",
messageData: { configName }
}
);
}
/**
* Loads a configuration file regardless of the source. Inspects the file path
* to determine the correctly way to load the config file.
* @param {string} filePath The path to the configuration.
* @returns {ConfigData|null} The configuration information.
* @private
*/
function loadConfigFile(filePath) {
switch (path.extname(filePath)) {
case ".js":
return loadJSConfigFile(filePath);
case ".json":
if (path.basename(filePath) === "package.json") {
return loadPackageJSONConfigFile(filePath);
}
return loadJSONConfigFile(filePath);
case ".yaml":
case ".yml":
return loadYAMLConfigFile(filePath);
default:
return loadLegacyConfigFile(filePath);
}
}
/**
* Write debug log.
* @param {string} request The requested module name.
* @param {string} relativeTo The file path to resolve the request relative to.
* @param {string} filePath The resolved file path.
* @returns {void}
*/
function writeDebugLogForLoading(request, relativeTo, filePath) {
/* istanbul ignore next */
if (debug.enabled) {
let nameAndVersion = null;
try {
const packageJsonPath = ModuleResolver.resolve(
`${request}/package.json`,
relativeTo
);
const { version = "unknown" } = require(packageJsonPath);
nameAndVersion = `${request}@${version}`;
} catch (error) {
debug("package.json was not found:", error.message);
nameAndVersion = request;
}
debug("Loaded: %s (%s)", nameAndVersion, filePath);
}
}
/**
* Concatenate two config data.
* @param {IterableIterator<ConfigArrayElement>|null} elements The config elements.
* @param {ConfigArray|null} parentConfigArray The parent config array.
* @returns {ConfigArray} The concatenated config array.
*/
function createConfigArray(elements, parentConfigArray) {
if (!elements) {
return parentConfigArray || new ConfigArray();
}
const configArray = new ConfigArray(...elements);
if (parentConfigArray && !configArray.isRoot()) {
configArray.unshift(...parentConfigArray);
}
return configArray;
}
/**
* Normalize a given plugin.
* - Ensure the object to have four properties: configs, environments, processors, and rules.
* - Ensure the object to not have other properties.
* @param {Plugin} plugin The plugin to normalize.
* @returns {Plugin} The normalized plugin.
*/
function normalizePlugin(plugin) {
return {
configs: plugin.configs || {},
environments: plugin.environments || {},
processors: plugin.processors || {},
rules: plugin.rules || {}
};
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The factory of `ConfigArray` objects.
*/
class ConfigArrayFactory {
/**
* Initialize this instance.
* @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
*/
constructor({
additionalPluginPool = new Map(),
cwd = process.cwd(),
resolvePluginsRelativeTo = cwd
} = {}) {
internalSlotsMap.set(this, { additionalPluginPool, cwd, resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo) });
}
/**
* Create `ConfigArray` instance from a config data.
* @param {ConfigData|null} configData The config data to create.
* @param {Object} [options] The options.
* @param {string} [options.filePath] The path to this config data.
* @param {string} [options.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config.
*/
create(configData, { filePath, name, parent } = {}) {
return createConfigArray(
configData
? this._normalizeConfigData(configData, filePath, name)
: null,
parent
);
}
/**
* Load a config file.
* @param {string} filePath The path to a config file.
* @param {Object} [options] The options.
* @param {string} [options.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config.
*/
loadFile(filePath, { name, parent } = {}) {
const { cwd } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, filePath);
return createConfigArray(
this._loadConfigData(absolutePath, name),
parent
);
}
/**
* Load the config file on a given directory if exists.
* @param {string} directoryPath The path to a directory.
* @param {Object} [options] The options.
* @param {string} [options.name] The config name.
* @param {ConfigArray} [options.parent] The parent config array.
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadInDirectory(directoryPath, { name, parent } = {}) {
const { cwd } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, directoryPath);
return createConfigArray(
this._loadConfigDataInDirectory(absolutePath, name),
parent
);
}
/**
* Load a given config file.
* @param {string} filePath The path to a config file.
* @param {string} name The config name.
* @returns {IterableIterator<ConfigArrayElement>} Loaded config.
* @private
*/
_loadConfigData(filePath, name) {
return this._normalizeConfigData(
loadConfigFile(filePath),
filePath,
name
);
}
/**
* Load the config file in a given directory if exists.
* @param {string} directoryPath The path to a directory.
* @param {string} name The config name.
* @returns {IterableIterator<ConfigArrayElement> | null} Loaded config. `null` if any config doesn't exist.
* @private
*/
_loadConfigDataInDirectory(directoryPath, name) {
for (const filename of configFilenames) {
const filePath = path.join(directoryPath, filename);
if (fs.existsSync(filePath)) {
let configData;
try {
configData = loadConfigFile(filePath);
} catch (error) {
if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
throw error;
}
}
if (configData) {
debug(`Config file found: ${filePath}`);
return this._normalizeConfigData(configData, filePath, name);
}
}
}
debug(`Config file not found on ${directoryPath}`);
return null;
}
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {string|undefined} providedFilePath The file path of this config.
* @param {string|undefined} providedName The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_normalizeConfigData(configData, providedFilePath, providedName) {
const { cwd } = internalSlotsMap.get(this);
const filePath = providedFilePath
? path.resolve(cwd, providedFilePath)
: "";
const name = providedName || (filePath && path.relative(cwd, filePath));
validateConfigSchema(configData, name || filePath);
return this._normalizeObjectConfigData(configData, filePath, name);
}
/**
* Normalize a given config to an array.
* @param {ConfigData|OverrideConfigData} configData The config data to normalize.
* @param {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigData(configData, filePath, name) {
const { cwd } = internalSlotsMap.get(this);
const { files, excludedFiles, ...configBody } = configData;
const basePath = filePath ? path.dirname(filePath) : cwd;
const criteria = OverrideTester.create(files, excludedFiles, basePath);
const elements =
this._normalizeObjectConfigDataBody(configBody, filePath, name);
// Apply the criteria to every element.
for (const element of elements) {
// Adopt the base path of the entry file (the outermost base path).
if (element.criteria) {
element.criteria.basePath = basePath;
}
/*
* Merge the criteria; this is for only file extension processors in
* `overrides` section for now.
*/
element.criteria = OverrideTester.and(criteria, element.criteria);
/*
* Remove `root` property to ignore `root` settings which came from
* `extends` in `overrides`.
*/
if (element.criteria) {
element.root = void 0;
}
yield element;
}
}
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigDataBody(
{
env,
extends: extend,
globals,
parser: parserName,
parserOptions,
plugins: pluginList,
processor,
root,
rules,
settings,
overrides: overrideList = []
},
filePath,
name
) {
const extendList = Array.isArray(extend) ? extend : [extend];
// Flatten `extends`.
for (const extendName of extendList.filter(Boolean)) {
yield* this._loadExtends(extendName, filePath, name);
}
// Load parser & plugins.
const parser =
parserName && this._loadParser(parserName, filePath, name);
const plugins =
pluginList && this._loadPlugins(pluginList, filePath, name);
// Yield pseudo config data for file extension processors.
if (plugins) {
yield* this._takeFileExtensionProcessors(plugins, filePath, name);
}
// Yield the config data except `extends` and `overrides`.
yield {
// Debug information.
name,
filePath,
// Config data.
criteria: null,
env,
globals,
parser,
parserOptions,
plugins,
processor,
root,
rules,
settings
};
// Flatten `overries`.
for (let i = 0; i < overrideList.length; ++i) {
yield* this._normalizeObjectConfigData(
overrideList[i],
filePath,
`${name}#overrides[${i}]`
);
}
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtends(extendName, importerPath, importerName) {
debug("Loading {extends:%j} relative to %s", extendName, importerPath);
try {
if (extendName.startsWith("eslint:")) {
return this._loadExtendedBuiltInConfig(
extendName,
importerName
);
}
if (extendName.startsWith("plugin:")) {
return this._loadExtendedPluginConfig(
extendName,
importerPath,
importerName
);
}
return this._loadExtendedShareableConfig(
extendName,
importerPath,
importerName
);
} catch (error) {
error.message += `\nReferenced from: ${importerPath || importerName}`;
throw error;
}
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedBuiltInConfig(extendName, importerName) {
const name = `${importerName} » ${extendName}`;
if (extendName === "eslint:recommended") {
return this._loadConfigData(eslintRecommendedPath, name);
}
if (extendName === "eslint:all") {
return this._loadConfigData(eslintAllPath, name);
}
throw configMissingError(extendName);
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedPluginConfig(extendName, importerPath, importerName) {
const slashIndex = extendName.lastIndexOf("/");
const pluginName = extendName.slice("plugin:".length, slashIndex);
const configName = extendName.slice(slashIndex + 1);
if (isFilePath(pluginName)) {
throw new Error("'extends' cannot use a file path for plugins.");
}
const plugin = this._loadPlugin(pluginName, importerPath, importerName);
const configData =
plugin.definition &&
plugin.definition.configs[configName];
if (configData) {
return this._normalizeConfigData(
configData,
plugin.filePath,
`${importerName} » plugin:${plugin.id}/${configName}`
);
}
throw plugin.error || configMissingError(extendName);
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {string} importerPath The file path which has the `extends` property.
* @param {string} importerName The name of the config which has the `extends` property.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedShareableConfig(extendName, importerPath, importerName) {
const { cwd } = internalSlotsMap.get(this);
const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
let request;
if (isFilePath(extendName)) {
request = extendName;
} else if (extendName.startsWith(".")) {
request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
} else {
request = naming.normalizePackageName(
extendName,
"eslint-config"
);
}
let filePath;
try {
filePath = ModuleResolver.resolve(request, relativeTo);
} catch (error) {
/* istanbul ignore else */
if (error && error.code === "MODULE_NOT_FOUND") {
throw configMissingError(extendName);
}
throw error;
}
writeDebugLogForLoading(request, relativeTo, filePath);
return this._loadConfigData(filePath, `${importerName} » ${request}`);
}
/**
* Load given plugins.
* @param {string[]} names The plugin names to load.
* @param {string} importerPath The path to a config file that imports it. This is just a debug info.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {Record<string,DependentPlugin>} The loaded parser.
* @private
*/
_loadPlugins(names, importerPath, importerName) {
return names.reduce((map, name) => {
if (isFilePath(name)) {
throw new Error("Plugins array cannot includes file paths.");
}
const plugin = this._loadPlugin(name, importerPath, importerName);
map[plugin.id] = plugin;
return map;
}, {});
}
/**
* Load a given parser.
* @param {string} nameOrPath The package name or the path to a parser file.
* @param {string} importerPath The path to a config file that imports it.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {DependentParser} The loaded parser.
*/
_loadParser(nameOrPath, importerPath, importerName) {
debug("Loading parser %j from %s", nameOrPath, importerPath);
const { cwd } = internalSlotsMap.get(this);
const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
try {
const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
return new ConfigDependency({
definition: require(filePath),
filePath,
id: nameOrPath,
importerName,
importerPath
});
} catch (error) {
// If the parser name is "espree", load the espree of ESLint.
if (nameOrPath === "espree") {
debug("Fallback espree.");
return new ConfigDependency({
definition: require("espree"),
filePath: require.resolve("espree"),
id: nameOrPath,
importerName,
importerPath
});
}
debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, importerName);
error.message = `Failed to load parser '${nameOrPath}' declared in '${importerName}': ${error.message}`;
return new ConfigDependency({
error,
id: nameOrPath,
importerName,
importerPath
});
}
}
/**
* Load a given plugin.
* @param {string} name The plugin name to load.
* @param {string} importerPath The path to a config file that imports it. This is just a debug info.
* @param {string} importerName The name of a config file that imports it. This is just a debug info.
* @returns {DependentPlugin} The loaded plugin.
* @private
*/
_loadPlugin(name, importerPath, importerName) {
debug("Loading plugin %j from %s", name, importerPath);
const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this);
const request = naming.normalizePackageName(name, "eslint-plugin");
const id = naming.getShorthandName(request, "eslint-plugin");
const relativeTo = path.join(resolvePluginsRelativeTo, "__placeholder__.js");
if (name.match(/\s+/u)) {
const error = Object.assign(
new Error(`Whitespace found in plugin name '${name}'`),
{
messageTemplate: "whitespace-found",
messageData: { pluginName: request }
}
);
return new ConfigDependency({
error,
id,
importerName,
importerPath
});
}
// Check for additional pool.
const plugin =
additionalPluginPool.get(request) ||
additionalPluginPool.get(id);
if (plugin) {
return new ConfigDependency({
definition: normalizePlugin(plugin),
filePath: importerPath,
id,
importerName,
importerPath
});
}
let filePath;
let error;
try {
filePath = ModuleResolver.resolve(request, relativeTo);
} catch (resolveError) {
error = resolveError;
/* istanbul ignore else */
if (error && error.code === "MODULE_NOT_FOUND") {
error.messageTemplate = "plugin-missing";
error.messageData = {
pluginName: request,
resolvePluginsRelativeTo,
importerName
};
}
}
if (filePath) {
try {
writeDebugLogForLoading(request, relativeTo, filePath);
return new ConfigDependency({
definition: normalizePlugin(require(filePath)),
filePath,
id,
importerName,
importerPath
});
} catch (loadError) {
error = loadError;
}
}
debug("Failed to load plugin '%s' declared in '%s'.", name, importerName);
error.message = `Failed to load plugin '${name}' declared in '${importerName}': ${error.message}`;
return new ConfigDependency({
error,
id,
importerName,
importerPath
});
}
/**
* Take file expression processors as config array elements.
* @param {Record<string,DependentPlugin>} plugins The plugin definitions.
* @param {string} filePath The file path of this config.
* @param {string} name The name of this config.
* @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
* @private
*/
*_takeFileExtensionProcessors(plugins, filePath, name) {
for (const pluginId of Object.keys(plugins)) {
const processors =
plugins[pluginId] &&
plugins[pluginId].definition &&
plugins[pluginId].definition.processors;
if (!processors) {
continue;
}
for (const processorId of Object.keys(processors)) {
if (processorId.startsWith(".")) {
yield* this._normalizeObjectConfigData(
{
files: [`*${processorId}`],
processor: `${pluginId}/${processorId}`
},
filePath,
`${name}#processors["${pluginId}/${processorId}"]`
);
}
}
}
}
}
module.exports = { ConfigArrayFactory };
+455
View File
@@ -0,0 +1,455 @@
/**
* @fileoverview `ConfigArray` class.
*
* `ConfigArray` class expresses the full of a configuration. It has the entry
* config file, base config files that were extended, loaded parsers, and loaded
* plugins.
*
* `ConfigArray` class provies three properties and two methods.
*
* - `pluginEnvironments`
* - `pluginProcessors`
* - `pluginRules`
* The `Map` objects that contain the members of all plugins that this
* config array contains. Those map objects don't have mutation methods.
* Those keys are the member ID such as `pluginId/memberName`.
* - `isRoot()`
* If `true` then this configuration has `root:true` property.
* - `extractConfig(filePath)`
* Extract the final configuration for a given file. This means merging
* every config array element which that `criteria` property matched. The
* `filePath` argument must be an absolute path.
*
* `ConfigArrayFactory` provides the loading logic of config files.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { ExtractedConfig } = require("./extracted-config");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
// Define types for VSCode IntelliSense.
/** @typedef {import("../../shared/types").Environment} Environment */
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../../shared/types").RuleConf} RuleConf */
/** @typedef {import("../../shared/types").Rule} Rule */
/** @typedef {import("../../shared/types").Plugin} Plugin */
/** @typedef {import("../../shared/types").Processor} Processor */
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
/** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */
/**
* @typedef {Object} ConfigArrayElement
* @property {string} name The name of this config element.
* @property {string} filePath The path to the source file of this config element.
* @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element.
* @property {Record<string, boolean>|undefined} env The environment settings.
* @property {Record<string, GlobalConf>|undefined} globals The global variable settings.
* @property {DependentParser|undefined} parser The parser loader.
* @property {Object|undefined} parserOptions The parser options.
* @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders.
* @property {string|undefined} processor The processor name to refer plugin's processor.
* @property {boolean|undefined} root The flag to express root.
* @property {Record<string, RuleConf>|undefined} rules The rule settings
* @property {Object|undefined} settings The shared settings.
*/
/**
* @typedef {Object} ConfigArrayInternalSlots
* @property {Map<string, ExtractedConfig>} cache The cache to extract configs.
* @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition.
* @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition.
* @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition.
*/
/** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */
const internalSlotsMap = new class extends WeakMap {
get(key) {
let value = super.get(key);
if (!value) {
value = {
cache: new Map(),
envMap: null,
processorMap: null,
ruleMap: null
};
super.set(key, value);
}
return value;
}
}();
/**
* Get the indices which are matched to a given file.
* @param {ConfigArrayElement[]} elements The elements.
* @param {string} filePath The path to a target file.
* @returns {number[]} The indices.
*/
function getMatchedIndices(elements, filePath) {
const indices = [];
for (let i = elements.length - 1; i >= 0; --i) {
const element = elements[i];
if (!element.criteria || element.criteria.test(filePath)) {
indices.push(i);
}
}
return indices;
}
/**
* Check if a value is a non-null object.
* @param {any} x The value to check.
* @returns {boolean} `true` if the value is a non-null object.
*/
function isNonNullObject(x) {
return typeof x === "object" && x !== null;
}
/**
* Merge two objects.
*
* Assign every property values of `y` to `x` if `x` doesn't have the property.
* If `x`'s property value is an object, it does recursive.
*
* @param {Object} target The destination to merge
* @param {Object|undefined} source The source to merge.
* @returns {void}
*/
function mergeWithoutOverwrite(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
if (isNonNullObject(target[key])) {
mergeWithoutOverwrite(target[key], source[key]);
} else if (target[key] === void 0) {
if (isNonNullObject(source[key])) {
target[key] = Array.isArray(source[key]) ? [] : {};
mergeWithoutOverwrite(target[key], source[key]);
} else if (source[key] !== void 0) {
target[key] = source[key];
}
}
}
}
/**
* Merge plugins.
* `target`'s definition is prior to `source`'s.
*
* @param {Record<string, DependentPlugin>} target The destination to merge
* @param {Record<string, DependentPlugin>|undefined} source The source to merge.
* @returns {void}
*/
function mergePlugins(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetValue = target[key];
const sourceValue = source[key];
// Adopt the plugin which was found at first.
if (targetValue === void 0) {
if (sourceValue.error) {
throw sourceValue.error;
}
target[key] = sourceValue;
}
}
}
/**
* Merge rule configs.
* `target`'s definition is prior to `source`'s.
*
* @param {Record<string, Array>} target The destination to merge
* @param {Record<string, RuleConf>|undefined} source The source to merge.
* @returns {void}
*/
function mergeRuleConfigs(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetDef = target[key];
const sourceDef = source[key];
// Adopt the rule config which was found at first.
if (targetDef === void 0) {
if (Array.isArray(sourceDef)) {
target[key] = [...sourceDef];
} else {
target[key] = [sourceDef];
}
/*
* If the first found rule config is severity only and the current rule
* config has options, merge the severity and the options.
*/
} else if (
targetDef.length === 1 &&
Array.isArray(sourceDef) &&
sourceDef.length >= 2
) {
targetDef.push(...sourceDef.slice(1));
}
}
}
/**
* Create the extracted config.
* @param {ConfigArray} instance The config elements.
* @param {number[]} indices The indices to use.
* @returns {ExtractedConfig} The extracted config.
*/
function createConfig(instance, indices) {
const slots = internalSlotsMap.get(instance);
const config = new ExtractedConfig();
// Merge elements.
for (const index of indices) {
const element = instance[index];
// Adopt the parser which was found at first.
if (!config.parser && element.parser) {
if (element.parser.error) {
throw element.parser.error;
}
config.parser = element.parser;
}
// Adopt the processor which was found at first.
if (!config.processor && element.processor) {
config.processor = element.processor;
}
// Merge others.
mergeWithoutOverwrite(config.env, element.env);
mergeWithoutOverwrite(config.globals, element.globals);
mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
mergeWithoutOverwrite(config.settings, element.settings);
mergePlugins(config.plugins, element.plugins, slots);
mergeRuleConfigs(config.rules, element.rules);
}
return config;
}
/**
* Collect definitions.
* @template T, U
* @param {string} pluginId The plugin ID for prefix.
* @param {Record<string,T>} defs The definitions to collect.
* @param {Map<string, U>} map The map to output.
* @param {function(T): U} [normalize] The normalize function for each value.
* @returns {void}
*/
function collect(pluginId, defs, map, normalize) {
if (defs) {
const prefix = pluginId && `${pluginId}/`;
for (const [key, value] of Object.entries(defs)) {
map.set(
`${prefix}${key}`,
normalize ? normalize(value) : value
);
}
}
}
/**
* Normalize a rule definition.
* @param {Function|Rule} rule The rule definition to normalize.
* @returns {Rule} The normalized rule definition.
*/
function normalizePluginRule(rule) {
return typeof rule === "function" ? { create: rule } : rule;
}
/**
* Delete the mutation methods from a given map.
* @param {Map<any, any>} map The map object to delete.
* @returns {void}
*/
function deleteMutationMethods(map) {
Object.defineProperties(map, {
clear: { configurable: true, value: void 0 },
delete: { configurable: true, value: void 0 },
set: { configurable: true, value: void 0 }
});
}
/**
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
* @param {ConfigArrayElement[]} elements The config elements.
* @param {ConfigArrayInternalSlots} slots The internal slots.
* @returns {void}
*/
function initPluginMemberMaps(elements, slots) {
const processed = new Set();
slots.envMap = new Map();
slots.processorMap = new Map();
slots.ruleMap = new Map();
for (const element of elements) {
if (!element.plugins) {
continue;
}
for (const [pluginId, value] of Object.entries(element.plugins)) {
const plugin = value.definition;
if (!plugin || processed.has(pluginId)) {
continue;
}
processed.add(pluginId);
collect(pluginId, plugin.environments, slots.envMap);
collect(pluginId, plugin.processors, slots.processorMap);
collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
}
}
deleteMutationMethods(slots.envMap);
deleteMutationMethods(slots.processorMap);
deleteMutationMethods(slots.ruleMap);
}
/**
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
* @param {ConfigArray} instance The config elements.
* @returns {ConfigArrayInternalSlots} The extracted config.
*/
function ensurePluginMemberMaps(instance) {
const slots = internalSlotsMap.get(instance);
if (!slots.ruleMap) {
initPluginMemberMaps(instance, slots);
}
return slots;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The Config Array.
*
* `ConfigArray` instance contains all settings, parsers, and plugins.
* You need to call `ConfigArray#extractConfig(filePath)` method in order to
* extract, merge and get only the config data which is related to an arbitrary
* file.
*
* @extends {Array<ConfigArrayElement>}
*/
class ConfigArray extends Array {
/**
* Get the plugin environments.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Environment>} The plugin environments.
*/
get pluginEnvironments() {
return ensurePluginMemberMaps(this).envMap;
}
/**
* Get the plugin processors.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Processor>} The plugin processors.
*/
get pluginProcessors() {
return ensurePluginMemberMaps(this).processorMap;
}
/**
* Get the plugin rules.
* The returned map cannot be mutated.
* @returns {ReadonlyMap<string, Rule>} The plugin rules.
*/
get pluginRules() {
return ensurePluginMemberMaps(this).ruleMap;
}
/**
* Check if this config has `root` flag.
* @returns {boolean} `true` if this config array is root.
*/
isRoot() {
for (let i = this.length - 1; i >= 0; --i) {
const root = this[i].root;
if (typeof root === "boolean") {
return root;
}
}
return false;
}
/**
* Extract the config data which is related to a given file.
* @param {string} filePath The absolute path to the target file.
* @returns {ExtractedConfig} The extracted config data.
*/
extractConfig(filePath) {
const { cache } = internalSlotsMap.get(this);
const indices = getMatchedIndices(this, filePath);
const cacheKey = indices.join(",");
if (!cache.has(cacheKey)) {
cache.set(cacheKey, createConfig(this, indices));
}
return cache.get(cacheKey);
}
}
const exportObject = {
ConfigArray,
/**
* Get the used extracted configs.
* CLIEngine will use this method to collect used deprecated rules.
* @param {ConfigArray} instance The config array object to get.
* @returns {ExtractedConfig[]} The used extracted configs.
* @private
*/
getUsedExtractedConfigs(instance) {
const { cache } = internalSlotsMap.get(instance);
return Array.from(cache.values());
}
};
module.exports = exportObject;
@@ -0,0 +1,114 @@
/**
* @fileoverview `ConfigDependency` class.
*
* `ConfigDependency` class expresses a loaded parser or plugin.
*
* If the parser or plugin was loaded successfully, it has `definition` property
* and `filePath` property. Otherwise, it has `error` property.
*
* When `JSON.stringify()` converted a `ConfigDependency` object to a JSON, it
* omits `definition` property.
*
* `ConfigArrayFactory` creates `ConfigDependency` objects when it loads parsers
* or plugins.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const util = require("util");
/**
* The class is to store parsers or plugins.
* This class hides the loaded object from `JSON.stringify()` and `console.log`.
* @template T
*/
class ConfigDependency {
/**
* Initialize this instance.
* @param {Object} data The dependency data.
* @param {T} [data.definition] The dependency if the loading succeeded.
* @param {Error} [data.error] The error object if the loading failed.
* @param {string} [data.filePath] The actual path to the dependency if the loading succeeded.
* @param {string} data.id The ID of this dependency.
* @param {string} data.importerName The name of the config file which loads this dependency.
* @param {string} data.importerPath The path to the config file which loads this dependency.
*/
constructor({
definition = null,
error = null,
filePath = null,
id,
importerName,
importerPath
}) {
/**
* The loaded dependency if the loading succeeded.
* @type {T|null}
*/
this.definition = definition;
/**
* The error object if the loading failed.
* @type {Error|null}
*/
this.error = error;
/**
* The loaded dependency if the loading succeeded.
* @type {string|null}
*/
this.filePath = filePath;
/**
* The ID of this dependency.
* @type {string}
*/
this.id = id;
/**
* The name of the config file which loads this dependency.
* @type {string}
*/
this.importerName = importerName;
/**
* The path to the config file which loads this dependency.
* @type {string}
*/
this.importerPath = importerPath;
}
/**
* @returns {Object} a JSON compatible object.
*/
toJSON() {
const obj = this[util.inspect.custom]();
// Display `error.message` (`Error#message` is unenumerable).
if (obj.error instanceof Error) {
obj.error = { ...obj.error, message: obj.error.message };
}
return obj;
}
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
const {
definition: _ignore, // eslint-disable-line no-unused-vars
...obj
} = this;
return obj;
}
}
/** @typedef {ConfigDependency<import("../../shared/types").Parser>} DependentParser */
/** @typedef {ConfigDependency<import("../../shared/types").Plugin>} DependentPlugin */
module.exports = { ConfigDependency };
@@ -0,0 +1,98 @@
/**
* @fileoverview `ExtractedConfig` class.
*
* `ExtractedConfig` class expresses a final configuration for a specific file.
*
* It provides one method.
*
* - `toCompatibleObjectAsConfigFileContent()`
* Convert this configuration to the compatible object as the content of
* config files. It converts the loaded parser and plugins to strings.
* `CLIEngine#getConfigForFile(filePath)` method uses this method.
*
* `ConfigArray#extractConfig(filePath)` creates a `ExtractedConfig` instance.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
// For VSCode intellisense
/** @typedef {import("../../shared/types").ConfigData} ConfigData */
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../../shared/types").SeverityConf} SeverityConf */
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
/**
* The class for extracted config data.
*/
class ExtractedConfig {
constructor() {
/**
* Environments.
* @type {Record<string, boolean>}
*/
this.env = {};
/**
* Global variables.
* @type {Record<string, GlobalConf>}
*/
this.globals = {};
/**
* Parser definition.
* @type {DependentParser|null}
*/
this.parser = null;
/**
* Options for the parser.
* @type {Object}
*/
this.parserOptions = {};
/**
* Plugin definitions.
* @type {Record<string, DependentPlugin>}
*/
this.plugins = {};
/**
* Processor ID.
* @type {string|null}
*/
this.processor = null;
/**
* Rule settings.
* @type {Record<string, [SeverityConf, ...any[]]>}
*/
this.rules = {};
/**
* Shared settings.
* @type {Object}
*/
this.settings = {};
}
/**
* Convert this config to the compatible object as a config file content.
* @returns {ConfigData} The converted object.
*/
toCompatibleObjectAsConfigFileContent() {
const {
processor: _ignore, // eslint-disable-line no-unused-vars
...config
} = this;
config.parser = config.parser && config.parser.filePath;
config.plugins = Object.keys(config.plugins).filter(Boolean).reverse();
return config;
}
}
module.exports = { ExtractedConfig };
+18
View File
@@ -0,0 +1,18 @@
/**
* @fileoverview `ConfigArray` class.
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const { ConfigArray, getUsedExtractedConfigs } = require("./config-array");
const { ConfigDependency } = require("./config-dependency");
const { ExtractedConfig } = require("./extracted-config");
const { OverrideTester } = require("./override-tester");
module.exports = {
ConfigArray,
ConfigDependency,
ExtractedConfig,
OverrideTester,
getUsedExtractedConfigs
};
@@ -0,0 +1,183 @@
/**
* @fileoverview `OverrideTester` class.
*
* `OverrideTester` class handles `files` property and `excludedFiles` property
* of `overrides` config.
*
* It provides one method.
*
* - `test(filePath)`
* Test if a file path matches the pair of `files` property and
* `excludedFiles` property. The `filePath` argument must be an absolute
* path.
*
* `ConfigArrayFactory` creates `OverrideTester` objects when it processes
* `overrides` properties.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const assert = require("assert");
const path = require("path");
const util = require("util");
const { Minimatch } = require("minimatch");
const minimatchOpts = { dot: true, matchBase: true };
/**
* @typedef {Object} Pattern
* @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
* @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
*/
/**
* Normalize a given pattern to an array.
* @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
* @returns {string[]|null} Normalized patterns.
* @private
*/
function normalizePatterns(patterns) {
if (Array.isArray(patterns)) {
return patterns.filter(Boolean);
}
if (typeof patterns === "string" && patterns) {
return [patterns];
}
return [];
}
/**
* Create the matchers of given patterns.
* @param {string[]} patterns The patterns.
* @returns {InstanceType<Minimatch>[] | null} The matchers.
*/
function toMatcher(patterns) {
if (patterns.length === 0) {
return null;
}
return patterns.map(pattern => new Minimatch(pattern, minimatchOpts));
}
/**
* Convert a given matcher to string.
* @param {Pattern} matchers The matchers.
* @returns {string} The string expression of the matcher.
*/
function patternToJson({ includes, excludes }) {
return {
includes: includes && includes.map(m => m.pattern),
excludes: excludes && excludes.map(m => m.pattern)
};
}
/**
* The class to test given paths are matched by the patterns.
*/
class OverrideTester {
/**
* Create a tester with given criteria.
* If there are no criteria, returns `null`.
* @param {string|string[]} files The glob patterns for included files.
* @param {string|string[]} excludedFiles The glob patterns for excluded files.
* @param {string} basePath The path to the base directory to test paths.
* @returns {OverrideTester|null} The created instance or `null`.
*/
static create(files, excludedFiles, basePath) {
const includePatterns = normalizePatterns(files);
const excludePatterns = normalizePatterns(excludedFiles);
const allPatterns = includePatterns.concat(excludePatterns);
if (allPatterns.length === 0) {
return null;
}
// Rejects absolute paths or relative paths to parents.
for (const pattern of allPatterns) {
if (path.isAbsolute(pattern) || pattern.includes("..")) {
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
}
}
const includes = toMatcher(includePatterns);
const excludes = toMatcher(excludePatterns);
return new OverrideTester([{ includes, excludes }], basePath);
}
/**
* Combine two testers by logical and.
* If either of the testers was `null`, returns the other tester.
* The `basePath` property of the two must be the same value.
* @param {OverrideTester|null} a A tester.
* @param {OverrideTester|null} b Another tester.
* @returns {OverrideTester|null} Combined tester.
*/
static and(a, b) {
if (!b) {
return a;
}
if (!a) {
return b;
}
assert.strictEqual(a.basePath, b.basePath);
return new OverrideTester(a.patterns.concat(b.patterns), a.basePath);
}
/**
* Initialize this instance.
* @param {Pattern[]} patterns The matchers.
* @param {string} basePath The base path.
*/
constructor(patterns, basePath) {
/** @type {Pattern[]} */
this.patterns = patterns;
/** @type {string} */
this.basePath = basePath;
}
/**
* Test if a given path is matched or not.
* @param {string} filePath The absolute path to the target file.
* @returns {boolean} `true` if the path was matched.
*/
test(filePath) {
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
}
const relativePath = path.relative(this.basePath, filePath);
return this.patterns.every(({ includes, excludes }) => (
(!includes || includes.some(m => m.match(relativePath))) &&
(!excludes || !excludes.some(m => m.match(relativePath)))
));
}
/**
* @returns {Object} a JSON compatible object.
*/
toJSON() {
if (this.patterns.length === 1) {
return {
...patternToJson(this.patterns[0]),
basePath: this.basePath
};
}
return {
AND: this.patterns.map(patternToJson),
basePath: this.basePath
};
}
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
return this.toJSON();
}
}
module.exports = { OverrideTester };
+465
View File
@@ -0,0 +1,465 @@
/**
* @fileoverview `FileEnumerator` class.
*
* `FileEnumerator` class has two responsibilities:
*
* 1. Find target files by processing glob patterns.
* 2. Tie each target file and appropriate configuration.
*
* It provies a method:
*
* - `iterateFiles(patterns)`
* Iterate files which are matched by given patterns together with the
* corresponded configuration. This is for `CLIEngine#executeOnFiles()`.
* While iterating files, it loads the configuration file of each directory
* before iterate files on the directory, so we can use the configuration
* files to determine target files.
*
* @example
* const enumerator = new FileEnumerator();
* const linter = new Linter();
*
* for (const { config, filePath } of enumerator.iterateFiles(["*.js"])) {
* const code = fs.readFileSync(filePath, "utf8");
* const messages = linter.verify(code, config, filePath);
*
* console.log(messages);
* }
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const getGlobParent = require("glob-parent");
const isGlob = require("is-glob");
const { escapeRegExp } = require("lodash");
const { Minimatch } = require("minimatch");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const { IgnoredPaths } = require("./ignored-paths");
const debug = require("debug")("eslint:file-enumerator");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const minimatchOpts = { dot: true, matchBase: true };
const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
const NONE = 0;
const IGNORED_SILENTLY = 1;
const IGNORED = 2;
// For VSCode intellisense
/** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
/**
* @typedef {Object} FileEnumeratorOptions
* @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays.
* @property {string} [cwd] The base directory to start lookup.
* @property {string[]} [extensions] The extensions to match files for directory patterns.
* @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {boolean} [ignore] The flag to check ignored files.
* @property {IgnoredPaths} [ignoredPaths] The ignored paths.
* @property {string[]} [rulePaths] The value of `--rulesdir` option.
*/
/**
* @typedef {Object} FileAndConfig
* @property {string} filePath The path to a target file.
* @property {ConfigArray} config The config entries of that file.
* @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified.
*/
/**
* @typedef {Object} FileEntry
* @property {string} filePath The path to a target file.
* @property {ConfigArray} config The config entries of that file.
* @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag.
* - `NONE` means the file is a target file.
* - `IGNORED_SILENTLY` means the file should be ignored silently.
* - `IGNORED` means the file should be ignored and warned because it was directly specified.
*/
/**
* @typedef {Object} FileEnumeratorInternalSlots
* @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays.
* @property {string} cwd The base directory to start lookup.
* @property {RegExp} extensionRegExp The RegExp to test if a string ends with specific file extensions.
* @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {boolean} ignoreFlag The flag to check ignored files.
* @property {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files.
* @property {IgnoredPaths} ignoredPaths The ignored paths.
*/
/** @type {WeakMap<FileEnumerator, FileEnumeratorInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Check if a string is a glob pattern or not.
* @param {string} pattern A glob pattern.
* @returns {boolean} `true` if the string is a glob pattern.
*/
function isGlobPattern(pattern) {
return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern);
}
/**
* Get stats of a given path.
* @param {string} filePath The path to target file.
* @returns {fs.Stats|null} The stats.
* @private
*/
function statSafeSync(filePath) {
try {
return fs.statSync(filePath);
} catch (error) {
/* istanbul ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
}
/**
* Get filenames in a given path to a directory.
* @param {string} directoryPath The path to target directory.
* @returns {string[]} The filenames.
* @private
*/
function readdirSafeSync(directoryPath) {
try {
return fs.readdirSync(directoryPath);
} catch (error) {
/* istanbul ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return [];
}
}
/**
* The error type when no files match a glob.
*/
class NoFilesFoundError extends Error {
/**
* @param {string} pattern - The glob pattern which was not found.
* @param {boolean} globDisabled - If `true` then the pattern was a glob pattern, but glob was disabled.
*/
constructor(pattern, globDisabled) {
super(`No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`);
this.messageTemplate = "file-not-found";
this.messageData = { pattern, globDisabled };
}
}
/**
* The error type when there are files matched by a glob, but all of them have been ignored.
*/
class AllFilesIgnoredError extends Error {
/**
* @param {string} pattern - The glob pattern which was not found.
*/
constructor(pattern) {
super(`All files matched by '${pattern}' are ignored.`);
this.messageTemplate = "all-files-ignored";
this.messageData = { pattern };
}
}
/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
class FileEnumerator {
/**
* Initialize this enumerator.
* @param {FileEnumeratorOptions} options The options.
*/
constructor({
cwd = process.cwd(),
configArrayFactory = new CascadingConfigArrayFactory({ cwd }),
extensions = [".js"],
globInputPaths = true,
ignore = true,
ignoredPaths = new IgnoredPaths({ cwd, ignore })
} = {}) {
internalSlotsMap.set(this, {
configArrayFactory,
cwd,
extensionRegExp: new RegExp(
`.\\.(?:${extensions
.map(ext => escapeRegExp(
ext.startsWith(".")
? ext.slice(1)
: ext
))
.join("|")
})$`,
"u"
),
globInputPaths,
ignoreFlag: ignore,
ignoredPaths,
ignoredPathsWithDotfiles: new IgnoredPaths({
...ignoredPaths.options,
dotfiles: true
})
});
}
/**
* The `RegExp` object that tests if a file path has the allowed file extensions.
* @type {RegExp}
*/
get extensionRegExp() {
return internalSlotsMap.get(this).extensionRegExp;
}
/**
* Iterate files which are matched by given glob patterns.
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
* @returns {IterableIterator<FileAndConfig>} The found files.
*/
*iterateFiles(patternOrPatterns) {
const { globInputPaths } = internalSlotsMap.get(this);
const patterns = Array.isArray(patternOrPatterns)
? patternOrPatterns
: [patternOrPatterns];
debug("Start to iterate files: %o", patterns);
// The set of paths to remove duplicate.
const set = new Set();
for (const pattern of patterns) {
let foundRegardlessOfIgnored = false;
let found = false;
// Skip empty string.
if (!pattern) {
continue;
}
// Iterate files of this pttern.
for (const { config, filePath, flag } of this._iterateFiles(pattern)) {
foundRegardlessOfIgnored = true;
if (flag === IGNORED_SILENTLY) {
continue;
}
found = true;
// Remove duplicate paths while yielding paths.
if (!set.has(filePath)) {
set.add(filePath);
yield {
config,
filePath,
ignored: flag === IGNORED
};
}
}
// Raise an error if any files were not found.
if (!foundRegardlessOfIgnored) {
throw new NoFilesFoundError(
pattern,
!globInputPaths && isGlob(pattern)
);
}
if (!found) {
throw new AllFilesIgnoredError(pattern);
}
}
debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
}
/**
* Iterate files which are matched by a given glob pattern.
* @param {string} pattern The glob pattern to iterate files.
* @returns {IterableIterator<FileEntry>} The found files.
*/
_iterateFiles(pattern) {
const { cwd, globInputPaths } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, pattern);
if (globInputPaths && isGlobPattern(pattern)) {
return this._iterateFilesWithGlob(
absolutePath,
dotfilesPattern.test(pattern)
);
}
const stat = statSafeSync(absolutePath);
if (stat && stat.isDirectory()) {
return this._iterateFilesWithDirectory(
absolutePath,
dotfilesPattern.test(pattern)
);
}
if (stat && stat.isFile()) {
return this._iterateFilesWithFile(absolutePath);
}
return [];
}
/**
* Iterate a file which is matched by a given path.
* @param {string} filePath The path to the target file.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithFile(filePath) {
debug(`File: ${filePath}`);
const { configArrayFactory } = internalSlotsMap.get(this);
const config = configArrayFactory.getConfigArrayForFile(filePath);
const ignored = this._isIgnoredFile(filePath, { direct: true });
const flag = ignored ? IGNORED : NONE;
return [{ config, filePath, flag }];
}
/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithDirectory(directoryPath, dotfiles) {
debug(`Directory: ${directoryPath}`);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive: true, selector: null }
);
}
/**
* Iterate files which are matched by a given glob pattern.
* @param {string} pattern The glob pattern to iterate files.
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithGlob(pattern, dotfiles) {
debug(`Glob: ${pattern}`);
const directoryPath = getGlobParent(pattern);
const globPart = pattern.slice(directoryPath.length + 1);
/*
* recursive if there are `**` or path separators in the glob part.
* Otherwise, patterns such as `src/*.js`, it doesn't need recursive.
*/
const recursive = /\*\*|\/|\\/u.test(globPart);
const selector = new Minimatch(pattern, minimatchOpts);
debug(`recursive? ${recursive}`);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive, selector }
);
}
/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {Object} options The options to iterate files.
* @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
* @param {boolean} [options.recursive] If `true` then it dives into sub directories.
* @param {InstanceType<Minimatch>} [options.selector] The matcher to choose files.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
*_iterateFilesRecursive(directoryPath, options) {
if (this._isIgnoredFile(directoryPath + path.sep, options)) {
return;
}
debug(`Enter the directory: ${directoryPath}`);
const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this);
/** @type {ConfigArray|null} */
let config = null;
// Enumerate the files of this directory.
for (const filename of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, filename);
const stat = statSafeSync(filePath); // TODO: Use `withFileTypes` in the future.
// Check if the file is matched.
if (stat && stat.isFile()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(filePath);
}
const ignored = this._isIgnoredFile(filePath, options);
const flag = ignored ? IGNORED_SILENTLY : NONE;
const matched = options.selector
// Started with a glob pattern; choose by the pattern.
? options.selector.match(filePath)
// Started with a directory path; choose by file extensions.
: extensionRegExp.test(filePath);
if (matched) {
debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`);
yield { config, filePath, flag };
} else {
debug(`Didn't match: ${filename}`);
}
// Dive into the sub directory.
} else if (options.recursive && stat && stat.isDirectory()) {
yield* this._iterateFilesRecursive(filePath, options);
}
}
debug(`Leave the directory: ${directoryPath}`);
}
/**
* Check if a given file should be ignored.
* @param {string} filePath The path to a file to check.
* @param {Object} options Options
* @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default.
* @param {boolean} [options.direct] If `true` then this is a direct specified file.
* @returns {boolean} `true` if the file should be ignored.
* @private
*/
_isIgnoredFile(filePath, { dotfiles = false, direct = false }) {
const {
ignoreFlag,
ignoredPaths,
ignoredPathsWithDotfiles
} = internalSlotsMap.get(this);
const adoptedIgnoredPaths = dotfiles
? ignoredPathsWithDotfiles
: ignoredPaths;
return ignoreFlag
? adoptedIgnoredPaths.contains(filePath)
: (!direct && adoptedIgnoredPaths.contains(filePath, "default"));
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { FileEnumerator };
@@ -4,7 +4,7 @@
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Helper Functions
@@ -4,7 +4,7 @@
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Public Interface
@@ -4,7 +4,7 @@
*/
"use strict";
const xmlEscape = require("../util/xml-escape");
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Helper Functions
File renamed without changes.
@@ -11,8 +11,7 @@
const fs = require("fs"),
path = require("path"),
ignore = require("ignore"),
pathUtils = require("./path-utils");
ignore = require("ignore");
const debug = require("debug")("eslint:ignored-paths");
@@ -168,7 +167,11 @@ class IgnoredPaths {
debug("Using specific ignore file");
try {
fs.statSync(options.ignorePath);
const stat = fs.statSync(options.ignorePath);
if (!stat.isFile()) {
throw new Error(`${options.ignorePath} is not a file`);
}
ignorePath = options.ignorePath;
} catch (e) {
e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`;
@@ -325,57 +328,35 @@ class IgnoredPaths {
* @returns {boolean} true if the file path matches one or more patterns, false otherwise
*/
contains(filepath, category) {
const isDir = filepath.endsWith(path.sep) ||
(path.sep === "\\" && filepath.endsWith("/"));
let result = false;
const basePath = this.getBaseDir();
const absolutePath = path.resolve(this.options.cwd, filepath);
const relativePath = pathUtils.getRelativePath(absolutePath, this.getBaseDir());
let relativePath = path.relative(basePath, absolutePath);
if (typeof category === "undefined") {
result = (this.ig.default.filter([relativePath]).length === 0) ||
(this.ig.custom.filter([relativePath]).length === 0);
} else {
result = (this.ig[category].filter([relativePath]).length === 0);
if (relativePath) {
if (isDir) {
relativePath += path.sep;
}
if (typeof category === "undefined") {
result =
(this.ig.default.filter([relativePath]).length === 0) ||
(this.ig.custom.filter([relativePath]).length === 0);
} else {
result =
(this.ig[category].filter([relativePath]).length === 0);
}
}
debug("contains:");
debug(" target = %j", filepath);
debug(" result = %j", result);
debug(" target = %j", filepath);
debug(" base = %j", basePath);
debug(" relative = %j", relativePath);
debug(" result = %j", result);
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 baseDir = this.getBaseDir();
const ig = ignore();
DEFAULT_IGNORE_DIRS.forEach(ignoreDir => this.addPatternRelativeToCwd(ig, ignoreDir));
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();
return function(absolutePath) {
const relative = pathUtils.getRelativePath(absolutePath, baseDir);
if (!relative) {
return false;
}
return !filter(relative);
};
}
}
module.exports = IgnoredPaths;
module.exports = { IgnoredPaths };
+7
View File
@@ -0,0 +1,7 @@
"use strict";
const { CLIEngine } = require("./cli-engine");
module.exports = {
CLIEngine
};
@@ -8,12 +8,12 @@
// Requirements
//-----------------------------------------------------------------------------
const assert = require("assert"),
fs = require("fs"),
fileEntryCache = require("file-entry-cache"),
hash = require("./hash"),
pkg = require("../../package.json"),
stringify = require("json-stable-stringify-without-jsonify");
const assert = require("assert");
const fs = require("fs");
const fileEntryCache = require("file-entry-cache");
const stringify = require("json-stable-stringify-without-jsonify");
const pkg = require("../../package.json");
const hash = require("./hash");
//-----------------------------------------------------------------------------
// Helpers
@@ -22,14 +22,11 @@ const assert = require("assert"),
const configHashCache = new WeakMap();
/**
* Calculates the hash of the config file used to validate a given file
* @param {Object} configHelper The config helper for retrieving configuration information
* @param {string} filename The path of the file to retrieve a config object for to calculate the hash
* Calculates the hash of the config
* @param {ConfigArray} config The config.
* @returns {string} The hash of the config
*/
function hashOfConfigFor(configHelper, filename) {
const config = configHelper.getConfig(filename);
function hashOfConfigFor(config) {
if (!configHashCache.has(config)) {
configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`));
}
@@ -52,15 +49,12 @@ class LintResultCache {
* Creates a new LintResultCache instance.
* @constructor
* @param {string} cacheFileLocation The cache file location.
* @param {Object} configHelper The configuration helper (used for
* configuration lookup by file path).
*/
constructor(cacheFileLocation, configHelper) {
constructor(cacheFileLocation) {
assert(cacheFileLocation, "Cache file location is required");
assert(configHelper, "Config helper is required");
this.fileEntryCache = fileEntryCache.create(cacheFileLocation);
this.configHelper = configHelper;
}
/**
@@ -68,10 +62,11 @@ class LintResultCache {
* cache. If the file is present and has not been changed, rebuild any
* missing result information.
* @param {string} filePath The file for which to retrieve lint results.
* @param {ConfigArray} config The config of the file.
* @returns {Object|null} The rebuilt lint results, or null if the file is
* changed or not in the filesystem.
*/
getCachedLintResults(filePath) {
getCachedLintResults(filePath, config) {
/*
* Cached lint results are valid if and only if:
@@ -83,7 +78,7 @@ class LintResultCache {
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const hashOfConfig = hashOfConfigFor(this.configHelper, filePath);
const hashOfConfig = hashOfConfigFor(config);
const changed = fileDescriptor.changed || fileDescriptor.meta.hashOfConfig !== hashOfConfig;
if (fileDescriptor.notFound || changed) {
@@ -105,10 +100,11 @@ class LintResultCache {
* applied), to prevent potentially incorrect results if fixes are not
* written to disk.
* @param {string} filePath The file for which to set lint results.
* @param {ConfigArray} config The config of the file.
* @param {Object} result The lint result to be set for the file.
* @returns {void}
*/
setCachedLintResults(filePath, result) {
setCachedLintResults(filePath, config, result) {
if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
return;
}
@@ -130,7 +126,7 @@ class LintResultCache {
}
fileDescriptor.meta.results = resultToSerialize;
fileDescriptor.meta.hashOfConfig = hashOfConfigFor(this.configHelper, result.filePath);
fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
}
}
@@ -22,7 +22,7 @@ const rulesDirCache = {};
* Load all rule modules from specified directory.
* @param {string} relativeRulesDir Path to rules directory, may be relative.
* @param {string} cwd Current working directory
* @returns {Object} Loaded rule modules by rule ids (file names).
* @returns {Object} Loaded rule modules.
*/
module.exports = function(relativeRulesDir, cwd) {
const rulesDir = path.resolve(cwd, relativeRulesDir);
@@ -38,7 +38,7 @@ module.exports = function(relativeRulesDir, cwd) {
if (path.extname(file) !== ".js") {
return;
}
rules[file.slice(0, -3)] = path.join(rulesDir, file);
rules[file.slice(0, -3)] = require(path.join(rulesDir, file));
});
rulesDirCache[rulesDir] = rules;
@@ -3,16 +3,6 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const pathUtils = require("../util/path-utils");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const NAMESPACE_REGEX = /^@.*\//iu;
/**
@@ -30,8 +20,8 @@ function normalizePackageName(name, prefix) {
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (normalizedName.indexOf("\\") > -1) {
normalizedName = pathUtils.convertPathToPosix(normalizedName);
if (normalizedName.includes("\\")) {
normalizedName = normalizedName.replace(/\\/gu, "/");
}
if (normalizedName.charAt(0) === "@") {
@@ -53,7 +43,7 @@ function normalizePackageName(name, prefix) {
*/
normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`);
}
} else if (normalizedName.indexOf(`${prefix}-`) !== 0) {
} else if (!normalizedName.startsWith(`${prefix}-`)) {
normalizedName = `${prefix}-${normalizedName}`;
}
File renamed without changes.
+16 -12
View File
@@ -17,10 +17,10 @@
const fs = require("fs"),
path = require("path"),
options = require("./options"),
CLIEngine = require("./cli-engine"),
mkdirp = require("mkdirp"),
log = require("./util/logging");
{ CLIEngine } = require("./cli-engine"),
options = require("./options"),
log = require("./shared/logging");
const debug = require("debug")("eslint:cli");
@@ -66,7 +66,8 @@ function translateOptions(cliOptions) {
fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true),
fixTypes: cliOptions.fixType,
allowInlineConfig: cliOptions.inlineConfig,
reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives
reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives,
resolvePluginsRelativeTo: cliOptions.resolvePluginsRelativeTo
};
}
@@ -81,24 +82,27 @@ function translateOptions(cliOptions) {
*/
function printResults(engine, results, format, outputFile) {
let formatter;
let rules;
let rulesMeta;
try {
formatter = engine.getFormatter(format);
rules = engine.getRules();
} catch (e) {
log.error(e.message);
return false;
}
const rulesMeta = {};
rules.forEach((rule, ruleId) => {
rulesMeta[ruleId] = rule.meta;
const output = formatter(results, {
get rulesMeta() {
if (!rulesMeta) {
rulesMeta = {};
for (const [ruleId, rule] of engine.getRules()) {
rulesMeta[ruleId] = rule.meta;
}
}
return rulesMeta;
}
});
const output = formatter(results, { rulesMeta });
if (output) {
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
-377
View File
@@ -1,377 +0,0 @@
/**
* @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("./util/file-finder");
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;
}
/**
* Determines if a module is can be resolved.
* @param {string} moduleId The ID (name) of the module
* @returns {boolean} True if it is resolvable; False otherwise.
*/
function isResolvable(moduleId) {
try {
require.resolve(moduleId);
return true;
} catch (err) {
return false;
}
}
//------------------------------------------------------------------------------
// API
//------------------------------------------------------------------------------
/**
* Configuration class
*/
class Config {
/**
* @param {Object} providedOptions Options to be passed in
* @param {Linter} linterContext Linter instance object
*/
constructor(providedOptions, linterContext) {
const options = providedOptions || {};
this.linterContext = linterContext;
this.plugins = new Plugins(linterContext.environments, linterContext.defineRule.bind(linterContext));
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) === "@";
this.specificConfig = ConfigFile.load(
isNamedConfig ? config : path.resolve(this.options.cwd, 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);
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.unshift(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.unshift(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);
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;
-130
View File
@@ -1,130 +0,0 @@
/**
* @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);
}
};
-640
View File
@@ -1,640 +0,0 @@
/**
* @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"),
ModuleResolver = require("../util/module-resolver"),
naming = require("../util/naming"),
pathIsInside = require("path-is-inside"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify-without-jsonify"),
importFresh = require("import-fresh");
const debug = require("debug")("eslint:config-file");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines sort order for object keys for json-stable-stringify
*
* see: https://github.com/samn/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/u, "");
}
/**
* 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|@/u.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}`;
e.messageTemplate = "failed-to-read-json";
e.messageData = {
path: filePath,
message: 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 importFresh(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.
* @throws {Error} If an error occurs linting the config file contents.
* @returns {void}
* @private
*/
function writeJSConfigFile(config, filePath) {
debug(`Writing JS config file: ${filePath}`);
let contentToWrite;
const stringifiedContent = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
try {
const CLIEngine = require("../cli-engine");
const linter = new CLIEngine({
baseConfig: config,
fix: true,
useEslintrc: false
});
const report = linter.executeOnText(stringifiedContent);
contentToWrite = report.results[0].output || stringifiedContent;
} catch (e) {
debug("Error linting JavaScript config file, writing unlinted version");
const errorMessage = e.message;
contentToWrite = stringifiedContent;
e.message = "An error occurred while generating your JavaScript config file. ";
e.message += "A config file was still generated, but the config file itself may not follow your linting rules.";
e.message += `\nError: ${errorMessage}`;
throw e;
} finally {
fs.writeFileSync(filePath, contentToWrite, "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
return configExtends.reduceRight((previousValue, parentPath) => {
try {
let extensionPath;
if (parentPath.startsWith("eslint:")) {
extensionPath = 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.
*/
extensionPath = (path.isAbsolute(parentPath)
? parentPath
: path.join(relativeTo || path.dirname(filePath), parentPath)
);
} else {
extensionPath = parentPath;
}
debug(`Loading ${extensionPath}`);
// eslint-disable-next-line no-use-before-define
return ConfigOps.merge(load(extensionPath, 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);
}
/**
* 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 = naming.normalizePackageName(pluginName, "eslint-plugin");
debug(`Attempting to resolve ${normalizedPackageName}`);
return {
filePath: require.resolve(normalizedPackageName),
configName,
configFullName
};
}
normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");
debug(`Attempting to resolve ${normalizedPackageName}`);
return {
filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)),
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);
}
}
const ruleMap = configContext.linterContext.getRules();
// validate the configuration before continuing
validator.validate(config, ruleMap.get.bind(ruleMap), configContext.linterContext.environments, resolvedPath.configFullName);
/*
* 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;
}
/**
* Checks whether the given filename points to a file
* @param {string} filename A path to a file
* @returns {boolean} `true` if a file exists at the given location
*/
function isExistingFile(filename) {
try {
return fs.statSync(filename).isFile();
} catch (err) {
if (err.code === "ENOENT") {
return false;
}
throw err;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
getBaseDir,
getLookupPath,
load,
loadObject,
resolve,
write,
applyExtends,
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) {
return CONFIG_FILES.map(filename => path.join(directory, filename)).find(isExistingFile) || null;
}
};
-404
View File
@@ -1,404 +0,0 @@
/**
* @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 && [] || {};
if (array) {
const resolvedTarget = 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(resolvedTarget);
}
const resolvedSrc = typeof src === "object" ? src : [src];
Object.keys(resolvedSrc).forEach((_, i) => {
const e = resolvedSrc[i];
if (typeof dst[i] === "undefined") {
dst[i] = e;
} else if (typeof e === "object") {
if (isRule) {
dst[i] = e;
} else {
dst[i] = deepmerge(resolvedTarget[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) {
return module.exports.getRuleSeverity(ruleConfig) === 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));
},
/**
* Normalizes a value for a global in a config
* @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in
* a global directive comment
* @returns {("readable"|"writeable"|"off")} The value normalized as a string
*/
normalizeConfigGlobal(configuredValue) {
switch (configuredValue) {
case "off":
return "off";
case true:
case "true":
case "writeable":
case "writable":
return "writeable";
case null:
case false:
case "false":
case "readable":
case "readonly":
return "readable";
// Fallback to minimize compatibility impact
default:
return "writeable";
}
}
};
-84
View File
@@ -1,84 +0,0 @@
/**
* @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;
-169
View File
@@ -1,169 +0,0 @@
/**
* @fileoverview Plugins manager
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint:plugins");
const naming = require("../util/naming");
const path = require("path");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Plugin class
*/
class Plugins {
/**
* Creates the plugins context
* @param {Environments} envContext - env context
* @param {function(string, Rule): void} defineRule - Callback for when a plugin is defined which introduces rules
*/
constructor(envContext, defineRule) {
this._plugins = Object.create(null);
this._environments = envContext;
this._defineRule = defineRule;
}
/**
* 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 longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const shortName = naming.getShorthandName(longName, "eslint-plugin");
// load up environments and rules
this._plugins[shortName] = plugin;
this._environments.importPlugin(plugin, shortName);
if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleId => {
const qualifiedRuleId = `${shortName}/${ruleId}`,
rule = plugin.rules[ruleId];
this._defineRule(qualifiedRuleId, rule);
});
}
}
/**
* 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 longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const shortName = naming.getShorthandName(longName, "eslint-plugin");
let plugin = null;
if (pluginName.match(/\s+/u)) {
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,
eslintPath: path.resolve(__dirname, "../..")
};
throw missingPluginErr;
}
// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
throw pluginLoadErr;
}
// This step is costly, so skip if debug is disabled
if (debug.enabled) {
const resolvedPath = require.resolve(longName);
let version = null;
try {
version = require(`${longName}/package.json`).version;
} catch (e) {
// Do nothing
}
const loadedPluginAndVersion = version
? `${longName}@${version}`
: `${longName}, version unknown`;
debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`);
}
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;
@@ -10,10 +10,10 @@
//------------------------------------------------------------------------------
const lodash = require("lodash"),
Linter = require("../linter"),
configRule = require("./config-rule"),
ConfigOps = require("./config-ops"),
recConfig = require("../../conf/eslint-recommended");
recConfig = require("../../conf/eslint-recommended"),
ConfigOps = require("../shared/config-ops"),
{ Linter } = require("../linter"),
configRule = require("./config-rule");
const debug = require("debug")("eslint:autoconfig");
const linter = new Linter();
+144
View File
@@ -0,0 +1,144 @@
/**
* @fileoverview Helper to locate and load configuration files.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
stringify = require("json-stable-stringify-without-jsonify");
const debug = require("debug")("eslint:config-file");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines sort order for object keys for json-stable-stringify
*
* see: https://github.com/samn/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
//------------------------------------------------------------------------------
/**
* 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.
* @throws {Error} If an error occurs linting the config file contents.
* @returns {void}
* @private
*/
function writeJSConfigFile(config, filePath) {
debug(`Writing JS config file: ${filePath}`);
let contentToWrite;
const stringifiedContent = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
try {
const { CLIEngine } = require("../cli-engine");
const linter = new CLIEngine({
baseConfig: config,
fix: true,
useEslintrc: false
});
const report = linter.executeOnText(stringifiedContent);
contentToWrite = report.results[0].output || stringifiedContent;
} catch (e) {
debug("Error linting JavaScript config file, writing unlinted version");
const errorMessage = e.message;
contentToWrite = stringifiedContent;
e.message = "An error occurred while generating your JavaScript config file. ";
e.message += "A config file was still generated, but the config file itself may not follow your linting rules.";
e.message += `\nError: ${errorMessage}`;
throw e;
} finally {
fs.writeFileSync(filePath, contentToWrite, "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.");
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
write
};
@@ -11,17 +11,18 @@
//------------------------------------------------------------------------------
const util = require("util"),
path = require("path"),
inquirer = require("inquirer"),
ProgressBar = require("progress"),
semver = require("semver"),
recConfig = require("../../conf/eslint-recommended"),
ConfigOps = require("../shared/config-ops"),
log = require("../shared/logging"),
ModuleResolver = require("../shared/relative-module-resolver"),
autoconfig = require("./autoconfig.js"),
ConfigFile = require("./config-file"),
ConfigOps = require("./config-ops"),
getSourceCodeOfFiles = require("../util/source-code-utils").getSourceCodeOfFiles,
ModuleResolver = require("../util/module-resolver"),
npmUtils = require("../util/npm-utils"),
recConfig = require("../../conf/eslint-recommended"),
log = require("../util/logging");
npmUtils = require("./npm-utils"),
{ getSourceCodeOfFiles } = require("./source-code-utils");
const debug = require("debug")("eslint:config-initializer");
@@ -307,34 +308,13 @@ function processAnswers(answers) {
return config;
}
/**
* process user's style guide of choice and return an appropriate config object.
* @param {string} guide name of the chosen style guide
* @returns {Object} config object
*/
function getConfigForStyleGuide(guide) {
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.");
}
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 eslintPath = ModuleResolver.resolve("eslint", path.join(process.cwd(), "__placeholder__.js"));
const eslint = require(eslintPath);
return eslint.linter.version || null;
@@ -573,7 +553,16 @@ function promptUser() {
earlyAnswers.styleguide = "airbnb-base";
}
const config = ConfigOps.merge(processAnswers(earlyAnswers), getConfigForStyleGuide(earlyAnswers.styleguide));
const config = processAnswers(earlyAnswers);
if (Array.isArray(config.extends)) {
config.extends.push(earlyAnswers.styleguide);
} else if (config.extends) {
config.extends = [config.extends, earlyAnswers.styleguide];
} else {
config.extends = [earlyAnswers.styleguide];
}
const modules = getModulesList(config);
return askInstallModules(modules, earlyAnswers.packageJsonExists)
@@ -617,13 +606,6 @@ function promptUser() {
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, answers);
@@ -631,7 +613,7 @@ function promptUser() {
const config = processAnswers(totalAnswers);
const modules = getModulesList(config);
return askInstallModules(modules).then(() => writeFile(config, answers.format));
return askInstallModules(modules).then(() => writeFile(config, earlyAnswers.format));
});
});
}
@@ -641,7 +623,6 @@ function promptUser() {
//------------------------------------------------------------------------------
const init = {
getConfigForStyleGuide,
getModulesList,
hasESLintVersionConflict,
installModules,
@@ -9,10 +9,7 @@
// Requirements
//------------------------------------------------------------------------------
const Rules = require("../rules"),
builtInRules = require("../built-in-rules-index");
const rules = new Rules();
const builtInRules = require("../rules");
//------------------------------------------------------------------------------
// Helpers
@@ -296,12 +293,17 @@ function generateConfigsFromSchema(schema) {
/**
* Generate possible rule configurations for all of the core rules
* @param {boolean} noDeprecated Indicates whether ignores deprecated rules or not.
* @returns {rulesConfig} Hash of rule names and arrays of possible configurations
*/
function createCoreRuleConfigs() {
return Object.keys(builtInRules).reduce((accumulator, id) => {
const rule = rules.get(id);
function createCoreRuleConfigs(noDeprecated = false) {
return Array.from(builtInRules).reduce((accumulator, [id, rule]) => {
const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated;
if (noDeprecated && isDeprecated) {
return accumulator;
}
accumulator[id] = generateConfigsFromSchema(schema);
return accumulator;
@@ -12,7 +12,7 @@
const fs = require("fs"),
spawn = require("cross-spawn"),
path = require("path"),
log = require("./logging");
log = require("../shared/logging");
//------------------------------------------------------------------------------
// Helpers
@@ -9,9 +9,21 @@
// Requirements
//------------------------------------------------------------------------------
const CLIEngine = require("../cli-engine"),
globUtils = require("./glob-utils"),
baseDefaultOptions = require("../../conf/default-cli-options");
const { CLIEngine } = require("../cli-engine");
/*
* This is used for:
*
* 1. Enumerate target file because we have not expose such a API on `CLIEngine`
* (https://github.com/eslint/eslint/issues/11222).
* 2. Create `SourceCode` instances. Because we don't have any function which
* instantiate `SourceCode` so it needs to take the created `SourceCode`
* instance out after linting.
*
* TODO1: Expose the API that enumerates target files.
* TODO2: Extract the creation logic of `SourceCode` from `Linter` class.
*/
const { getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); // eslint-disable-line no-restricted-modules
const debug = require("debug")("eslint:source-code-utils");
@@ -22,22 +34,23 @@ const debug = require("debug")("eslint:source-code-utils");
/**
* Get the SourceCode object for a single file
* @param {string} filename The fully resolved filename to get SourceCode from.
* @param {Object} options A CLIEngine options object.
* @param {Object} engine A CLIEngine.
* @returns {Array} Array of the SourceCode object representing the file
* and fatal error message.
*/
function getSourceCodeOfFile(filename, options) {
function getSourceCodeOfFile(filename, engine) {
debug("getting sourceCode of", filename);
const opts = Object.assign({}, options, { rules: {} });
const cli = new CLIEngine(opts);
const results = cli.executeOnFiles([filename]);
const results = engine.executeOnFiles([filename]);
if (results && results.results[0] && results.results[0].messages[0] && results.results[0].messages[0].fatal) {
const msg = results.results[0].messages[0];
throw new Error(`(${filename}:${msg.line}:${msg.column}) ${msg.message}`);
}
const sourceCode = cli.linter.getSourceCode();
// TODO: extract the logic that creates source code objects to `SourceCode#parse(text, options)` or something like.
const { linter } = getCLIEngineInternalSlots(engine);
const sourceCode = linter.getSourceCode();
return sourceCode;
}
@@ -56,38 +69,28 @@ function getSourceCodeOfFile(filename, options) {
/**
* Gets the SourceCode of a single file, or set of files.
* @param {string[]|string} patterns A filename, directory name, or glob, or an array of them
* @param {Object} [providedOptions] A CLIEngine options object. If not provided, the default cli options will be used.
* @param {progressCallback} [providedCallback] Callback for reporting execution status
* @param {Object} options A CLIEngine options object. If not provided, the default cli options will be used.
* @param {progressCallback} callback Callback for reporting execution status
* @returns {Object} The SourceCode of all processed files.
*/
function getSourceCodeOfFiles(patterns, providedOptions, providedCallback) {
function getSourceCodeOfFiles(patterns, options, callback) {
const sourceCodes = {};
const globPatternsList = typeof patterns === "string" ? [patterns] : patterns;
let options, callback;
const engine = new CLIEngine({ ...options, rules: {} });
const defaultOptions = Object.assign({}, baseDefaultOptions, { cwd: process.cwd() });
if (typeof providedOptions === "undefined") {
options = defaultOptions;
callback = null;
} else if (typeof providedOptions === "function") {
callback = providedOptions;
options = defaultOptions;
} else if (typeof providedOptions === "object") {
options = Object.assign({}, defaultOptions, providedOptions);
callback = providedCallback;
}
debug("constructed options:", options);
const filenames = globUtils.listFilesToProcess(globPatternsList, options)
.filter(fileInfo => !fileInfo.ignored)
.reduce((files, fileInfo) => files.concat(fileInfo.filename), []);
// TODO: make file iteration as a public API and use it.
const { fileEnumerator } = getCLIEngineInternalSlots(engine);
const filenames =
Array.from(fileEnumerator.iterateFiles(globPatternsList))
.filter(entry => !entry.ignored)
.map(entry => entry.filePath);
if (filenames.length === 0) {
debug(`Did not find any files matching pattern(s): ${globPatternsList}`);
}
filenames.forEach(filename => {
const sourceCode = getSourceCodeOfFile(filename, options);
const sourceCode = getSourceCodeOfFile(filename, engine);
if (sourceCode) {
debug("got sourceCode of", filename);
@@ -97,6 +100,7 @@ function getSourceCodeOfFiles(patterns, providedOptions, providedCallback) {
callback(filenames.length); // eslint-disable-line callback-return
}
});
return sourceCodes;
}
@@ -10,11 +10,11 @@
//------------------------------------------------------------------------------
const assert = require("assert"),
{ breakableTypePattern } = require("../../shared/ast-utils"),
CodePath = require("./code-path"),
CodePathSegment = require("./code-path-segment"),
IdGenerator = require("./id-generator"),
debug = require("./debug-helpers"),
astUtils = require("../util/ast-utils");
debug = require("./debug-helpers");
//------------------------------------------------------------------------------
// Helpers
@@ -41,6 +41,19 @@ function isHandledLogicalOperator(operator) {
return operator === "&&" || operator === "||";
}
/**
* Gets the label if the parent node of a given node is a LabeledStatement.
*
* @param {ASTNode} node - A node to get.
* @returns {string|null} The label or `null`.
*/
function getLabel(node) {
if (node.parent.type === "LabeledStatement") {
return node.parent.label.name;
}
return null;
}
/**
* Checks whether or not a given logical expression node goes different path
* between the `true` case and the `false` case.
@@ -391,7 +404,7 @@ function processCodePathToEnter(analyzer, node) {
case "SwitchStatement":
state.pushSwitchContext(
node.cases.some(isCaseNode),
astUtils.getLabel(node)
getLabel(node)
);
break;
@@ -416,11 +429,11 @@ function processCodePathToEnter(analyzer, node) {
case "ForStatement":
case "ForInStatement":
case "ForOfStatement":
state.pushLoopContext(node.type, astUtils.getLabel(node));
state.pushLoopContext(node.type, getLabel(node));
break;
case "LabeledStatement":
if (!astUtils.isBreakableStatement(node.body)) {
if (!breakableTypePattern.test(node.body.type)) {
state.pushBreakContext(false, node.label.name);
}
break;
@@ -531,7 +544,7 @@ function processCodePathToExit(analyzer, node) {
break;
case "LabeledStatement":
if (!astUtils.isBreakableStatement(node.body)) {
if (!breakableTypePattern.test(node.body.type)) {
state.popBreakContext();
}
break;
@@ -11,7 +11,7 @@
//------------------------------------------------------------------------------
const levn = require("levn"),
ConfigOps = require("../config/config-ops");
ConfigOps = require("../shared/config-ops");
const debug = require("debug")("eslint:config-comment-parser");
+13
View File
@@ -0,0 +1,13 @@
"use strict";
const { Linter } = require("./linter");
const interpolate = require("./interpolate");
const SourceCodeFixer = require("./source-code-fixer");
module.exports = {
Linter,
// For testers.
SourceCodeFixer,
interpolate
};
File renamed without changes.
File diff suppressed because it is too large. Load diff
@@ -206,7 +206,7 @@ class NodeEventGenerator {
* @param {SafeEmitter} emitter
* An SafeEmitter which is the destination of events. This emitter must already
* have registered listeners for all of the events that it needs to listen for.
* (See lib/util/safe-emitter.js for more details on `SafeEmitter`.)
* (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
* @returns {NodeEventGenerator} new instance
*/
constructor(emitter) {
@@ -227,7 +227,7 @@ function createProblem(options) {
/**
* 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, messageIds: Object}} metadata Metadata for the reported problem
* @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean}} metadata Metadata for the reported problem
* @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
* @returns {function(...args): ReportInfo} Function that returns information about the report
*/
@@ -275,7 +275,7 @@ module.exports = function createReportTranslator(metadata) {
message: interpolate(computedMessage, descriptor.data),
messageId: descriptor.messageId,
loc: normalizeReportLoc(descriptor),
fix: normalizeFixes(descriptor, metadata.sourceCode)
fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode)
});
};
};
File renamed without changes.
+77
View File
@@ -0,0 +1,77 @@
/**
* @fileoverview Defines a storage for rules.
* @author Nicholas C. Zakas
* @author aladdin-add
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const builtInRules = require("../rules");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* 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);
}
/**
* 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);
}
/**
* 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 (typeof this._rules[ruleId] === "string") {
this.define(ruleId, require(this._rules[ruleId]));
}
if (this._rules[ruleId]) {
return this._rules[ruleId];
}
if (builtInRules.has(ruleId)) {
return builtInRules.get(ruleId);
}
return null;
}
*[Symbol.iterator]() {
yield* builtInRules;
for (const ruleId of Object.keys(this._rules)) {
yield [ruleId, this.get(ruleId)];
}
}
}
module.exports = Rules;
File renamed without changes.
@@ -48,7 +48,7 @@ const ALIGN = [alignLeft, alignRight, alignRight];
/**
* display the data
* @param {Object} data Data object to be displayed
* @returns {string} modified string
* @returns {void} prints modified string with console.log
* @private
*/
function display(data) {
+5
View File
@@ -64,6 +64,11 @@ module.exports = optionator({
type: "Object",
description: "Specify parser options"
},
{
option: "resolve-plugins-relative-to",
type: "path::String",
description: "A folder where plugins should be resolved from, CWD by default"
},
{
heading: "Specifying rules and plugins"
},
+5
View File
@@ -0,0 +1,5 @@
"use strict";
module.exports = {
RuleTester: require("./rule-tester")
};
@@ -40,15 +40,15 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
const
assert = require("assert"),
path = require("path"),
util = require("util"),
validator = require("../config/config-validator"),
ajv = require("../util/ajv"),
Linter = require("../linter"),
Environments = require("../config/environments"),
SourceCodeFixer = require("../util/source-code-fixer"),
interpolate = require("../util/interpolate");
lodash = require("lodash"),
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"),
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
const ajv = require("../shared/ajv")({ strictDefaults: true });
//------------------------------------------------------------------------------
// Private Members
@@ -305,8 +305,6 @@ class RuleTester {
linter.defineRules(this.rules);
const ruleMap = linter.getRules();
/**
* Run the rule for the given item
* @param {string|Object} item Item to run the rule against
@@ -349,7 +347,7 @@ class RuleTester {
config.rules[ruleName] = 1;
}
const schema = validator.getRuleOptionsSchema(rule);
const schema = getRuleOptionsSchema(rule);
/*
* Setup AST getters.
@@ -365,6 +363,11 @@ class RuleTester {
}
}));
if (typeof config.parser === "string") {
assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths");
linter.defineParser(config.parser, require(config.parser));
}
if (schema) {
ajv.validateSchema(schema);
@@ -377,9 +380,21 @@ class RuleTester {
throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
}
/*
* `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
* and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
* the schema (e.g. using invalid defaults in a schema), and only one of these errors can be reported at a time. As a result,
* the schema is compiled here separately from checking for `validateSchema` errors.
*/
try {
ajv.compile(schema);
} catch (err) {
throw new Error(`Schema for rule ${ruleName} is invalid: ${err.message}`);
}
}
validator.validate(config, ruleMap.get.bind(ruleMap), new Environments(), "rule-tester");
validate(config, "rule-tester", id => (id === ruleName ? rule : null));
return {
messages: linter.verify(code, config, filename, true),
-110
View File
@@ -1,110 +0,0 @@
/**
* @fileoverview Defines a storage for rules.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const ruleReplacements = require("../conf/replacements").rules;
const builtInRules = require("./built-in-rules-index");
//------------------------------------------------------------------------------
// 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);
Object.keys(builtInRules).forEach(ruleId => {
this.define(ruleId, builtInRules[ruleId]);
});
}
/**
* Registers a rule module for rule id in storage.
* @param {string} ruleId Rule id (file name).
* @param {Function} ruleModule Rule handler.
* @returns {void}
*/
define(ruleId, ruleModule) {
this._rules[ruleId] = normalizeRule(ruleModule);
}
/**
* 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;
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -4,7 +4,7 @@
*/
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -11,7 +11,7 @@
const lodash = require("lodash");
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const util = require("../util/ast-utils");
const util = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -90,7 +90,7 @@ module.exports = {
*/
function isAllowed(name) {
return allow.findIndex(
entry => name === entry || name.match(new RegExp(entry)) // eslint-disable-line require-unicode-regexp
entry => name === entry || name.match(new RegExp(entry, "u"))
) !== -1;
}
+3 -3
View File
@@ -8,8 +8,8 @@
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("../util/patterns/letters");
const astUtils = require("../util/ast-utils");
const LETTER_PATTERN = require("./utils/patterns/letters");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
@@ -91,7 +91,7 @@ function createRegExpForIgnorePatterns(normalizedOptions) {
const ignorePatternStr = normalizedOptions[key].ignorePattern;
if (ignorePatternStr) {
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`); // eslint-disable-line require-unicode-regexp
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
normalizedOptions[key].ignorePatternRegExp = regExp;
}
+2 -2
View File
@@ -10,7 +10,7 @@
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
@@ -21,7 +21,7 @@ const DEFAULT_OPTIONS = Object.freeze({
objects: "never",
imports: "never",
exports: "never",
functions: "ignore"
functions: "never"
});
/**
+1 -1
View File
@@ -4,7 +4,7 @@
*/
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+3 -3
View File
@@ -12,7 +12,7 @@
const lodash = require("lodash");
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
@@ -55,7 +55,7 @@ module.exports = {
],
messages: {
complex: "{{name}} has a complexity of {{complexity}}."
complex: "{{name}} has a complexity of {{complexity}}. Maximum allowed is {{max}}."
}
},
@@ -102,7 +102,7 @@ module.exports = {
context.report({
node,
messageId: "complex",
data: { name, complexity }
data: { name, complexity, max: THRESHOLD }
});
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -10,7 +10,7 @@
const lodash = require("lodash");
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
+21 -2
View File
@@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
@@ -103,6 +103,20 @@ module.exports = {
return first.loc.start.line === last.loc.end.line;
}
/**
* Determines if the given node is a lexical declaration (let, const, function, or class)
* @param {ASTNode} node The node to check
* @returns {boolean} True if the node is a lexical declaration
* @private
*/
function isLexicalDeclaration(node) {
if (node.type === "VariableDeclaration") {
return node.kind === "const" || node.kind === "let";
}
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
}
/**
* Checks if the given token is an `else` token or not.
*
@@ -238,8 +252,13 @@ module.exports = {
} else if (multiOrNest) {
if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
const leadingComments = sourceCode.getCommentsBefore(body.body[0]);
const isLexDef = isLexicalDeclaration(body.body[0]);
expected = leadingComments.length > 0;
if (isLexDef) {
expected = true;
} else {
expected = leadingComments.length > 0;
}
} else if (!isOneLiner(body)) {
expected = true;
}
+1 -1
View File
@@ -39,7 +39,7 @@ module.exports = {
create(context) {
const options = context.options[0] || {};
const commentPattern = options.commentPattern
? new RegExp(options.commentPattern) // eslint-disable-line require-unicode-regexp
? new RegExp(options.commentPattern, "u")
: DEFAULT_COMMENT_PATTERN;
const sourceCode = context.getSourceCode();
+1 -1
View File
@@ -5,7 +5,7 @@
"use strict";
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+3 -3
View File
@@ -8,14 +8,14 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/u;
const keywords = require("../util/keywords");
const keywords = require("./utils/keywords");
module.exports = {
meta: {
@@ -61,7 +61,7 @@ module.exports = {
let allowPattern;
if (options.allowPattern) {
allowPattern = new RegExp(options.allowPattern); // eslint-disable-line require-unicode-regexp
allowPattern = new RegExp(options.allowPattern, "u");
}
/**
+1 -1
View File
@@ -9,7 +9,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
+1 -1
View File
@@ -9,7 +9,7 @@
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../util/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
Loaded 100 of 516 files, more files were not shown because too many files have changed in this diff. Show more