build: update npm install command to use --legacy-peer-deps

This change modifies the build scripts to use --legacy-peer-deps flag when installing npm dependencies to resolve potential peer dependency conflicts during the build process. The change is applied to both bash (build.sh) and batch (build.bat) build scripts.
This commit is contained in:
Luiz Costa committed 2025-11-18 10:23:55 -03:00
1 parent 20129a1ac7
commit ac4f232e5e
787 files changed
+82341 -121690

No files matched your search

+117 -167
View File
@@ -1,9 +1,11 @@
[![npm version](https://img.shields.io/npm/v/eslint.svg)](https://www.npmjs.com/package/eslint)
[![Downloads](https://img.shields.io/npm/dm/eslint.svg)](https://www.npmjs.com/package/eslint)
[![Build Status](https://github.com/eslint/eslint/workflows/CI/badge.svg)](https://github.com/eslint/eslint/actions)
<br>
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Feslint%2Feslint?ref=badge_shield)
<br />
[![Open Collective Backers](https://img.shields.io/opencollective/backers/eslint)](https://opencollective.com/eslint)
[![Open Collective Sponsors](https://img.shields.io/opencollective/sponsors/eslint)](https://opencollective.com/eslint)
[![Follow us on Twitter](https://img.shields.io/twitter/follow/geteslint?label=Follow&style=social)](https://twitter.com/intent/user?screen_name=geteslint)
# ESLint
@@ -13,120 +15,109 @@
[Contribute to ESLint](https://eslint.org/docs/latest/contribute) |
[Report Bugs](https://eslint.org/docs/latest/contribute/report-bugs) |
[Code of Conduct](https://eslint.org/conduct) |
[X](https://x.com/geteslint) |
[Twitter](https://twitter.com/geteslint) |
[Discord](https://eslint.org/chat) |
[Mastodon](https://fosstodon.org/@eslint) |
[Bluesky](https://bsky.app/profile/eslint.org)
[Mastodon](https://fosstodon.org/@eslint)
ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. In many ways, it is similar to JSLint and JSHint with a few exceptions:
- ESLint uses [Espree](https://github.com/eslint/js/tree/main/packages/espree) for JavaScript parsing.
- ESLint uses an AST to evaluate patterns in code.
- ESLint is completely pluggable, every single rule is a plugin and you can add more at runtime.
* ESLint uses [Espree](https://github.com/eslint/espree) for JavaScript parsing.
* ESLint uses an AST to evaluate patterns in code.
* ESLint is completely pluggable, every single rule is a plugin and you can add more at runtime.
## Table of Contents
1. [Installation and Usage](#installation-and-usage)
1. [Configuration](#configuration)
1. [Version Support](#version-support)
1. [Code of Conduct](#code-of-conduct)
1. [Filing Issues](#filing-issues)
1. [Frequently Asked Questions](#frequently-asked-questions)
1. [Releases](#releases)
1. [Security Policy](#security-policy)
1. [Semantic Versioning Policy](#semantic-versioning-policy)
1. [License](#license)
1. [Team](#team)
1. [Sponsors](#sponsors)
1. [Technology Sponsors](#technology-sponsors) <!-- markdownlint-disable-line MD051 -->
2. [Configuration](#configuration)
3. [Code of Conduct](#code-of-conduct)
4. [Filing Issues](#filing-issues)
5. [Frequently Asked Questions](#frequently-asked-questions)
6. [Releases](#releases)
7. [Security Policy](#security-policy)
8. [Semantic Versioning Policy](#semantic-versioning-policy)
9. [Stylistic Rule Updates](#stylistic-rule-updates)
10. [License](#license)
11. [Team](#team)
12. [Sponsors](#sponsors)
13. [Technology Sponsors](#technology-sponsors)
## Installation and Usage
Prerequisites: [Node.js](https://nodejs.org/) (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.)
Prerequisites: [Node.js](https://nodejs.org/) (`^12.22.0`, `^14.17.0`, or `>=16.0.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.)
You can install and configure ESLint using this command:
```shell
npm init @eslint/config@latest
npm init @eslint/config
```
After that, you can run ESLint on any file or directory like this:
```shell
npx eslint yourfile.js
./node_modules/.bin/eslint yourfile.js
```
### pnpm Installation
To use ESLint with pnpm, we recommend setting up a `.npmrc` file with at least the following settings:
```text
auto-install-peers=true
node-linker=hoisted
```
This ensures that pnpm installs dependencies in a way that is more compatible with npm and is less likely to produce errors.
## Configuration
You can configure rules in your `eslint.config.js` files as in this example:
After running `npm init @eslint/config`, you'll have an `.eslintrc` file in your directory. In it, you'll see some rules configured like this:
```js
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js", "**/*.cjs", "**/*.mjs"],
rules: {
"prefer-const": "warn",
"no-constant-binary-expression": "error",
},
},
]);
```json
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
```
The names `"prefer-const"` and `"no-constant-binary-expression"` are the names of [rules](https://eslint.org/docs/rules) in ESLint. The first value is the error level of the rule and can be one of these values:
The names `"semi"` and `"quotes"` are the names of [rules](https://eslint.org/docs/rules) in ESLint. The first value is the error level of the rule and can be one of these values:
- `"off"` or `0` - turn the rule off
- `"warn"` or `1` - turn the rule on as a warning (doesn't affect exit code)
- `"error"` or `2` - turn the rule on as an error (exit code will be 1)
* `"off"` or `0` - turn the rule off
* `"warn"` or `1` - turn the rule on as a warning (doesn't affect exit code)
* `"error"` or `2` - turn the rule on as an error (exit code will be 1)
The three error levels allow you fine-grained control over how ESLint applies rules (for more configuration options and details, see the [configuration docs](https://eslint.org/docs/latest/use/configure)).
## Version Support
The ESLint team provides ongoing support for the current version and six months of limited support for the previous version. Limited support includes critical bug fixes, security issues, and compatibility issues only.
ESLint offers commercial support for both current and previous versions through our partners, [Tidelift][tidelift] and [HeroDevs][herodevs].
See [Version Support](https://eslint.org/version-support) for more details.
## Code of Conduct
ESLint adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
ESLint adheres to the [JS Foundation Code of Conduct](https://eslint.org/conduct).
## Filing Issues
Before filing an issue, please be sure to read the guidelines for what you're reporting:
- [Bug Report](https://eslint.org/docs/latest/contribute/report-bugs)
- [Propose a New Rule](https://eslint.org/docs/latest/contribute/propose-new-rule)
- [Proposing a Rule Change](https://eslint.org/docs/latest/contribute/propose-rule-change)
- [Request a Change](https://eslint.org/docs/latest/contribute/request-change)
* [Bug Report](https://eslint.org/docs/latest/contribute/report-bugs)
* [Propose a New Rule](https://eslint.org/docs/latest/contribute/propose-new-rule)
* [Proposing a Rule Change](https://eslint.org/docs/latest/contribute/propose-rule-change)
* [Request a Change](https://eslint.org/docs/latest/contribute/request-change)
## Frequently Asked Questions
### Does ESLint support JSX?
### I'm using JSCS, should I migrate to ESLint?
Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](https://eslint.org/docs/latest/use/configure)). Please note that supporting JSX syntax _is not_ the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics.
Yes. [JSCS has reached end of life](https://eslint.org/blog/2016/07/jscs-end-of-life) and is no longer supported.
We have prepared a [migration guide](https://eslint.org/docs/latest/use/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?
No, ESLint and Prettier have different jobs: ESLint is a linter (looking for problematic patterns) and Prettier is a code formatter. Using both tools is common, refer to [Prettier's documentation](https://prettier.io/docs/en/install#eslint-and-other-linters) to learn how to configure them to work well with each other.
No, ESLint and Prettier have diffent jobs: ESLint is a linter (looking for problematic patterns) and Prettier is a code formatter. Using both tools is common, refer to [Prettier's documentation](https://prettier.io/docs/en/install#eslint-and-other-linters) to learn how to configure them to work well with each other.
### Why can't ESLint find my plugins?
* 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.
* 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?
Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](https://eslint.org/docs/latest/use/configure)). Please note that supporting JSX syntax *is not* the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics.
### What ECMAScript versions does ESLint support?
ESLint has full support for ECMAScript 3, 5, and every year from 2015 up until the most recent stage 4 specification (the default). You can set your desired ECMAScript syntax and other settings (like global variables) through [configuration](https://eslint.org/docs/latest/use/configure).
ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, and 2023. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/latest/use/configure).
### What about experimental features?
@@ -136,18 +127,6 @@ In other cases (including if rules need to warn on more or fewer cases due to ne
Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/latest/contribute). Until then, please use the appropriate parser and plugin(s) for your experimental feature.
### Which Node.js versions does ESLint support?
ESLint updates the supported Node.js versions with each major release of ESLint. At that time, ESLint's supported Node.js versions are updated to be:
1. The most recent maintenance release of Node.js
1. The lowest minor version of the Node.js LTS release that includes the features the ESLint team wants to use.
1. The Node.js Current release
ESLint is also expected to work with Node.js versions released after the Node.js Current release.
Refer to the [Quick Start Guide](https://eslint.org/docs/latest/use/getting-started#prerequisites) for the officially supported Node.js versions for a given ESLint release.
### Where to ask for help?
Open a [discussion](https://github.com/eslint/eslint/discussions) or stop by our [Discord server](https://eslint.org/chat).
@@ -174,58 +153,47 @@ ESLint takes security seriously. We work hard to ensure that ESLint is safe for
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:
- Patch release (intended to not break your lint build)
- A bug fix in a rule that results in ESLint reporting fewer linting errors.
- A bug fix to the CLI or core (including formatters).
- Improvements to documentation.
- Non-user-facing changes such as refactoring code, adding, deleting, or modifying tests, and increasing test coverage.
- Re-releasing after a failed release (i.e., publishing a release that doesn't work for anyone).
- Minor release (might break your lint build)
- A bug fix in a rule that results in ESLint reporting more linting errors.
- A new rule is created.
- A new option to an existing rule that does not result in ESLint reporting more linting errors by default.
- A new addition to an existing rule to support a newly-added language feature (within the last 12 months) that will result in ESLint reporting more linting errors by default.
- An existing rule is deprecated.
- A new CLI capability is created.
- New capabilities to the public API are added (new classes, new methods, new arguments to existing methods, etc.).
- A new formatter is created.
- `eslint:recommended` is updated and will result in strictly fewer linting errors (e.g., rule removals).
- Major release (likely to break your lint build)
- `eslint:recommended` is updated and may result in new linting errors (e.g., rule additions, most rule option updates).
- A new option to an existing rule that results in ESLint reporting more linting errors by default.
- An existing formatter is removed.
- Part of the public API is removed or changed in an incompatible way. The public API includes:
- Rule schemas
- Configuration schema
- Command-line options
- Node.js API
- Rule, formatter, parser, plugin APIs
* Patch release (intended to not break your lint build)
* A bug fix in a rule that results in ESLint reporting fewer linting errors.
* A bug fix to the CLI or core (including formatters).
* Improvements to documentation.
* Non-user-facing changes such as refactoring code, adding, deleting, or modifying tests, and increasing test coverage.
* Re-releasing after a failed release (i.e., publishing a release that doesn't work for anyone).
* Minor release (might break your lint build)
* A bug fix in a rule that results in ESLint reporting more linting errors.
* A new rule is created.
* A new option to an existing rule that does not result in ESLint reporting more linting errors by default.
* A new addition to an existing rule to support a newly-added language feature (within the last 12 months) that will result in ESLint reporting more linting errors by default.
* An existing rule is deprecated.
* A new CLI capability is created.
* New capabilities to the public API are added (new classes, new methods, new arguments to existing methods, etc.).
* A new formatter is created.
* `eslint:recommended` is updated and will result in strictly fewer linting errors (e.g., rule removals).
* Major release (likely to break your lint build)
* `eslint:recommended` is updated and may result in new linting errors (e.g., rule additions, most rule option updates).
* A new option to an existing rule that results in ESLint reporting more linting errors by default.
* An existing formatter is removed.
* Part of the public API is removed or changed in an incompatible way. The public API includes:
* Rule schemas
* Configuration schema
* Command-line options
* Node.js API
* Rule, formatter, parser, plugin APIs
According to our policy, any minor update may report more linting 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.
## Stylistic Rule Updates
Stylistic rules are frozen according to [our policy](https://eslint.org/blog/2020/05/changes-to-rules-policies) on how we evaluate new rules and rule changes.
This means:
* **Bug fixes**: We will still fix bugs in stylistic rules.
* **New ECMAScript features**: We will also make sure stylistic rules are compatible with new ECMAScript features.
* **New options**: We will **not** add any new options to stylistic rules unless an option is the only way to fix a bug or support a newly-added ECMAScript feature.
## License
MIT License
Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
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.
[![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
@@ -245,11 +213,6 @@ The people who manage releases, review feature requests, and meet regularly to e
Nicholas C. Zakas
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/fasttime">
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
Francesco Trotta
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/mdjermanovic">
<img src="https://github.com/mdjermanovic.png?s=75" width="75" height="75" alt="Milos Djermanovic's Avatar"><br />
Milos Djermanovic
@@ -277,35 +240,25 @@ Nitin Kumar
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/DMartens">
<img src="https://github.com/DMartens.png?s=75" width="75" height="75" alt="fnx's Avatar"><br />
fnx
<a href="https://github.com/bmish">
<img src="https://github.com/bmish.png?s=75" width="75" height="75" alt="Bryan Mishkin's Avatar"><br />
Bryan Mishkin
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/JoshuaKGoldberg">
<img src="https://github.com/JoshuaKGoldberg.png?s=75" width="75" height="75" alt="Josh Goldberg ✨'s Avatar"><br />
Josh Goldberg ✨
<a href="https://github.com/fasttime">
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
Francesco Trotta
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/SwetaTanwar">
<img src="https://github.com/SwetaTanwar.png?s=75" width="75" height="75" alt="Sweta Tanwar's Avatar"><br />
Sweta Tanwar
<a href="https://github.com/ota-meshi">
<img src="https://github.com/ota-meshi.png?s=75" width="75" height="75" alt="Yosuke Ota's Avatar"><br />
Yosuke Ota
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/Tanujkanti4441">
<img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
Tanuj Kanti
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/lumirlumir">
<img src="https://github.com/lumirlumir.png?s=75" width="75" height="75" alt="루밀LuMir's Avatar"><br />
루밀LuMir
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/Pixel998">
<img src="https://github.com/Pixel998.png?s=75" width="75" height="75" alt="Pixel998's Avatar"><br />
Pixel998
</a>
</td></tr></tbody></table>
### Website Team
@@ -319,8 +272,8 @@ Amaresh S M
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/harish-sethuraman">
<img src="https://github.com/harish-sethuraman.png?s=75" width="75" height="75" alt="Harish's Avatar"><br />
Harish
<img src="https://github.com/harish-sethuraman.png?s=75" width="75" height="75" alt="Strek's Avatar"><br />
Strek
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/kecrily">
@@ -331,24 +284,21 @@ Percy Ma
<!--teamend-->
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
<!--sponsorsstart-->
## Sponsors
The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
to get your logo on our READMEs and [website](https://eslint.org/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>Platinum Sponsors</h3>
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
<h3>Technology Sponsors</h3>
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
<p><a href="#"><img src="https://images.opencollective.com/2021-frameworks-fund/logo.png" alt="Chrome Frameworks Fund" height="undefined"></a> <a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
<p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p>
<!--sponsorsend-->
[tidelift]: https://tidelift.com/funding/github/npm/eslint
[herodevs]: https://www.herodevs.com/support/eslint-nes?utm_source=ESLintWebsite&utm_medium=ESLintWebsite&utm_campaign=ESLintNES&utm_id=ESLintNES
## Technology Sponsors
* Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com)
* Hosting for ([eslint.org](https://eslint.org)) is sponsored by [Netlify](https://www.netlify.com)
* Password management is sponsored by [1Password](https://www.1password.com)
+77 -100
View File
@@ -9,14 +9,9 @@
"use strict";
const mod = require("node:module");
// to use V8's code cache to speed up instantiation time
mod.enableCompileCache?.();
// must do this initialization *before* other requires in order to work
if (process.argv.includes("--debug")) {
require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}
//------------------------------------------------------------------------------
@@ -45,20 +40,20 @@ if (process.argv.includes("--debug")) {
* @returns {Promise<string>} The read text.
*/
function readStdin() {
return new Promise((resolve, reject) => {
let content = "";
let chunk = "";
return new Promise((resolve, reject) => {
let content = "";
let chunk = "";
process.stdin
.setEncoding("utf8")
.on("readable", () => {
while ((chunk = process.stdin.read()) !== null) {
content += chunk;
}
})
.on("end", () => resolve(content))
.on("error", reject);
});
process.stdin
.setEncoding("utf8")
.on("readable", () => {
while ((chunk = process.stdin.read()) !== null) {
content += chunk;
}
})
.on("end", () => resolve(content))
.on("error", reject);
});
}
/**
@@ -67,32 +62,34 @@ function readStdin() {
* @returns {string} The error message.
*/
function getErrorMessage(error) {
// Lazy loading because this is used only if an error happened.
const util = require("node:util");
// Foolproof -- third-party module might throw non-object.
if (typeof error !== "object" || error === null) {
return String(error);
}
// Lazy loading because this is used only if an error happened.
const util = require("util");
// Use templates if `error.messageTemplate` is present.
if (typeof error.messageTemplate === "string") {
try {
const template = require(`../messages/${error.messageTemplate}.js`);
// Foolproof -- third-party module might throw non-object.
if (typeof error !== "object" || error === null) {
return String(error);
}
return template(error.messageData || {});
} catch {
// Ignore template error then fallback to use `error.stack`.
}
}
// Use templates if `error.messageTemplate` is present.
if (typeof error.messageTemplate === "string") {
try {
const template = require(`../messages/${error.messageTemplate}.js`);
// Use the stacktrace if it's an error object.
if (typeof error.stack === "string") {
return error.stack;
}
return template(error.messageData || {});
} catch {
// Otherwise, dump the object.
return util.format("%o", error);
// Ignore template error then fallback to use `error.stack`.
}
}
// Use the stacktrace if it's an error object.
if (typeof error.stack === "string") {
return error.stack;
}
// Otherwise, dump the object.
return util.format("%o", error);
}
/**
@@ -114,21 +111,21 @@ let hadFatalError = false;
* @returns {void}
*/
function onFatalError(error) {
process.exitCode = 2;
hadFatalError = true;
process.exitCode = 2;
hadFatalError = true;
const { version } = require("../package.json");
const message = `
const { version } = require("../package.json");
const message = `
Oops! Something went wrong! :(
ESLint: ${version}
${getErrorMessage(error)}`;
if (!displayedErrors.has(message)) {
console.error(message);
displayedErrors.add(message);
}
if (!displayedErrors.has(message)) {
console.error(message);
displayedErrors.add(message);
}
}
//------------------------------------------------------------------------------
@@ -136,61 +133,41 @@ ${getErrorMessage(error)}`;
//------------------------------------------------------------------------------
(async function main() {
process.on("uncaughtException", onFatalError);
process.on("unhandledRejection", onFatalError);
process.on("uncaughtException", onFatalError);
process.on("unhandledRejection", onFatalError);
// Call the config initializer if `--init` is present.
if (process.argv.includes("--init")) {
// `eslint --init` has been moved to `@eslint/create-config`
console.warn(
"You can also run this command directly using 'npm init @eslint/config@latest'.",
);
// Call the config initializer if `--init` is present.
if (process.argv.includes("--init")) {
const spawn = require("cross-spawn");
// `eslint --init` has been moved to `@eslint/create-config`
console.warn("You can also run this command directly using 'npm init @eslint/config'.");
spawn.sync("npm", ["init", "@eslint/config@latest"], {
encoding: "utf8",
stdio: "inherit",
});
return;
}
const spawn = require("cross-spawn");
// start the MCP server if `--mcp` is present
if (process.argv.includes("--mcp")) {
console.warn(
"You can also run this command directly using 'npx @eslint/mcp@latest'.",
);
spawn.sync("npm", ["init", "@eslint/config"], { encoding: "utf8", stdio: "inherit" });
return;
}
const spawn = require("cross-spawn");
// Otherwise, call the CLI.
const exitCode = await require("../lib/cli").execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null,
true
);
spawn.sync("npx", ["@eslint/mcp@latest"], {
encoding: "utf8",
stdio: "inherit",
});
return;
}
// Otherwise, call the CLI.
const cli = require("../lib/cli");
const exitCode = await cli.execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null,
true,
);
/*
* If an uncaught exception or unhandled rejection was detected in the meantime,
* keep the fatal exit code 2 that is already assigned to `process.exitCode`.
* Without this condition, exit code 2 (unsuccessful execution) could be overwritten with
* 1 (successful execution, lint problems found) or even 0 (successful execution, no lint problems found).
* This ensures that unexpected errors that seemingly don't affect the success
* of the execution will still cause a non-zero exit code, as it's a common
* practice and the default behavior of Node.js to exit with non-zero
* in case of an uncaught exception or unhandled rejection.
*
* Otherwise, assign the exit code returned from CLI.
*/
if (!hadFatalError) {
process.exitCode = exitCode;
}
})().catch(onFatalError);
/*
* If an uncaught exception or unhandled rejection was detected in the meantime,
* keep the fatal exit code 2 that is already assigned to `process.exitCode`.
* Without this condition, exit code 2 (unsuccessful execution) could be overwritten with
* 1 (successful execution, lint problems found) or even 0 (successful execution, no lint problems found).
* This ensures that unexpected errors that seemingly don't affect the success
* of the execution will still cause a non-zero exit code, as it's a common
* practice and the default behavior of Node.js to exit with non-zero
* in case of an uncaught exception or unhandled rejection.
*
* Otherwise, assign the exit code returned from CLI.
*/
if (!hadFatalError) {
process.exitCode = exitCode;
}
}()).catch(onFatalError);
+93
View File
@@ -0,0 +1,93 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview Defines a schema for configs.
* @author Sylvan Mably
*/
"use strict";
const baseConfigProperties = {
$schema: { type: "string" },
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" },
noInlineConfig: { type: "boolean" },
reportUnusedDisableDirectives: { type: "boolean" },
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
};
const configSchema = {
definitions: {
stringOrStrings: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false
}
]
},
stringOrStringsRequired: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false,
minItems: 1
}
]
},
// Config at top-level.
objectConfig: {
type: "object",
properties: {
root: { type: "boolean" },
ignorePatterns: { $ref: "#/definitions/stringOrStrings" },
...baseConfigProperties
},
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;
+22 -22
View File
@@ -6,27 +6,27 @@
"use strict";
module.exports = {
configFile: null,
baseConfig: false,
rulePaths: [],
useEslintrc: true,
envs: [],
globals: [],
extensions: null,
ignore: true,
ignorePath: void 0,
cache: false,
configFile: null,
baseConfig: false,
rulePaths: [],
useEslintrc: true,
envs: [],
globals: [],
extensions: null,
ignore: true,
ignorePath: void 0,
cache: false,
/*
* in order to honor the cacheFile option if specified
* this option should not have a default value otherwise
* it will always be used
*/
cacheLocation: "",
cacheFile: ".eslintcache",
cacheStrategy: "metadata",
fix: false,
allowInlineConfig: true,
reportUnusedDisableDirectives: void 0,
globInputPaths: true,
/*
* in order to honor the cacheFile option if specified
* this option should not have a default value otherwise
* it will always be used
*/
cacheLocation: "",
cacheFile: ".eslintcache",
cacheStrategy: "metadata",
fix: false,
allowInlineConfig: true,
reportUnusedDisableDirectives: void 0,
globInputPaths: true
};
-16
View File
@@ -1,16 +0,0 @@
/**
* @fileoverview Configuration related to ECMAScript versions
* @author Milos Djermanovic
*/
"use strict";
/**
* The latest ECMAScript version supported by ESLint.
* @type {number} year-based ECMAScript version
*/
const LATEST_ECMA_VERSION = 2026;
module.exports = {
LATEST_ECMA_VERSION,
};
+94 -109
View File
@@ -10,160 +10,145 @@
//-----------------------------------------------------------------------------
const commonjs = {
exports: true,
global: false,
module: false,
require: false,
exports: true,
global: false,
module: false,
require: false
};
const es3 = {
Array: false,
Boolean: false,
constructor: false,
Date: false,
decodeURI: false,
decodeURIComponent: false,
encodeURI: false,
encodeURIComponent: false,
Error: false,
escape: false,
eval: false,
EvalError: false,
Function: false,
hasOwnProperty: false,
Infinity: false,
isFinite: false,
isNaN: false,
isPrototypeOf: false,
Math: false,
NaN: false,
Number: false,
Object: false,
parseFloat: false,
parseInt: false,
propertyIsEnumerable: false,
RangeError: false,
ReferenceError: false,
RegExp: false,
String: false,
SyntaxError: false,
toLocaleString: false,
toString: false,
TypeError: false,
undefined: false,
unescape: false,
URIError: false,
valueOf: false,
Array: false,
Boolean: false,
constructor: false,
Date: false,
decodeURI: false,
decodeURIComponent: false,
encodeURI: false,
encodeURIComponent: false,
Error: false,
escape: false,
eval: false,
EvalError: false,
Function: false,
hasOwnProperty: false,
Infinity: false,
isFinite: false,
isNaN: false,
isPrototypeOf: false,
Math: false,
NaN: false,
Number: false,
Object: false,
parseFloat: false,
parseInt: false,
propertyIsEnumerable: false,
RangeError: false,
ReferenceError: false,
RegExp: false,
String: false,
SyntaxError: false,
toLocaleString: false,
toString: false,
TypeError: false,
undefined: false,
unescape: false,
URIError: false,
valueOf: false
};
const es5 = {
...es3,
JSON: false,
...es3,
JSON: false
};
const es2015 = {
...es5,
ArrayBuffer: false,
DataView: false,
Float32Array: false,
Float64Array: false,
Int16Array: false,
Int32Array: false,
Int8Array: false,
Intl: false,
Map: false,
Promise: false,
Proxy: false,
Reflect: false,
Set: false,
Symbol: false,
Uint16Array: false,
Uint32Array: false,
Uint8Array: false,
Uint8ClampedArray: false,
WeakMap: false,
WeakSet: false,
...es5,
ArrayBuffer: false,
DataView: false,
Float32Array: false,
Float64Array: false,
Int16Array: false,
Int32Array: false,
Int8Array: false,
Map: false,
Promise: false,
Proxy: false,
Reflect: false,
Set: false,
Symbol: false,
Uint16Array: false,
Uint32Array: false,
Uint8Array: false,
Uint8ClampedArray: false,
WeakMap: false,
WeakSet: false
};
// no new globals in ES2016
const es2016 = {
...es2015,
...es2015
};
const es2017 = {
...es2016,
Atomics: false,
SharedArrayBuffer: false,
...es2016,
Atomics: false,
SharedArrayBuffer: false
};
// no new globals in ES2018
const es2018 = {
...es2017,
...es2017
};
// no new globals in ES2019
const es2019 = {
...es2018,
...es2018
};
const es2020 = {
...es2019,
BigInt: false,
BigInt64Array: false,
BigUint64Array: false,
globalThis: false,
...es2019,
BigInt: false,
BigInt64Array: false,
BigUint64Array: false,
globalThis: false
};
const es2021 = {
...es2020,
AggregateError: false,
FinalizationRegistry: false,
WeakRef: false,
...es2020,
AggregateError: false,
FinalizationRegistry: false,
WeakRef: false
};
const es2022 = {
...es2021,
...es2021
};
const es2023 = {
...es2022,
...es2022
};
const es2024 = {
...es2023,
...es2023
};
const es2025 = {
...es2024,
Float16Array: false,
Iterator: false,
};
const es2026 = {
...es2025,
AsyncDisposableStack: false,
DisposableStack: false,
SuppressedError: false,
};
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
module.exports = {
commonjs,
es3,
es5,
es2015,
es2016,
es2017,
es2018,
es2019,
es2020,
es2021,
es2022,
es2023,
es2024,
es2025,
es2026,
commonjs,
es3,
es5,
es2015,
es2016,
es2017,
es2018,
es2019,
es2020,
es2021,
es2022,
es2023,
es2024
};
+20 -24
View File
@@ -1,26 +1,22 @@
{
"rules": {
"generator-star": ["generator-star-spacing"],
"global-strict": ["strict"],
"no-arrow-condition": ["no-confusing-arrow", "no-constant-condition"],
"no-comma-dangle": ["comma-dangle"],
"no-empty-class": ["no-empty-character-class"],
"no-empty-label": ["no-labels"],
"no-extra-strict": ["strict"],
"no-reserved-keys": ["quote-props"],
"no-space-before-semi": ["semi-spacing"],
"no-wrap-func": ["no-extra-parens"],
"space-after-function-name": ["space-before-function-paren"],
"space-after-keywords": ["keyword-spacing"],
"space-before-function-parentheses": ["space-before-function-paren"],
"space-before-keywords": ["keyword-spacing"],
"space-in-brackets": [
"object-curly-spacing",
"array-bracket-spacing",
"computed-property-spacing"
],
"space-return-throw-case": ["keyword-spacing"],
"space-unary-word-ops": ["space-unary-ops"],
"spaced-line-comment": ["spaced-comment"]
}
"rules": {
"generator-star": ["generator-star-spacing"],
"global-strict": ["strict"],
"no-arrow-condition": ["no-confusing-arrow", "no-constant-condition"],
"no-comma-dangle": ["comma-dangle"],
"no-empty-class": ["no-empty-character-class"],
"no-empty-label": ["no-labels"],
"no-extra-strict": ["strict"],
"no-reserved-keys": ["quote-props"],
"no-space-before-semi": ["semi-spacing"],
"no-wrap-func": ["no-extra-parens"],
"space-after-function-name": ["space-before-function-paren"],
"space-after-keywords": ["keyword-spacing"],
"space-before-function-parentheses": ["space-before-function-paren"],
"space-before-keywords": ["keyword-spacing"],
"space-in-brackets": ["object-curly-spacing", "array-bracket-spacing", "computed-property-spacing"],
"space-return-throw-case": ["keyword-spacing"],
"space-unary-word-ops": ["space-unary-ops"],
"spaced-line-comment": ["spaced-comment"]
}
}
+25 -88
View File
@@ -1,91 +1,28 @@
{
"types": {
"problem": [],
"suggestion": [],
"layout": []
},
"deprecated": [],
"removed": [
{
"removed": "generator-star",
"replacedBy": [{ "rule": { "name": "generator-star-spacing" } }]
"types": {
"problem": [],
"suggestion": [],
"layout": []
},
{
"removed": "global-strict",
"replacedBy": [{ "rule": { "name": "strict" } }]
},
{
"removed": "no-arrow-condition",
"replacedBy": [
{ "rule": { "name": "no-confusing-arrow" } },
{ "rule": { "name": "no-constant-condition" } }
]
},
{
"removed": "no-comma-dangle",
"replacedBy": [{ "rule": { "name": "comma-dangle" } }]
},
{
"removed": "no-empty-class",
"replacedBy": [{ "rule": { "name": "no-empty-character-class" } }]
},
{
"removed": "no-empty-label",
"replacedBy": [{ "rule": { "name": "no-labels" } }]
},
{
"removed": "no-extra-strict",
"replacedBy": [{ "rule": { "name": "strict" } }]
},
{
"removed": "no-reserved-keys",
"replacedBy": [{ "rule": { "name": "quote-props" } }]
},
{
"removed": "no-space-before-semi",
"replacedBy": [{ "rule": { "name": "semi-spacing" } }]
},
{
"removed": "no-wrap-func",
"replacedBy": [{ "rule": { "name": "no-extra-parens" } }]
},
{
"removed": "space-after-function-name",
"replacedBy": [{ "rule": { "name": "space-before-function-paren" } }]
},
{
"removed": "space-after-keywords",
"replacedBy": [{ "rule": { "name": "keyword-spacing" } }]
},
{
"removed": "space-before-function-parentheses",
"replacedBy": [{ "rule": { "name": "space-before-function-paren" } }]
},
{
"removed": "space-before-keywords",
"replacedBy": [{ "rule": { "name": "keyword-spacing" } }]
},
{
"removed": "space-in-brackets",
"replacedBy": [
{ "rule": { "name": "object-curly-spacing" } },
{ "rule": { "name": "array-bracket-spacing" } },
{ "rule": { "name": "computed-property-spacing" } }
]
},
{
"removed": "space-return-throw-case",
"replacedBy": [{ "rule": { "name": "keyword-spacing" } }]
},
{
"removed": "space-unary-word-ops",
"replacedBy": [{ "rule": { "name": "space-unary-ops" } }]
},
{
"removed": "spaced-line-comment",
"replacedBy": [{ "rule": { "name": "spaced-comment" } }]
},
{ "removed": "valid-jsdoc", "replacedBy": [] },
{ "removed": "require-jsdoc", "replacedBy": [] }
]
"deprecated": [],
"removed": [
{ "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
{ "removed": "global-strict", "replacedBy": ["strict"] },
{ "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
{ "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
{ "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
{ "removed": "no-empty-label", "replacedBy": ["no-labels"] },
{ "removed": "no-extra-strict", "replacedBy": ["strict"] },
{ "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
{ "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
{ "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
{ "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
{ "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
{ "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
{ "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
]
}
+20 -16
View File
@@ -9,11 +9,11 @@
// Requirements
//-----------------------------------------------------------------------------
const { ESLint, shouldUseFlatConfig } = require("./eslint/eslint");
const { LegacyESLint } = require("./eslint/legacy-eslint");
const { ESLint, FlatESLint } = require("./eslint");
const { shouldUseFlatConfig } = require("./eslint/flat-eslint");
const { Linter } = require("./linter");
const { RuleTester } = require("./rule-tester");
const { SourceCode } = require("./languages/js/source-code");
const { SourceCode } = require("./source-code");
//-----------------------------------------------------------------------------
// Functions
@@ -23,18 +23,22 @@ const { SourceCode } = require("./languages/js/source-code");
* Loads the correct ESLint constructor given the options.
* @param {Object} [options] The options object
* @param {boolean} [options.useFlatConfig] Whether or not to use a flat config
* @param {string} [options.cwd] The current working directory
* @returns {Promise<ESLint|LegacyESLint>} The ESLint constructor
*/
async function loadESLint({ useFlatConfig } = {}) {
/*
* Note: The v8.x version of this function also accepted a `cwd` option, but
* it is not used in this implementation so we silently ignore it.
*/
async function loadESLint({ useFlatConfig, cwd = process.cwd() } = {}) {
const shouldESLintUseFlatConfig =
useFlatConfig ?? (await shouldUseFlatConfig());
/*
* Note: The v9.x version of this function doesn't have a cwd option
* because it's not used. It's only used in the v8.x version of this
* function.
*/
return shouldESLintUseFlatConfig ? ESLint : LegacyESLint;
const shouldESLintUseFlatConfig = typeof useFlatConfig === "boolean"
? useFlatConfig
: await shouldUseFlatConfig({ cwd });
return shouldESLintUseFlatConfig ? FlatESLint : ESLint;
}
//-----------------------------------------------------------------------------
@@ -42,9 +46,9 @@ async function loadESLint({ useFlatConfig } = {}) {
//-----------------------------------------------------------------------------
module.exports = {
Linter,
loadESLint,
ESLint,
RuleTester,
SourceCode,
Linter,
loadESLint,
ESLint,
RuleTester,
SourceCode
};
File diff suppressed because it is too large. Load diff
+347 -341
View File
@@ -34,15 +34,18 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("node:fs");
const path = require("node:path");
const fs = require("fs");
const path = require("path");
const getGlobParent = require("glob-parent");
const isGlob = require("is-glob");
const escapeRegExp = require("escape-string-regexp");
const { Minimatch } = require("minimatch");
const {
Legacy: { IgnorePattern, CascadingConfigArrayFactory },
Legacy: {
IgnorePattern,
CascadingConfigArrayFactory
}
} = require("@eslint/eslintrc");
const debug = require("debug")("eslint:file-enumerator");
@@ -51,7 +54,7 @@ const debug = require("debug")("eslint:file-enumerator");
//------------------------------------------------------------------------------
const minimatchOpts = { dot: true, matchBase: true };
const dotfilesPattern = /(?:^\.|[/\\]\.)[^/\\.].*/u;
const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
const NONE = 0;
const IGNORED_SILENTLY = 1;
const IGNORED = 2;
@@ -105,7 +108,7 @@ const internalSlotsMap = new WeakMap();
* @returns {boolean} `true` if the string is a glob pattern.
*/
function isGlobPattern(pattern) {
return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern);
return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern);
}
/**
@@ -116,15 +119,16 @@ function isGlobPattern(pattern) {
* @private
*/
function statSafeSync(filePath) {
try {
return fs.statSync(filePath);
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
try {
return fs.statSync(filePath);
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
}
/**
@@ -135,15 +139,16 @@ function statSafeSync(filePath) {
* @private
*/
function readdirSafeSync(directoryPath) {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true });
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return [];
}
try {
return fs.readdirSync(directoryPath, { withFileTypes: true });
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return [];
}
}
/**
@@ -152,45 +157,50 @@ function readdirSafeSync(directoryPath) {
* @returns {RegExp | null} The created `RegExp` object or null.
*/
function createExtensionRegExp(extensions) {
if (extensions) {
const normalizedExts = extensions.map(ext =>
escapeRegExp(ext.startsWith(".") ? ext.slice(1) : ext),
);
if (extensions) {
const normalizedExts = extensions.map(ext => escapeRegExp(
ext.startsWith(".")
? ext.slice(1)
: ext
));
return new RegExp(`.\\.(?:${normalizedExts.join("|")})$`, "u");
}
return null;
return new RegExp(
`.\\.(?:${normalizedExts.join("|")})$`,
"u"
);
}
return null;
}
/**
* 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 };
}
/**
* @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 };
}
/**
* @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 };
}
}
/**
@@ -198,340 +208,336 @@ class AllFilesIgnoredError extends Error {
* 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,
getEslintRecommendedConfig: () =>
require("@eslint/js").configs.recommended,
getEslintAllConfig: () => require("@eslint/js").configs.all,
}),
extensions = null,
globInputPaths = true,
errorOnUnmatchedPattern = true,
ignore = true,
} = {}) {
internalSlotsMap.set(this, {
configArrayFactory,
cwd,
defaultIgnores: IgnorePattern.createDefaultIgnore(cwd),
extensionRegExp: createExtensionRegExp(extensions),
globInputPaths,
errorOnUnmatchedPattern,
ignoreFlag: ignore,
});
}
/**
* Check if a given file is target or not.
* @param {string} filePath The path to a candidate file.
* @param {ConfigArray} [providedConfig] Optional. The configuration for the file.
* @returns {boolean} `true` if the file is a target.
*/
isTargetPath(filePath, providedConfig) {
const { configArrayFactory, extensionRegExp } =
internalSlotsMap.get(this);
/**
* Initialize this enumerator.
* @param {FileEnumeratorOptions} options The options.
*/
constructor({
cwd = process.cwd(),
configArrayFactory = new CascadingConfigArrayFactory({
cwd,
getEslintRecommendedConfig: () => require("@eslint/js").configs.recommended,
getEslintAllConfig: () => require("@eslint/js").configs.all
}),
extensions = null,
globInputPaths = true,
errorOnUnmatchedPattern = true,
ignore = true
} = {}) {
internalSlotsMap.set(this, {
configArrayFactory,
cwd,
defaultIgnores: IgnorePattern.createDefaultIgnore(cwd),
extensionRegExp: createExtensionRegExp(extensions),
globInputPaths,
errorOnUnmatchedPattern,
ignoreFlag: ignore
});
}
// If `--ext` option is present, use it.
if (extensionRegExp) {
return extensionRegExp.test(filePath);
}
/**
* Check if a given file is target or not.
* @param {string} filePath The path to a candidate file.
* @param {ConfigArray} [providedConfig] Optional. The configuration for the file.
* @returns {boolean} `true` if the file is a target.
*/
isTargetPath(filePath, providedConfig) {
const {
configArrayFactory,
extensionRegExp
} = internalSlotsMap.get(this);
// `.js` file is target by default.
if (filePath.endsWith(".js")) {
return true;
}
// If `--ext` option is present, use it.
if (extensionRegExp) {
return extensionRegExp.test(filePath);
}
// use `overrides[].files` to check additional targets.
const config =
providedConfig ||
configArrayFactory.getConfigArrayForFile(filePath, {
ignoreNotFoundError: true,
});
// `.js` file is target by default.
if (filePath.endsWith(".js")) {
return true;
}
return config.isAdditionalTargetPath(filePath);
}
// use `overrides[].files` to check additional targets.
const config =
providedConfig ||
configArrayFactory.getConfigArrayForFile(
filePath,
{ ignoreNotFoundError: true }
);
/**
* Iterate files which are matched by given glob patterns.
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
* @throws {NoFilesFoundError|AllFilesIgnoredError} On an unmatched pattern.
* @returns {IterableIterator<FileAndConfig>} The found files.
*/
*iterateFiles(patternOrPatterns) {
const { globInputPaths, errorOnUnmatchedPattern } =
internalSlotsMap.get(this);
const patterns = Array.isArray(patternOrPatterns)
? patternOrPatterns
: [patternOrPatterns];
return config.isAdditionalTargetPath(filePath);
}
debug("Start to iterate files: %o", patterns);
/**
* Iterate files which are matched by given glob patterns.
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
* @throws {NoFilesFoundError|AllFilesIgnoredError} On an unmatched pattern.
* @returns {IterableIterator<FileAndConfig>} The found files.
*/
*iterateFiles(patternOrPatterns) {
const { globInputPaths, errorOnUnmatchedPattern } = internalSlotsMap.get(this);
const patterns = Array.isArray(patternOrPatterns)
? patternOrPatterns
: [patternOrPatterns];
// The set of paths to remove duplicate.
const set = new Set();
debug("Start to iterate files: %o", patterns);
for (const pattern of patterns) {
let foundRegardlessOfIgnored = false;
let found = false;
// The set of paths to remove duplicate.
const set = new Set();
// Skip empty string.
if (!pattern) {
continue;
}
for (const pattern of patterns) {
let foundRegardlessOfIgnored = false;
let found = false;
// Iterate files of this pattern.
for (const { config, filePath, flag } of this._iterateFiles(
pattern,
)) {
foundRegardlessOfIgnored = true;
if (flag === IGNORED_SILENTLY) {
continue;
}
found = true;
// Skip empty string.
if (!pattern) {
continue;
}
// Remove duplicate paths while yielding paths.
if (!set.has(filePath)) {
set.add(filePath);
yield {
config,
filePath,
ignored: flag === IGNORED,
};
}
}
// Iterate files of this pattern.
for (const { config, filePath, flag } of this._iterateFiles(pattern)) {
foundRegardlessOfIgnored = true;
if (flag === IGNORED_SILENTLY) {
continue;
}
found = true;
// Raise an error if any files were not found.
if (errorOnUnmatchedPattern) {
if (!foundRegardlessOfIgnored) {
throw new NoFilesFoundError(
pattern,
!globInputPaths && isGlob(pattern),
);
}
if (!found) {
throw new AllFilesIgnoredError(pattern);
}
}
}
// Remove duplicate paths while yielding paths.
if (!set.has(filePath)) {
set.add(filePath);
yield {
config,
filePath,
ignored: flag === IGNORED
};
}
}
debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
}
// Raise an error if any files were not found.
if (errorOnUnmatchedPattern) {
if (!foundRegardlessOfIgnored) {
throw new NoFilesFoundError(
pattern,
!globInputPaths && isGlob(pattern)
);
}
if (!found) {
throw new AllFilesIgnoredError(pattern);
}
}
}
/**
* 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);
const isDot = dotfilesPattern.test(pattern);
const stat = statSafeSync(absolutePath);
debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
}
if (stat && stat.isDirectory()) {
return this._iterateFilesWithDirectory(absolutePath, isDot);
}
if (stat && stat.isFile()) {
return this._iterateFilesWithFile(absolutePath);
}
if (globInputPaths && isGlobPattern(pattern)) {
return this._iterateFilesWithGlob(pattern, isDot);
}
/**
* 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);
const isDot = dotfilesPattern.test(pattern);
const stat = statSafeSync(absolutePath);
return [];
}
if (stat && stat.isDirectory()) {
return this._iterateFilesWithDirectory(absolutePath, isDot);
}
if (stat && stat.isFile()) {
return this._iterateFilesWithFile(absolutePath);
}
if (globInputPaths && isGlobPattern(pattern)) {
return this._iterateFilesWithGlob(pattern, isDot);
}
/**
* 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}`);
return [];
}
const { configArrayFactory } = internalSlotsMap.get(this);
const config = configArrayFactory.getConfigArrayForFile(filePath);
const ignored = this._isIgnoredFile(filePath, { config, direct: true });
const flag = ignored ? IGNORED : NONE;
/**
* 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}`);
return [{ config, filePath, flag }];
}
const { configArrayFactory } = internalSlotsMap.get(this);
const config = configArrayFactory.getConfigArrayForFile(filePath);
const ignored = this._isIgnoredFile(filePath, { config, direct: true });
const flag = ignored ? IGNORED : NONE;
/**
* 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 [{ config, filePath, flag }];
}
return this._iterateFilesRecursive(directoryPath, {
dotfiles,
recursive: true,
selector: null,
});
}
/**
* 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}`);
/**
* 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}`);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive: true, selector: null }
);
}
const { cwd } = internalSlotsMap.get(this);
const directoryPath = path.resolve(cwd, getGlobParent(pattern));
const absolutePath = path.resolve(cwd, pattern);
const globPart = absolutePath.slice(directoryPath.length + 1);
/**
* 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}`);
/*
* 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(absolutePath, minimatchOpts);
const { cwd } = internalSlotsMap.get(this);
const directoryPath = path.resolve(cwd, getGlobParent(pattern));
const absolutePath = path.resolve(cwd, pattern);
const globPart = absolutePath.slice(directoryPath.length + 1);
debug(`recursive? ${recursive}`);
/*
* 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(absolutePath, minimatchOpts);
return this._iterateFilesRecursive(directoryPath, {
dotfiles,
recursive,
selector,
});
}
debug(`recursive? ${recursive}`);
/**
* 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) {
debug(`Enter the directory: ${directoryPath}`);
const { configArrayFactory } = internalSlotsMap.get(this);
return this._iterateFilesRecursive(
directoryPath,
{ dotfiles, recursive, selector }
);
}
/** @type {ConfigArray|null} */
let config = null;
/**
* 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) {
debug(`Enter the directory: ${directoryPath}`);
const { configArrayFactory } = internalSlotsMap.get(this);
// Enumerate the files of this directory.
for (const entry of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, entry.name);
const fileInfo = entry.isSymbolicLink()
? statSafeSync(filePath)
: entry;
/** @type {ConfigArray|null} */
let config = null;
if (!fileInfo) {
continue;
}
// Enumerate the files of this directory.
for (const entry of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, entry.name);
const fileInfo = entry.isSymbolicLink() ? statSafeSync(filePath) : entry;
// Check if the file is matched.
if (fileInfo.isFile()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,
if (!fileInfo) {
continue;
}
/*
* We must ignore `ConfigurationNotFoundError` at this
* point because we don't know if target files exist in
* this directory.
*/
{ ignoreNotFoundError: true },
);
}
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.
this.isTargetPath(filePath, config);
// Check if the file is matched.
if (fileInfo.isFile()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,
if (matched) {
const ignored = this._isIgnoredFile(filePath, {
...options,
config,
});
const flag = ignored ? IGNORED_SILENTLY : NONE;
/*
* We must ignore `ConfigurationNotFoundError` at this
* point because we don't know if target files exist in
* this directory.
*/
{ ignoreNotFoundError: true }
);
}
const matched = options.selector
debug(
`Yield: ${entry.name}${ignored ? " but ignored" : ""}`,
);
yield {
config: configArrayFactory.getConfigArrayForFile(
filePath,
),
filePath,
flag,
};
} else {
debug(`Didn't match: ${entry.name}`);
}
// Started with a glob pattern; choose by the pattern.
? options.selector.match(filePath)
// Dive into the sub directory.
} else if (options.recursive && fileInfo.isDirectory()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,
{ ignoreNotFoundError: true },
);
}
const ignored = this._isIgnoredFile(filePath + path.sep, {
...options,
config,
});
// Started with a directory path; choose by file extensions.
: this.isTargetPath(filePath, config);
if (!ignored) {
yield* this._iterateFilesRecursive(filePath, options);
}
}
}
if (matched) {
const ignored = this._isIgnoredFile(filePath, { ...options, config });
const flag = ignored ? IGNORED_SILENTLY : NONE;
debug(`Leave the directory: ${directoryPath}`);
}
debug(`Yield: ${entry.name}${ignored ? " but ignored" : ""}`);
yield {
config: configArrayFactory.getConfigArrayForFile(filePath),
filePath,
flag
};
} else {
debug(`Didn't match: ${entry.name}`);
}
/**
* Check if a given file should be ignored.
* @param {string} filePath The path to a file to check.
* @param {Object} options Options
* @param {ConfigArray} [options.config] The config for this file.
* @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,
{ config: providedConfig, dotfiles = false, direct = false },
) {
const { configArrayFactory, defaultIgnores, ignoreFlag } =
internalSlotsMap.get(this);
// Dive into the sub directory.
} else if (options.recursive && fileInfo.isDirectory()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,
{ ignoreNotFoundError: true }
);
}
const ignored = this._isIgnoredFile(
filePath + path.sep,
{ ...options, config }
);
if (ignoreFlag) {
const config =
providedConfig ||
configArrayFactory.getConfigArrayForFile(filePath, {
ignoreNotFoundError: true,
});
const ignores =
config.extractConfig(filePath).ignores || defaultIgnores;
if (!ignored) {
yield* this._iterateFilesRecursive(filePath, options);
}
}
}
return ignores(filePath, dotfiles);
}
debug(`Leave the directory: ${directoryPath}`);
}
return !direct && defaultIgnores(filePath, dotfiles);
}
/**
* Check if a given file should be ignored.
* @param {string} filePath The path to a file to check.
* @param {Object} options Options
* @param {ConfigArray} [options.config] The config for this file.
* @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, {
config: providedConfig,
dotfiles = false,
direct = false
}) {
const {
configArrayFactory,
defaultIgnores,
ignoreFlag
} = internalSlotsMap.get(this);
if (ignoreFlag) {
const config =
providedConfig ||
configArrayFactory.getConfigArrayForFile(
filePath,
{ ignoreNotFoundError: true }
);
const ignores =
config.extractConfig(filePath).ignores || defaultIgnores;
return ignores(filePath, dotfiles);
}
return !direct && defaultIgnores(filePath, dotfiles);
}
}
//------------------------------------------------------------------------------
+60
View File
@@ -0,0 +1,60 @@
/**
* @fileoverview CheckStyle XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<checkstyle version=\"4.3\">";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${xmlEscape(result.filePath)}">`;
messages.forEach(message => {
output += [
`<error line="${xmlEscape(message.line || 0)}"`,
`column="${xmlEscape(message.column || 0)}"`,
`severity="${xmlEscape(getMessageType(message))}"`,
`message="${xmlEscape(message.message)}${message.ruleId ? ` (${message.ruleId})` : ""}"`,
`source="${message.ruleId ? xmlEscape(`eslint.rules.${message.ruleId}`) : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</checkstyle>";
return output;
};
+60
View File
@@ -0,0 +1,60 @@
/**
* @fileoverview Compact reporter
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}: `;
output += `line ${message.line || 0}`;
output += `, col ${message.column || 0}`;
output += `, ${getMessageType(message)}`;
output += ` - ${message.message}`;
output += message.ruleId ? ` (${message.ruleId})` : "";
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
+45 -17
View File
@@ -1,18 +1,46 @@
[
{
"name": "html",
"description": "Outputs results to HTML. The `html` formatter is useful for visual presentation in the browser."
},
{
"name": "json-with-metadata",
"description": "Outputs JSON-serialized results. The `json-with-metadata` provides the same linting results as the [`json`](#json) formatter with additional metadata about the rules applied. The linting results are included in the `results` property and the rules metadata is included in the `metadata` property.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
},
{
"name": "json",
"description": "Outputs JSON-serialized results. The `json` formatter is useful when you want to programmatically work with the CLI's linting results.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
},
{
"name": "stylish",
"description": "Human-readable output format. This is the default formatter."
}
]
{
"name": "checkstyle",
"description": "Outputs results to the [Checkstyle](https://checkstyle.sourceforge.io/) format."
},
{
"name": "compact",
"description": "Human-readable output format. Mimics the default output of JSHint."
},
{
"name": "html",
"description": "Outputs results to HTML. The `html` formatter is useful for visual presentation in the browser."
},
{
"name": "jslint-xml",
"description": "Outputs results to format compatible with the [JSLint Jenkins plugin](https://plugins.jenkins.io/jslint/)."
},
{
"name": "json-with-metadata",
"description": "Outputs JSON-serialized results. The `json-with-metadata` provides the same linting results as the [`json`](#json) formatter with additional metadata about the rules applied. The linting results are included in the `results` property and the rules metadata is included in the `metadata` property.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
},
{
"name": "json",
"description": "Outputs JSON-serialized results. The `json` formatter is useful when you want to programmatically work with the CLI's linting results.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
},
{
"name": "junit",
"description": "Outputs results to format compatible with the [JUnit Jenkins plugin](https://plugins.jenkins.io/junit/)."
},
{
"name": "stylish",
"description": "Human-readable output format. This is the default formatter."
},
{
"name": "tap",
"description": "Outputs results to the [Test Anything Protocol (TAP)](https://testanything.org/) specification format."
},
{
"name": "unix",
"description": "Outputs results to a format similar to many commands in UNIX-like systems. Parsable with tools such as [grep](https://www.gnu.org/software/grep/manual/grep.html), [sed](https://www.gnu.org/software/sed/manual/sed.html), and [awk](https://www.gnu.org/software/gawk/manual/gawk.html)."
},
{
"name": "visualstudio",
"description": "Outputs results to format compatible with the integrated terminal of the [Visual Studio](https://visualstudio.microsoft.com/) IDE. When using Visual Studio, you can click on the linting results in the integrated terminal to go to the issue in the source code."
}
]
+97 -105
View File
@@ -8,22 +8,22 @@
// Helpers
//------------------------------------------------------------------------------
const encodeHTML = (function () {
const encodeHTMLRules = {
"&": "&#38;",
"<": "&#60;",
">": "&#62;",
'"': "&#34;",
"'": "&#39;",
};
const matchHTML = /[&<>"']/gu;
const encodeHTML = (function() {
const encodeHTMLRules = {
"&": "&#38;",
"<": "&#60;",
">": "&#62;",
'"': "&#34;",
"'": "&#39;"
};
const matchHTML = /[&<>"']/ug;
return function (code) {
return code
? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m)
: "";
};
})();
return function(code) {
return code
? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m)
: "";
};
}());
/**
* Get the final HTML document.
@@ -31,9 +31,9 @@ const encodeHTML = (function () {
* @returns {string} HTML document.
*/
function pageTemplate(it) {
const { reportColor, reportSummary, date, results } = it;
const { reportColor, reportSummary, date, results } = it;
return `
return `
<!DOCTYPE html>
<html>
<head>
@@ -182,11 +182,11 @@ function pageTemplate(it) {
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {number} count A number controlling whether word should be pluralized.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return count === 1 ? word : `${word}s`;
return (count === 1 ? word : `${word}s`);
}
/**
@@ -196,29 +196,29 @@ function pluralize(word, count) {
* @returns {string} The formatted string, pluralized where necessary
*/
function renderSummary(totalErrors, totalWarnings) {
const totalProblems = totalErrors + totalWarnings;
let renderedText = `${totalProblems} ${pluralize("problem", totalProblems)}`;
const totalProblems = totalErrors + totalWarnings;
let renderedText = `${totalProblems} ${pluralize("problem", totalProblems)}`;
if (totalProblems !== 0) {
renderedText += ` (${totalErrors} ${pluralize("error", totalErrors)}, ${totalWarnings} ${pluralize("warning", totalWarnings)})`;
}
return renderedText;
if (totalProblems !== 0) {
renderedText += ` (${totalErrors} ${pluralize("error", totalErrors)}, ${totalWarnings} ${pluralize("warning", totalWarnings)})`;
}
return renderedText;
}
/**
* Get the color based on whether there are errors/warnings...
* @param {string} totalErrors Total errors
* @param {string} totalWarnings Total warnings
* @returns {number} The color code (0 = green, 1 = yellow, 2 = red)
* @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
*/
function renderColor(totalErrors, totalWarnings) {
if (totalErrors !== 0) {
return 2;
}
if (totalWarnings !== 0) {
return 1;
}
return 0;
if (totalErrors !== 0) {
return 2;
}
if (totalWarnings !== 0) {
return 1;
}
return 0;
}
/**
@@ -227,18 +227,18 @@ function renderColor(totalErrors, totalWarnings) {
* @returns {string} HTML (table row) describing the message.
*/
function messageTemplate(it) {
const {
parentIndex,
lineNumber,
columnNumber,
severityNumber,
severityName,
message,
ruleUrl,
ruleId,
} = it;
const {
parentIndex,
lineNumber,
columnNumber,
severityNumber,
severityName,
message,
ruleUrl,
ruleId
} = it;
return `
return `
<tr style="display: none;" class="f-${parentIndex}">
<td>${lineNumber}:${columnNumber}</td>
<td class="clr-${severityNumber}">${severityName}</td>
@@ -253,42 +253,41 @@ function messageTemplate(it) {
/**
* Get HTML (table rows) describing the messages.
* @param {Array} messages Messages.
* @param {number} parentIndex Index of the parent HTML row.
* @param {int} parentIndex Index of the parent HTML row.
* @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
* @returns {string} HTML (table rows) describing the messages.
*/
function renderMessages(messages, parentIndex, rulesMeta) {
/**
* Get HTML (table row) describing a message.
* @param {Object} message Message.
* @returns {string} HTML (table row) describing a message.
*/
return messages
.map(message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
let ruleUrl;
if (rulesMeta) {
const meta = rulesMeta[message.ruleId];
/**
* Get HTML (table row) describing a message.
* @param {Object} message Message.
* @returns {string} HTML (table row) describing a message.
*/
return messages.map(message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
let ruleUrl;
if (meta && meta.docs && meta.docs.url) {
ruleUrl = meta.docs.url;
}
}
if (rulesMeta) {
const meta = rulesMeta[message.ruleId];
return messageTemplate({
parentIndex,
lineNumber,
columnNumber,
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId,
ruleUrl,
});
})
.join("\n");
if (meta && meta.docs && meta.docs.url) {
ruleUrl = meta.docs.url;
}
}
return messageTemplate({
parentIndex,
lineNumber,
columnNumber,
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId,
ruleUrl
});
}).join("\n");
}
/**
@@ -297,9 +296,9 @@ function renderMessages(messages, parentIndex, rulesMeta) {
* @returns {string} HTML (table row) describing the result for the file.
*/
function resultTemplate(it) {
const { color, index, filePath, summary } = it;
const { color, index, filePath, summary } = it;
return `
return `
<tr class="bg-${color}" data-group="f-${index}">
<th colspan="4">
[+] ${encodeHTML(filePath)}
@@ -316,44 +315,37 @@ function resultTemplate(it) {
* @returns {string} HTML string describing the results.
*/
function renderResults(results, rulesMeta) {
return results
.map(
(result, index) =>
resultTemplate({
index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(
result.errorCount,
result.warningCount,
),
}) + renderMessages(result.messages, index, rulesMeta),
)
.join("\n");
return results.map((result, index) => resultTemplate({
index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(result.errorCount, result.warningCount)
}) + renderMessages(result.messages, index, rulesMeta)).join("\n");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function (results, data) {
let totalErrors, totalWarnings;
module.exports = function(results, data) {
let totalErrors,
totalWarnings;
const metaData = data ? data.rulesMeta : {};
const metaData = data ? data.rulesMeta : {};
totalErrors = 0;
totalWarnings = 0;
totalErrors = 0;
totalWarnings = 0;
// Iterate over results to get totals
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
});
// Iterate over results to get totals
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
});
return pageTemplate({
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results, metaData),
});
return pageTemplate({
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results, metaData)
});
};
+41
View File
@@ -0,0 +1,41 @@
/**
* @fileoverview JSLint XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<jslint>";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${result.filePath}">`;
messages.forEach(message => {
output += [
`<issue line="${message.line}"`,
`char="${message.column}"`,
`evidence="${xmlEscape(message.source || "")}"`,
`reason="${xmlEscape(message.message || "")}${message.ruleId ? ` (${message.ruleId})` : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</jslint>";
return output;
};
@@ -8,9 +8,9 @@
// Public Interface
//------------------------------------------------------------------------------
module.exports = function (results, data) {
return JSON.stringify({
results,
metadata: data,
});
module.exports = function(results, data) {
return JSON.stringify({
results,
metadata: data
});
};
+2 -2
View File
@@ -8,6 +8,6 @@
// Public Interface
//------------------------------------------------------------------------------
module.exports = function (results) {
return JSON.stringify(results);
module.exports = function(results) {
return JSON.stringify(results);
};
+82
View File
@@ -0,0 +1,82 @@
/**
* @fileoverview jUnit Reporter
* @author Jamund Ferguson
*/
"use strict";
const xmlEscape = require("../xml-escape");
const path = require("path");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
/**
* Returns a full file path without extension
* @param {string} filePath input file path
* @returns {string} file path without extension
* @private
*/
function pathWithoutExt(filePath) {
return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)));
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
output += "<testsuites>\n";
results.forEach(result => {
const messages = result.messages;
const classname = pathWithoutExt(result.filePath);
if (messages.length > 0) {
output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
messages.forEach(message => {
const type = message.fatal ? "error" : "failure";
output += `<testcase time="0" name="org.eslint.${message.ruleId || "unknown"}" classname="${classname}">`;
output += `<${type} message="${xmlEscape(message.message || "")}">`;
output += "<![CDATA[";
output += `line ${message.line || 0}, col `;
output += `${message.column || 0}, ${getMessageType(message)}`;
output += ` - ${xmlEscape(message.message || "")}`;
output += (message.ruleId ? ` (${message.ruleId})` : "");
output += "]]>";
output += `</${type}>`;
output += "</testcase>\n";
});
output += "</testsuite>\n";
} else {
output += `<testsuite package="org.eslint" time="0" tests="1" errors="0" name="${result.filePath}">\n`;
output += `<testcase time="0" name="${result.filePath}" classname="${classname}" />\n`;
output += "</testsuite>\n";
}
});
output += "</testsuites>\n";
return output;
};
+65 -86
View File
@@ -5,8 +5,8 @@
"use strict";
const chalk = require("chalk"),
util = require("node:util"),
table = require("../../shared/text-table");
stripAnsi = require("strip-ansi"),
table = require("text-table");
//------------------------------------------------------------------------------
// Helpers
@@ -15,108 +15,87 @@ const chalk = require("chalk"),
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
* @param {number} count A number controlling whether word should be pluralized.
* @param {int} count A number controlling whether word should be pluralized.
* @returns {string} The original word with an s on the end if count is not one.
*/
function pluralize(word, count) {
return count === 1 ? word : `${word}s`;
return (count === 1 ? word : `${word}s`);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function (results) {
let output = "\n",
errorCount = 0,
warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow";
module.exports = function(results) {
results.forEach(result => {
const messages = result.messages;
let output = "\n",
errorCount = 0,
warningCount = 0,
fixableErrorCount = 0,
fixableWarningCount = 0,
summaryColor = "yellow";
if (messages.length === 0) {
return;
}
results.forEach(result => {
const messages = result.messages;
errorCount += result.errorCount;
warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
if (messages.length === 0) {
return;
}
output += `${chalk.underline(result.filePath)}\n`;
errorCount += result.errorCount;
warningCount += result.warningCount;
fixableErrorCount += result.fixableErrorCount;
fixableWarningCount += result.fixableWarningCount;
output += `${table(
messages.map(message => {
let messageType;
output += `${chalk.underline(result.filePath)}\n`;
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
summaryColor = "red";
} else {
messageType = chalk.yellow("warning");
}
output += `${table(
messages.map(message => {
let messageType;
return [
"",
String(message.line || 0),
String(message.column || 0),
messageType,
message.message.replace(/([^ ])\.$/u, "$1"),
chalk.dim(message.ruleId || ""),
];
}),
{
align: ["", "r", "l"],
stringLength(str) {
return util.stripVTControlCharacters(str).length;
},
},
)
.split("\n")
.map(el =>
el.replace(/(\d+)\s+(\d+)/u, (m, p1, p2) =>
chalk.dim(`${p1}:${p2}`),
),
)
.join("\n")}\n\n`;
});
if (message.fatal || message.severity === 2) {
messageType = chalk.red("error");
summaryColor = "red";
} else {
messageType = chalk.yellow("warning");
}
const total = errorCount + warningCount;
return [
"",
message.line || 0,
message.column || 0,
messageType,
message.message.replace(/([^ ])\.$/u, "$1"),
chalk.dim(message.ruleId || "")
];
}),
{
align: ["", "r", "l"],
stringLength(str) {
return stripAnsi(str).length;
}
}
).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/u, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
});
if (total > 0) {
output += chalk[summaryColor].bold(
[
"\u2716 ",
total,
pluralize(" problem", total),
" (",
errorCount,
pluralize(" error", errorCount),
", ",
warningCount,
pluralize(" warning", warningCount),
")\n",
].join(""),
);
const total = errorCount + warningCount;
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
output += chalk[summaryColor].bold(
[
" ",
fixableErrorCount,
pluralize(" error", fixableErrorCount),
" and ",
fixableWarningCount,
pluralize(" warning", fixableWarningCount),
" potentially fixable with the `--fix` option.\n",
].join(""),
);
}
}
if (total > 0) {
output += chalk[summaryColor].bold([
"\u2716 ", total, pluralize(" problem", total),
" (", errorCount, pluralize(" error", errorCount), ", ",
warningCount, pluralize(" warning", warningCount), ")\n"
].join(""));
// Resets output color, for prevent change on top level
return total > 0 ? chalk.reset(output) : "";
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
output += chalk[summaryColor].bold([
" ", fixableErrorCount, pluralize(" error", fixableErrorCount), " and ",
fixableWarningCount, pluralize(" warning", fixableWarningCount),
" potentially fixable with the `--fix` option.\n"
].join(""));
}
}
// Resets output color, for prevent change on top level
return total > 0 ? chalk.reset(output) : "";
};
+95
View File
@@ -0,0 +1,95 @@
/**
* @fileoverview TAP reporter
* @author Jonathan Kingston
*/
"use strict";
const yaml = require("js-yaml");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
/**
* Takes in a JavaScript object and outputs a TAP diagnostics string
* @param {Object} diagnostic JavaScript object to be embedded as YAML into output.
* @returns {string} diagnostics string with YAML embedded - TAP version 13 compliant
*/
function outputDiagnostics(diagnostic) {
const prefix = " ";
let output = `${prefix}---\n`;
output += prefix + yaml.dump(diagnostic).split("\n").join(`\n${prefix}`);
output += "...\n";
return output;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = `TAP version 13\n1..${results.length}\n`;
results.forEach((result, id) => {
const messages = result.messages;
let testResult = "ok";
let diagnostics = {};
if (messages.length > 0) {
messages.forEach(message => {
const severity = getMessageType(message);
const diagnostic = {
message: message.message,
severity,
data: {
line: message.line || 0,
column: message.column || 0,
ruleId: message.ruleId || ""
}
};
// This ensures a warning message is not flagged as error
if (severity === "error") {
testResult = "not ok";
}
/*
* If we have multiple messages place them under a messages key
* The first error will be logged as message key
* This is to adhere to TAP 13 loosely defined specification of having a message key
*/
if ("message" in diagnostics) {
if (typeof diagnostics.messages === "undefined") {
diagnostics.messages = [];
}
diagnostics.messages.push(diagnostic);
} else {
diagnostics = diagnostic;
}
});
}
output += `${testResult} ${id + 1} - ${result.filePath}\n`;
// If we have an error include diagnostics
if (messages.length > 0) {
output += outputDiagnostics(diagnostics);
}
});
return output;
};
+58
View File
@@ -0,0 +1,58 @@
/**
* @fileoverview unix-style formatter.
* @author oshi-shinobu
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}:`;
output += `${message.line || 0}:`;
output += `${message.column || 0}:`;
output += ` ${message.message} `;
output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`;
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
+63
View File
@@ -0,0 +1,63 @@
/**
* @fileoverview Visual Studio compatible formatter
* @author Ronald Pijnacker
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += result.filePath;
output += `(${message.line || 0}`;
output += message.column ? `,${message.column}` : "";
output += `): ${getMessageType(message)}`;
output += message.ruleId ? ` ${message.ruleId}` : "";
output += ` : ${message.message}`;
output += "\n";
});
});
if (total === 0) {
output += "no problems";
} else {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};
+1 -1
View File
@@ -25,7 +25,7 @@ const murmur = require("imurmurhash");
* @returns {string} the hash
*/
function hash(str) {
return murmur(str).result().toString(36);
return murmur(str).result().toString(36);
}
//------------------------------------------------------------------------------
+1 -1
View File
@@ -3,5 +3,5 @@
const { CLIEngine } = require("./cli-engine");
module.exports = {
CLIEngine,
CLIEngine
};
+126 -143
View File
@@ -8,21 +8,15 @@
// Requirements
//-----------------------------------------------------------------------------
const fs = require("node:fs");
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 assert = require("../shared/assert");
const hash = require("./hash");
const debug = require("debug")("eslint:lint-result-cache");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../types").Linter.Config} Config */
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
@@ -32,8 +26,8 @@ const nodeVersion = process && process.version;
const validCacheStrategies = ["metadata", "content"];
const invalidCacheStrategyErrorMessage = `Cache strategy must be one of: ${validCacheStrategies
.map(strategy => `"${strategy}"`)
.join(", ")}`;
.map(strategy => `"${strategy}"`)
.join(", ")}`;
/**
* Tests whether a provided cacheStrategy is valid
@@ -41,23 +35,22 @@ const invalidCacheStrategyErrorMessage = `Cache strategy must be one of: ${valid
* @returns {boolean} true if `cacheStrategy` is one of `validCacheStrategies`; false otherwise
*/
function isValidCacheStrategy(cacheStrategy) {
return validCacheStrategies.includes(cacheStrategy);
return (
validCacheStrategies.includes(cacheStrategy)
);
}
/**
* Calculates the hash of the config
* @param {Config} config The config.
* @param {ConfigArray} config The config.
* @returns {string} The hash of the config
*/
function hashOfConfigFor(config) {
if (!configHashCache.has(config)) {
configHashCache.set(
config,
hash(`${pkg.version}_${nodeVersion}_${stringify(config)}`),
);
}
if (!configHashCache.has(config)) {
configHashCache.set(config, hash(`${pkg.version}_${nodeVersion}_${stringify(config)}`));
}
return configHashCache.get(config);
return configHashCache.get(config);
}
//-----------------------------------------------------------------------------
@@ -70,151 +63,141 @@ function hashOfConfigFor(config) {
* serialize and adding them back in on retrieval.
*/
class LintResultCache {
/**
* Creates a new LintResultCache instance.
* @param {string} cacheFileLocation The cache file location.
* @param {"metadata" | "content"} cacheStrategy The cache strategy to use.
*/
constructor(cacheFileLocation, cacheStrategy) {
assert(cacheFileLocation, "Cache file location is required");
assert(cacheStrategy, "Cache strategy is required");
assert(
isValidCacheStrategy(cacheStrategy),
invalidCacheStrategyErrorMessage,
);
debug(`Caching results to ${cacheFileLocation}`);
/**
* Creates a new LintResultCache instance.
* @param {string} cacheFileLocation The cache file location.
* @param {"metadata" | "content"} cacheStrategy The cache strategy to use.
*/
constructor(cacheFileLocation, cacheStrategy) {
assert(cacheFileLocation, "Cache file location is required");
assert(cacheStrategy, "Cache strategy is required");
assert(
isValidCacheStrategy(cacheStrategy),
invalidCacheStrategyErrorMessage
);
const useChecksum = cacheStrategy === "content";
debug(`Caching results to ${cacheFileLocation}`);
debug(`Using "${cacheStrategy}" strategy to detect changes`);
const useChecksum = cacheStrategy === "content";
this.fileEntryCache = fileEntryCache.create(
cacheFileLocation,
void 0,
useChecksum,
);
this.cacheFileLocation = cacheFileLocation;
}
debug(
`Using "${cacheStrategy}" strategy to detect changes`
);
/**
* Retrieve cached lint results for a given file path, if present in the
* 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 {Config} 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, config) {
const cachedResults = this.getValidCachedLintResults(filePath, config);
this.fileEntryCache = fileEntryCache.create(
cacheFileLocation,
void 0,
useChecksum
);
this.cacheFileLocation = cacheFileLocation;
}
if (!cachedResults) {
return cachedResults;
}
/**
* Retrieve cached lint results for a given file path, if present in the
* 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, config) {
/*
* Shallow clone the object to ensure that any properties added or modified afterwards
* will not be accidentally stored in the cache file when `reconcile()` is called.
* https://github.com/eslint/eslint/issues/13507
* All intentional changes to the cache file must be done through `setCachedLintResults()`.
*/
const results = { ...cachedResults };
/*
* Cached lint results are valid if and only if:
* 1. The file is present in the filesystem
* 2. The file has not changed since the time it was previously linted
* 3. The ESLint configuration has not changed since the time the file
* was previously linted
* If any of these are not true, we will not reuse the lint results.
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const hashOfConfig = hashOfConfigFor(config);
const changed =
fileDescriptor.changed ||
fileDescriptor.meta.hashOfConfig !== hashOfConfig;
// If source is present but null, need to reread the file from the filesystem.
if (results.source === null) {
debug(
`Rereading cached result source from filesystem: ${filePath}`,
);
results.source = fs.readFileSync(filePath, "utf-8");
}
if (fileDescriptor.notFound) {
debug(`File not found on the file system: ${filePath}`);
return null;
}
return results;
}
if (changed) {
debug(`Cache entry not found or no longer valid: ${filePath}`);
return null;
}
/**
* Retrieve cached lint results for a given file path, if present in the
* cache and still valid.
* @param {string} filePath The file for which to retrieve lint results.
* @param {Config} config The config of the file.
* @returns {Object|null} The cached lint results if present in the cache
* and still valid; null otherwise.
*/
getValidCachedLintResults(filePath, config) {
/*
* Cached lint results are valid if and only if:
* 1. The file is present in the filesystem
* 2. The file has not changed since the time it was previously linted
* 3. The ESLint configuration has not changed since the time the file
* was previously linted
* If any of these are not true, we will not reuse the lint results.
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const cachedResults = fileDescriptor.meta.results;
if (fileDescriptor.notFound) {
debug(`File not found on the file system: ${filePath}`);
return null;
}
// Just in case, not sure if this can ever happen.
if (!cachedResults) {
return cachedResults;
}
const hashOfConfig = hashOfConfigFor(config);
const changed =
fileDescriptor.changed ||
fileDescriptor.meta.hashOfConfig !== hashOfConfig;
/*
* Shallow clone the object to ensure that any properties added or modified afterwards
* will not be accidentally stored in the cache file when `reconcile()` is called.
* https://github.com/eslint/eslint/issues/13507
* All intentional changes to the cache file must be done through `setCachedLintResults()`.
*/
const results = { ...cachedResults };
if (changed) {
debug(`Cache entry not found or no longer valid: ${filePath}`);
return null;
}
// If source is present but null, need to reread the file from the filesystem.
if (results.source === null) {
debug(`Rereading cached result source from filesystem: ${filePath}`);
results.source = fs.readFileSync(filePath, "utf-8");
}
return fileDescriptor.meta.results;
}
return results;
}
/**
* Set the cached lint results for a given file path, after removing any
* information that will be both unnecessary and difficult to serialize.
* Avoids caching results with an "output" property (meaning fixes were
* 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 {Config} config The config of the file.
* @param {Object} result The lint result to be set for the file.
* @returns {void}
*/
setCachedLintResults(filePath, config, result) {
if (result && Object.hasOwn(result, "output")) {
return;
}
/**
* Set the cached lint results for a given file path, after removing any
* information that will be both unnecessary and difficult to serialize.
* Avoids caching results with an "output" property (meaning fixes were
* 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, config, result) {
if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
return;
}
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
if (fileDescriptor && !fileDescriptor.notFound) {
debug(`Updating cached result: ${filePath}`);
if (fileDescriptor && !fileDescriptor.notFound) {
debug(`Updating cached result: ${filePath}`);
// Serialize the result, except that we want to remove the file source if present.
const resultToSerialize = Object.assign({}, result);
// Serialize the result, except that we want to remove the file source if present.
const resultToSerialize = Object.assign({}, result);
/*
* Set result.source to null.
* In `getCachedLintResults`, if source is explicitly null, we will
* read the file from the filesystem to set the value again.
*/
if (Object.hasOwn(resultToSerialize, "source")) {
resultToSerialize.source = null;
}
/*
* Set result.source to null.
* In `getCachedLintResults`, if source is explicitly null, we will
* read the file from the filesystem to set the value again.
*/
if (Object.prototype.hasOwnProperty.call(resultToSerialize, "source")) {
resultToSerialize.source = null;
}
fileDescriptor.meta.results = resultToSerialize;
fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
}
}
fileDescriptor.meta.results = resultToSerialize;
fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
}
}
/**
* Persists the in-memory cache to disk.
* @returns {void}
*/
reconcile() {
debug(`Persisting cached results: ${this.cacheFileLocation}`);
this.fileEntryCache.reconcile();
}
/**
* Persists the in-memory cache to disk.
* @returns {void}
*/
reconcile() {
debug(`Persisting cached results: ${this.cacheFileLocation}`);
this.fileEntryCache.reconcile();
}
}
module.exports = LintResultCache;
+17 -17
View File
@@ -9,8 +9,8 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("node:fs"),
path = require("node:path");
const fs = require("fs"),
path = require("path");
const rulesDirCache = {};
@@ -24,23 +24,23 @@ const rulesDirCache = {};
* @param {string} cwd Current working directory
* @returns {Object} Loaded rule modules.
*/
module.exports = function (relativeRulesDir, cwd) {
const rulesDir = path.resolve(cwd, relativeRulesDir);
module.exports = function(relativeRulesDir, cwd) {
const rulesDir = path.resolve(cwd, relativeRulesDir);
// cache will help performance as IO operation are expensive
if (rulesDirCache[rulesDir]) {
return rulesDirCache[rulesDir];
}
// cache will help performance as IO operation are expensive
if (rulesDirCache[rulesDir]) {
return rulesDirCache[rulesDir];
}
const rules = Object.create(null);
const rules = Object.create(null);
fs.readdirSync(rulesDir).forEach(file => {
if (path.extname(file) !== ".js") {
return;
}
rules[file.slice(0, -3)] = require(path.join(rulesDir, file));
});
rulesDirCache[rulesDir] = rules;
fs.readdirSync(rulesDir).forEach(file => {
if (path.extname(file) !== ".js") {
return;
}
rules[file.slice(0, -3)] = require(path.join(rulesDir, file));
});
rulesDirCache[rulesDir] = rules;
return rules;
return rules;
};
+34
View File
@@ -0,0 +1,34 @@
/**
* @fileoverview XML character escaper
* @author George Chung
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Returns the escaped value for a character
* @param {string} s string to examine
* @returns {string} severity level
* @private
*/
module.exports = function(s) {
return (`${s}`).replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, c => { // eslint-disable-line no-control-regex -- Converting controls to entities
switch (c) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "\"":
return "&quot;";
case "'":
return "&apos;";
default:
return `&#${c.charCodeAt(0)};`;
}
});
};
+361 -443
View File
@@ -15,73 +15,217 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("node:fs"),
{ mkdir, stat, writeFile } = require("node:fs/promises"),
path = require("node:path"),
{ pathToFileURL } = require("node:url"),
{ LegacyESLint } = require("./eslint"),
{
ESLint,
shouldUseFlatConfig,
locateConfigFileToUse,
} = require("./eslint/eslint"),
createCLIOptions = require("./options"),
log = require("./shared/logging"),
RuntimeInfo = require("./shared/runtime-info"),
translateOptions = require("./shared/translate-cli-options");
const { getCacheFile } = require("./eslint/eslint-helpers");
const { SuppressionsService } = require("./services/suppressions-service");
const fs = require("fs"),
path = require("path"),
{ promisify } = require("util"),
{ ESLint } = require("./eslint"),
{ FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"),
createCLIOptions = require("./options"),
log = require("./shared/logging"),
RuntimeInfo = require("./shared/runtime-info"),
{ normalizeSeverityToString } = require("./shared/severity");
const { Legacy: { naming } } = require("@eslint/eslintrc");
const { ModuleImporter } = require("@humanwhocodes/module-importer");
const debug = require("debug")("eslint:cli");
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
/** @typedef {import("./types").ESLint.LintResult} LintResult */
/** @typedef {import("./types").ESLint.ResultsMeta} ResultsMeta */
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const mkdir = promisify(fs.mkdir);
const stat = promisify(fs.stat);
const writeFile = promisify(fs.writeFile);
/**
* Predicate function for whether or not to apply fixes in quiet mode.
* If a message is a warning, do not apply a fix.
* @param {LintMessage} message The lint result.
* @returns {boolean} True if the lint message is an error (and thus should be
* autofixed), false otherwise.
*/
function quietFixPredicate(message) {
return message.severity === 2;
}
/**
* Translates the CLI options into the options expected by the ESLint constructor.
* @param {ParsedCLIOptions} cliOptions The CLI options to translate.
* @param {"flat"|"eslintrc"} [configType="eslintrc"] The format of the
* config to generate.
* @returns {Promise<ESLintOptions>} The options object for the ESLint constructor.
* @private
*/
async function translateOptions({
cache,
cacheFile,
cacheLocation,
cacheStrategy,
config,
configLookup,
env,
errorOnUnmatchedPattern,
eslintrc,
ext,
fix,
fixDryRun,
fixType,
global,
ignore,
ignorePath,
ignorePattern,
inlineConfig,
parser,
parserOptions,
plugin,
quiet,
reportUnusedDisableDirectives,
reportUnusedDisableDirectivesSeverity,
resolvePluginsRelativeTo,
rule,
rulesdir,
warnIgnored
}, configType) {
let overrideConfig, overrideConfigFile;
const importer = new ModuleImporter();
if (configType === "flat") {
overrideConfigFile = (typeof config === "string") ? config : !configLookup;
if (overrideConfigFile === false) {
overrideConfigFile = void 0;
}
let globals = {};
if (global) {
globals = global.reduce((obj, name) => {
if (name.endsWith(":true")) {
obj[name.slice(0, -5)] = "writable";
} else {
obj[name] = "readonly";
}
return obj;
}, globals);
}
overrideConfig = [{
languageOptions: {
globals,
parserOptions: parserOptions || {}
},
rules: rule ? rule : {}
}];
if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
overrideConfig[0].linterOptions = {
reportUnusedDisableDirectives: reportUnusedDisableDirectives
? "error"
: normalizeSeverityToString(reportUnusedDisableDirectivesSeverity)
};
}
if (parser) {
overrideConfig[0].languageOptions.parser = await importer.import(parser);
}
if (plugin) {
const plugins = {};
for (const pluginName of plugin) {
const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
plugins[shortName] = await importer.import(longName);
}
overrideConfig[0].plugins = plugins;
}
} else {
overrideConfigFile = config;
overrideConfig = {
env: env && env.reduce((obj, name) => {
obj[name] = true;
return obj;
}, {}),
globals: global && global.reduce((obj, name) => {
if (name.endsWith(":true")) {
obj[name.slice(0, -5)] = "writable";
} else {
obj[name] = "readonly";
}
return obj;
}, {}),
ignorePatterns: ignorePattern,
parser,
parserOptions,
plugins: plugin,
rules: rule
};
}
const options = {
allowInlineConfig: inlineConfig,
cache,
cacheLocation: cacheLocation || cacheFile,
cacheStrategy,
errorOnUnmatchedPattern,
fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
fixTypes: fixType,
ignore,
overrideConfig,
overrideConfigFile
};
if (configType === "flat") {
options.ignorePatterns = ignorePattern;
options.warnIgnored = warnIgnored;
} else {
options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
options.rulePaths = rulesdir;
options.useEslintrc = eslintrc;
options.extensions = ext;
options.ignorePath = ignorePath;
if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
options.reportUnusedDisableDirectives = reportUnusedDisableDirectives
? "error"
: normalizeSeverityToString(reportUnusedDisableDirectivesSeverity);
}
}
return options;
}
/**
* Count error messages.
* @param {LintResult[]} results The lint results.
* @returns {{errorCount:number;fatalErrorCount:number,warningCount:number}} The number of error messages.
*/
function countErrors(results) {
let errorCount = 0;
let fatalErrorCount = 0;
let warningCount = 0;
let errorCount = 0;
let fatalErrorCount = 0;
let warningCount = 0;
for (const result of results) {
errorCount += result.errorCount;
fatalErrorCount += result.fatalErrorCount;
warningCount += result.warningCount;
}
for (const result of results) {
errorCount += result.errorCount;
fatalErrorCount += result.fatalErrorCount;
warningCount += result.warningCount;
}
return { errorCount, fatalErrorCount, warningCount };
}
/**
* Creates an options module from the provided CLI options and encodes it as a data URL.
* @param {ParsedCLIOptions} options The CLI options.
* @returns {URL} The URL of the options module.
*/
function createOptionsModule(options) {
const translateOptionsFileURL = new URL(
"./shared/translate-cli-options.js",
pathToFileURL(__filename),
).href;
const optionsSrc =
`import translateOptions from ${JSON.stringify(translateOptionsFileURL)};\n` +
`export default await translateOptions(${JSON.stringify(options)}, "flat");\n`;
// Base64 encoding is typically shorter than URL encoding
return new URL(
`data:text/javascript;base64,${Buffer.from(optionsSrc).toString("base64")}`,
);
return { errorCount, fatalErrorCount, warningCount };
}
/**
@@ -90,14 +234,14 @@ function createOptionsModule(options) {
* @returns {Promise<boolean>} `true` if the given path is a directory.
*/
async function isDirectory(filePath) {
try {
return (await stat(filePath)).isDirectory();
} catch (error) {
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
return false;
}
throw error;
}
try {
return (await stat(filePath)).isDirectory();
} catch (error) {
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
return false;
}
throw error;
}
}
/**
@@ -111,64 +255,39 @@ async function isDirectory(filePath) {
* @private
*/
async function printResults(engine, results, format, outputFile, resultsMeta) {
let formatter;
let formatter;
try {
formatter = await engine.loadFormatter(format);
} catch (e) {
log.error(e.message);
return false;
}
try {
formatter = await engine.loadFormatter(format);
} catch (e) {
log.error(e.message);
return false;
}
const output = await formatter.format(results, resultsMeta);
const output = await formatter.format(results, resultsMeta);
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (output) {
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (await isDirectory(filePath)) {
log.error(
"Cannot write to output file path, it is a directory: %s",
outputFile,
);
return false;
}
if (await isDirectory(filePath)) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false;
}
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else if (output) {
log.info(output);
}
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else {
log.info(output);
}
}
return true;
}
/**
* Validates the `--concurrency` flag value.
* @param {string} concurrency The `--concurrency` flag value to validate.
* @returns {void}
* @throws {Error} If the `--concurrency` flag value is invalid.
*/
function validateConcurrency(concurrency) {
if (
concurrency === void 0 ||
concurrency === "auto" ||
concurrency === "off"
) {
return;
}
const concurrencyValue = Number(concurrency);
if (!Number.isInteger(concurrencyValue) || concurrencyValue < 1) {
throw new Error(
`Option concurrency: '${concurrency}' is not a positive integer, 'auto' or 'off'.`,
);
}
return true;
}
//------------------------------------------------------------------------------
@@ -180,374 +299,173 @@ function validateConcurrency(concurrency) {
* for other Node.js programs to effectively run the CLI.
*/
const cli = {
/**
* Calculates the command string for the --inspect-config operation.
* @param {string} configFile The path to the config file to inspect.
* @returns {Promise<string>} The command string to execute.
*/
async calculateInspectConfigFlags(configFile) {
// find the config file
const { configFilePath, basePath } = await locateConfigFileToUse({
cwd: process.cwd(),
configFile,
});
return ["--config", configFilePath, "--basePath", basePath];
},
/**
* Executes the CLI based on an array of arguments that is passed in.
* @param {string|Array|Object} args The arguments to process.
* @param {string} [text] The text to lint (used for TTY).
* @param {boolean} [allowFlatConfig] Whether or not to allow flat config.
* @returns {Promise<number>} The exit code for the operation.
*/
async execute(args, text, allowFlatConfig) {
if (Array.isArray(args)) {
debug("CLI args: %o", args.slice(2));
}
/**
* Executes the CLI based on an array of arguments that is passed in.
* @param {string|Array|Object} args The arguments to process.
* @param {string} [text] The text to lint (used for TTY).
* @param {boolean} [allowFlatConfig=true] Whether or not to allow flat config.
* @returns {Promise<number>} The exit code for the operation.
*/
async execute(args, text, allowFlatConfig = true) {
if (Array.isArray(args)) {
debug("CLI args: %o", args.slice(2));
}
/*
* Before doing anything, we need to see if we are using a
* flat config file. If so, then we need to change the way command
* line args are parsed. This is temporary, and when we fully
* switch to flat config we can remove this logic.
*/
/*
* Before doing anything, we need to see if we are using a
* flat config file. If so, then we need to change the way command
* line args are parsed. This is temporary, and when we fully
* switch to flat config we can remove this logic.
*/
const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig();
const usingFlatConfig =
allowFlatConfig && (await shouldUseFlatConfig());
debug("Using flat config?", usingFlatConfig);
debug("Using flat config?", usingFlatConfig);
const CLIOptions = createCLIOptions(usingFlatConfig);
if (allowFlatConfig && !usingFlatConfig) {
const { WarningService } = require("./services/warning-service");
new WarningService().emitESLintRCWarning();
}
/** @type {ParsedCLIOptions} */
let options;
const CLIOptions = createCLIOptions(usingFlatConfig);
try {
options = CLIOptions.parse(args);
} catch (error) {
debug("Error parsing CLI options:", error.message);
/** @type {ParsedCLIOptions} */
let options;
let errorMessage = error.message;
try {
options = CLIOptions.parse(args);
validateConcurrency(options.concurrency);
} catch (error) {
debug("Error parsing CLI options:", error.message);
if (usingFlatConfig) {
errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details.";
}
let errorMessage = error.message;
log.error(errorMessage);
return 2;
}
if (usingFlatConfig) {
errorMessage +=
"\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details.";
}
const files = options._;
const useStdin = typeof text === "string";
log.error(errorMessage);
return 2;
}
if (options.help) {
log.info(CLIOptions.generateHelp());
return 0;
}
if (options.version) {
log.info(RuntimeInfo.version());
return 0;
}
if (options.envInfo) {
try {
log.info(RuntimeInfo.environment());
return 0;
} catch (err) {
debug("Error retrieving environment info");
log.error(err.message);
return 2;
}
}
const files = options._;
const useStdin = typeof text === "string";
if (options.printConfig) {
if (files.length) {
log.error("The --print-config option must be used with exactly one file name.");
return 2;
}
if (useStdin) {
log.error("The --print-config option is not available for piped-in code.");
return 2;
}
if (options.help) {
log.info(CLIOptions.generateHelp());
return 0;
}
if (options.version) {
log.info(RuntimeInfo.version());
return 0;
}
if (options.envInfo) {
try {
log.info(RuntimeInfo.environment());
return 0;
} catch (err) {
debug("Error retrieving environment info");
log.error(err.message);
return 2;
}
}
const engine = usingFlatConfig
? new FlatESLint(await translateOptions(options, "flat"))
: new ESLint(await translateOptions(options));
const fileConfig =
await engine.calculateConfigForFile(options.printConfig);
if (options.printConfig) {
if (files.length) {
log.error(
"The --print-config option must be used with exactly one file name.",
);
return 2;
}
if (useStdin) {
log.error(
"The --print-config option is not available for piped-in code.",
);
return 2;
}
log.info(JSON.stringify(fileConfig, null, " "));
return 0;
}
const engine = usingFlatConfig
? new ESLint(await translateOptions(options, "flat"))
: new LegacyESLint(await translateOptions(options));
const fileConfig = await engine.calculateConfigForFile(
options.printConfig,
);
debug(`Running on ${useStdin ? "text" : "files"}`);
log.info(JSON.stringify(fileConfig, null, " "));
return 0;
}
if (options.fix && options.fixDryRun) {
log.error("The --fix option and the --fix-dry-run option cannot be used together.");
return 2;
}
if (useStdin && options.fix) {
log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
return 2;
}
if (options.fixType && !options.fix && !options.fixDryRun) {
log.error("The --fix-type option requires either --fix or --fix-dry-run.");
return 2;
}
if (options.inspectConfig) {
log.info(
"You can also run this command directly using 'npx @eslint/config-inspector@latest' in the same directory as your configuration file.",
);
if (options.reportUnusedDisableDirectives && options.reportUnusedDisableDirectivesSeverity !== void 0) {
log.error("The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together.");
return 2;
}
try {
const flatOptions = await translateOptions(options, "flat");
const spawn = require("cross-spawn");
const flags = await cli.calculateInspectConfigFlags(
flatOptions.overrideConfigFile,
);
const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint;
spawn.sync(
"npx",
["@eslint/config-inspector@latest", ...flags],
{ encoding: "utf8", stdio: "inherit" },
);
} catch (error) {
log.error(error);
return 2;
}
const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc"));
let results;
return 0;
}
if (useStdin) {
results = await engine.lintText(text, {
filePath: options.stdinFilename,
debug(`Running on ${useStdin ? "text" : "files"}`);
// flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
warnIgnored: usingFlatConfig ? void 0 : true
});
} else {
results = await engine.lintFiles(files);
}
if (options.fix && options.fixDryRun) {
log.error(
"The --fix option and the --fix-dry-run option cannot be used together.",
);
return 2;
}
if (useStdin && options.fix) {
log.error(
"The --fix option is not available for piped-in code; use --fix-dry-run instead.",
);
return 2;
}
if (options.fixType && !options.fix && !options.fixDryRun) {
log.error(
"The --fix-type option requires either --fix or --fix-dry-run.",
);
return 2;
}
if (options.fix) {
debug("Fix mode enabled - applying fixes");
await ActiveESLint.outputFixes(results);
}
if (
options.reportUnusedDisableDirectives &&
options.reportUnusedDisableDirectivesSeverity !== void 0
) {
log.error(
"The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together.",
);
return 2;
}
let resultsToPrint = results;
if (usingFlatConfig && options.ext) {
// Passing `--ext ""` results in `options.ext` being an empty array.
if (options.ext.length === 0) {
log.error("The --ext option value cannot be empty.");
return 2;
}
if (options.quiet) {
debug("Quiet mode enabled - filtering out warnings");
resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
}
// Passing `--ext ,ts` results in an empty string at index 0. Passing `--ext ts,,tsx` results in an empty string at index 1.
const emptyStringIndex = options.ext.indexOf("");
const resultCounts = countErrors(results);
const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
const resultsMeta = tooManyWarnings
? {
maxWarningsExceeded: {
maxWarnings: options.maxWarnings,
foundWarnings: resultCounts.warningCount
}
}
: {};
if (emptyStringIndex >= 0) {
log.error(
`The --ext option arguments cannot be empty strings. Found an empty string at index ${emptyStringIndex}.`,
);
return 2;
}
}
if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {
if (options.suppressAll && options.suppressRule) {
log.error(
"The --suppress-all option and the --suppress-rule option cannot be used together.",
);
return 2;
}
// Errors and warnings from the original unfiltered results should determine the exit code
const shouldExitForFatalErrors =
options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
if (options.suppressAll && options.pruneSuppressions) {
log.error(
"The --suppress-all option and the --prune-suppressions option cannot be used together.",
);
return 2;
}
if (!resultCounts.errorCount && tooManyWarnings) {
log.error(
"ESLint found too many warnings (maximum: %s).",
options.maxWarnings
);
}
if (options.suppressRule && options.pruneSuppressions) {
log.error(
"The --suppress-rule option and the --prune-suppressions option cannot be used together.",
);
return 2;
}
if (shouldExitForFatalErrors) {
return 2;
}
if (
useStdin &&
(options.suppressAll ||
options.suppressRule ||
options.pruneSuppressions)
) {
log.error(
"The --suppress-all, --suppress-rule, and --prune-suppressions options cannot be used with piped-in code.",
);
return 2;
}
return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
}
const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint;
/** @type {ESLint|LegacyESLint} */
let engine;
if (options.concurrency && options.concurrency !== "off") {
const optionsURL = createOptionsModule(options);
engine = await ESLint.fromOptionsModule(optionsURL);
} else {
const eslintOptions = await translateOptions(
options,
usingFlatConfig ? "flat" : "eslintrc",
);
engine = new ActiveESLint(eslintOptions);
}
let results;
if (useStdin) {
results = await engine.lintText(text, {
filePath: options.stdinFilename,
// flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
warnIgnored: usingFlatConfig ? void 0 : true,
});
} else {
results = await engine.lintFiles(files);
}
if (options.fix) {
debug("Fix mode enabled - applying fixes");
await ActiveESLint.outputFixes(results);
}
let unusedSuppressions = {};
if (!useStdin) {
const suppressionsFileLocation = getCacheFile(
options.suppressionsLocation || "eslint-suppressions.json",
process.cwd(),
{
prefix: "suppressions_",
},
);
if (
options.suppressionsLocation &&
!fs.existsSync(suppressionsFileLocation) &&
!options.suppressAll &&
!options.suppressRule
) {
log.error(
"The suppressions file does not exist. Please run the command with `--suppress-all` or `--suppress-rule` to create it.",
);
return 2;
}
if (
options.suppressAll ||
options.suppressRule ||
options.pruneSuppressions ||
fs.existsSync(suppressionsFileLocation)
) {
const suppressions = new SuppressionsService({
filePath: suppressionsFileLocation,
cwd: process.cwd(),
});
if (options.suppressAll || options.suppressRule) {
await suppressions.suppress(results, options.suppressRule);
}
if (options.pruneSuppressions) {
await suppressions.prune(results);
}
const suppressionResults = suppressions.applySuppressions(
results,
await suppressions.load(),
);
results = suppressionResults.results;
unusedSuppressions = suppressionResults.unused;
}
}
let resultsToPrint = results;
if (options.quiet) {
debug("Quiet mode enabled - filtering out warnings");
resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
}
const resultCounts = countErrors(results);
const tooManyWarnings =
options.maxWarnings >= 0 &&
resultCounts.warningCount > options.maxWarnings;
const resultsMeta = tooManyWarnings
? {
maxWarningsExceeded: {
maxWarnings: options.maxWarnings,
foundWarnings: resultCounts.warningCount,
},
}
: {};
if (
await printResults(
engine,
resultsToPrint,
options.format,
options.outputFile,
resultsMeta,
)
) {
// Errors and warnings from the original unfiltered results should determine the exit code
const shouldExitForFatalErrors =
options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
if (!resultCounts.errorCount && tooManyWarnings) {
log.error(
"ESLint found too many warnings (maximum: %s).",
options.maxWarnings,
);
}
if (!options.passOnUnprunedSuppressions) {
const unusedSuppressionsCount =
Object.keys(unusedSuppressions).length;
if (unusedSuppressionsCount > 0) {
log.error(
"There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.",
);
debug(JSON.stringify(unusedSuppressions, null, 2));
return 2;
}
}
if (shouldExitForFatalErrors) {
return 2;
}
return resultCounts.errorCount || tooManyWarnings ? 1 : 0;
}
return 2;
},
return 2;
}
};
module.exports = cli;
-12
View File
@@ -1,12 +0,0 @@
/**
* @fileoverview exports for config helpers
* @author Nicholas C. Zakas
*/
"use strict";
const { defineConfig, globalIgnores } = require("@eslint/config-helpers");
module.exports = {
defineConfig,
globalIgnores,
};
-816
View File
@@ -1,816 +0,0 @@
/**
* @fileoverview Utility to load config files
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("node:path");
const fs = require("node:fs/promises");
const findUp = require("find-up");
const { pathToFileURL } = require("node:url");
const debug = require("debug")("eslint:config-loader");
const { FlatConfigArray } = require("./flat-config-array");
const { WarningService } = require("../services/warning-service");
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/** @typedef {import("../types").Linter.Config} Config */
/**
* @typedef {Object} ConfigLoaderOptions
* @property {string|false|undefined} configFile The path to the config file to use.
* @property {string} cwd The current working directory.
* @property {boolean} ignoreEnabled Indicates if ignore patterns should be honored.
* @property {Config|Array<Config>} [baseConfig] The base config to use.
* @property {Array<Config>} [defaultConfigs] The default configs to use.
* @property {Array<string>} [ignorePatterns] The ignore patterns to use.
* @property {Config|Array<Config>} [overrideConfig] The override config to use.
* @property {boolean} [hasUnstableNativeNodeJsTSConfigFlag] The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
* @property {WarningService} [warningService] The warning service to use.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const FLAT_CONFIG_FILENAMES = [
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.mts",
"eslint.config.cts",
];
const importedConfigFileModificationTime = new Map();
/**
* Asserts that the given file path is valid.
* @param {string} filePath The file path to check.
* @returns {void}
* @throws {Error} If `filePath` is not a non-empty string.
*/
function assertValidFilePath(filePath) {
if (!filePath || typeof filePath !== "string") {
throw new Error("'filePath' must be a non-empty string");
}
}
/**
* Asserts that a configuration exists. A configuration exists if any
* of the following are true:
* - `configFilePath` is defined.
* - `useConfigFile` is `false`.
* @param {string|undefined} configFilePath The path to the config file.
* @param {ConfigLoaderOptions} loaderOptions The options to use when loading configuration files.
* @returns {void}
* @throws {Error} If no configuration exists.
*/
function assertConfigurationExists(configFilePath, loaderOptions) {
const { configFile: useConfigFile } = loaderOptions;
if (!configFilePath && useConfigFile !== false) {
const error = new Error("Could not find config file.");
error.messageTemplate = "config-file-missing";
throw error;
}
}
/**
* Check if the file is a TypeScript file.
* @param {string} filePath The file path to check.
* @returns {boolean} `true` if the file is a TypeScript file, `false` if it's not.
*/
function isFileTS(filePath) {
const fileExtension = path.extname(filePath);
return /^\.[mc]?ts$/u.test(fileExtension);
}
/**
* Check if ESLint is running in Bun.
* @returns {boolean} `true` if the ESLint is running Bun, `false` if it's not.
*/
function isRunningInBun() {
return !!globalThis.Bun;
}
/**
* Check if ESLint is running in Deno.
* @returns {boolean} `true` if the ESLint is running in Deno, `false` if it's not.
*/
function isRunningInDeno() {
return !!globalThis.Deno;
}
/**
* Checks if native TypeScript support is
* enabled in the current Node.js process.
*
* This function determines if the
* {@linkcode NodeJS.ProcessFeatures.typescript | typescript}
* feature is present in the
* {@linkcode process.features} object
* and if its value is either "strip" or "transform".
* @returns {boolean} `true` if native TypeScript support is enabled, otherwise `false`.
* @since 9.24.0
*/
function isNativeTypeScriptSupportEnabled() {
return (
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- it's still an experimental feature.
["strip", "transform"].includes(process.features.typescript)
);
}
/**
* Load the TypeScript configuration file.
* @param {string} filePath The absolute file path to load.
* @param {URL} fileURL The file URL to load.
* @param {number} mtime The last modified timestamp of the file.
* @returns {Promise<any>} The configuration loaded from the file.
* @since 9.24.0
*/
async function loadTypeScriptConfigFileWithJiti(filePath, fileURL, mtime) {
const { createJiti, version: jitiVersion } =
// eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
await ConfigLoader.loadJiti().catch(() => {
throw new Error(
"The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.",
);
});
// `createJiti` was added in jiti v2.
if (typeof createJiti !== "function") {
throw new Error(
"You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.",
);
}
/*
* Disabling `moduleCache` allows us to reload a
* config file when the last modified timestamp changes.
*/
const jitiOptions = {
moduleCache: false,
};
if (jitiVersion.startsWith("2.1.")) {
jitiOptions.interopDefault = false;
}
const jiti = createJiti(__filename, jitiOptions);
const config = await jiti.import(fileURL.href);
importedConfigFileModificationTime.set(filePath, mtime);
return config?.default ?? config;
}
/**
* Dynamically imports a module from the given file path.
* @param {string} filePath The absolute file path of the module to import.
* @param {URL} fileURL The file URL to load.
* @param {number} mtime The last modified timestamp of the file.
* @returns {Promise<any>} - A {@linkcode Promise | promise} that resolves to the imported ESLint config.
* @since 9.24.0
*/
async function dynamicImportConfig(filePath, fileURL, mtime) {
const module = await import(fileURL.href);
importedConfigFileModificationTime.set(filePath, mtime);
return module.default;
}
/**
* Load the config array from the given filename.
* @param {string} filePath The filename to load from.
* @param {boolean} hasUnstableNativeNodeJsTSConfigFlag The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
* @returns {Promise<any>} The config loaded from the config file.
*/
async function loadConfigFile(filePath, hasUnstableNativeNodeJsTSConfigFlag) {
debug(`Loading config from ${filePath}`);
const fileURL = pathToFileURL(filePath);
debug(`Config file URL is ${fileURL}`);
const mtime = (await fs.stat(filePath)).mtime.getTime();
/*
* Append a query with the config file's modification time (`mtime`) in order
* to import the current version of the config file. Without the query, `import()` would
* cache the config file module by the pathname only, and then always return
* the same version (the one that was actual when the module was imported for the first time).
*
* This ensures that the config file module is loaded and executed again
* if it has been changed since the last time it was imported.
* If it hasn't been changed, `import()` will just return the cached version.
*
* Note that we should not overuse queries (e.g., by appending the current time
* to always reload the config file module) as that could cause memory leaks
* because entries are never removed from the import cache.
*/
fileURL.searchParams.append("mtime", mtime);
/*
* With queries, we can bypass the import cache. However, when import-ing a CJS module,
* Node.js uses the require infrastructure under the hood. That includes the require cache,
* which caches the config file module by its file path (queries have no effect).
* Therefore, we also need to clear the require cache before importing the config file module.
* In order to get the same behavior with ESM and CJS config files, in particular - to reload
* the config file only if it has been changed, we track file modification times and clear
* the require cache only if the file has been changed.
*/
if (importedConfigFileModificationTime.get(filePath) !== mtime) {
delete require.cache[filePath];
}
const isTS = isFileTS(filePath);
const isBun = isRunningInBun();
const isDeno = isRunningInDeno();
/*
* If we are dealing with a TypeScript file, then we need to use `jiti` to load it
* in Node.js. Deno and Bun both allow native importing of TypeScript files.
*
* When Node.js supports native TypeScript imports, we can remove this check.
*/
if (isTS) {
if (hasUnstableNativeNodeJsTSConfigFlag) {
if (isNativeTypeScriptSupportEnabled()) {
return await dynamicImportConfig(filePath, fileURL, mtime);
}
if (!("typescript" in process.features)) {
throw new Error(
"The unstable_native_nodejs_ts_config flag is not supported in older versions of Node.js.",
);
}
throw new Error(
"The unstable_native_nodejs_ts_config flag is enabled, but native TypeScript support is not enabled in the current Node.js process. You need to either enable native TypeScript support by passing --experimental-strip-types or remove the unstable_native_nodejs_ts_config flag.",
);
}
if (!isDeno && !isBun) {
return await loadTypeScriptConfigFileWithJiti(
filePath,
fileURL,
mtime,
);
}
}
// fallback to normal runtime behavior
return await dynamicImportConfig(filePath, fileURL, mtime);
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Encapsulates the loading and caching of configuration files when looking up
* from the file being linted.
*/
class ConfigLoader {
/**
* Map of config file paths to the config arrays for those directories.
* @type {Map<string, FlatConfigArray|Promise<FlatConfigArray>>}
*/
#configArrays = new Map();
/**
* Map of absolute directory names to the config file paths for those directories.
* @type {Map<string, {configFilePath:string,basePath:string}|Promise<{configFilePath:string,basePath:string}>>}
*/
#configFilePaths = new Map();
/**
* The options to use when loading configuration files.
* @type {ConfigLoaderOptions}
*/
#options;
/**
* Creates a new instance.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
*/
constructor(options) {
this.#options = options.warningService
? options
: { ...options, warningService: new WarningService() };
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from `fromDirectory` for a file named `eslint.config.js`.
* @param {string} fromDirectory The directory from which to start searching.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
async #locateConfigFileToUse(fromDirectory) {
// check cache first
if (this.#configFilePaths.has(fromDirectory)) {
return this.#configFilePaths.get(fromDirectory);
}
const resultPromise = ConfigLoader.locateConfigFileToUse({
useConfigFile: this.#options.configFile,
cwd: this.#options.cwd,
fromDirectory,
});
// ensure `ConfigLoader.locateConfigFileToUse` is called only once for `fromDirectory`
this.#configFilePaths.set(fromDirectory, resultPromise);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
const result = await resultPromise;
this.#configFilePaths.set(fromDirectory, result);
return result;
}
/**
* Calculates the config array for this run based on inputs.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
async #calculateConfigArray(configFilePath, basePath) {
// check for cached version first
if (this.#configArrays.has(configFilePath)) {
return this.#configArrays.get(configFilePath);
}
const configsPromise = ConfigLoader.calculateConfigArray(
configFilePath,
basePath,
this.#options,
);
// ensure `ConfigLoader.calculateConfigArray` is called only once for `configFilePath`
this.#configArrays.set(configFilePath, configsPromise);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
const configs = await configsPromise;
this.#configArrays.set(configFilePath, configs);
return configs;
}
/**
* Returns the config file path for the given directory or file. This will either use
* the override config file that was specified in the constructor options or
* search for a config file from the directory.
* @param {string} fileOrDirPath The file or directory path to get the config file path for.
* @returns {Promise<string|undefined>} The config file path or `undefined` if not found.
* @throws {Error} If `fileOrDirPath` is not a non-empty string.
* @throws {Error} If `fileOrDirPath` is not an absolute path.
*/
async findConfigFileForPath(fileOrDirPath) {
assertValidFilePath(fileOrDirPath);
const absoluteDirPath = path.resolve(
this.#options.cwd,
path.dirname(fileOrDirPath),
);
const { configFilePath } =
await this.#locateConfigFileToUse(absoluteDirPath);
return configFilePath;
}
/**
* 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 or directory to retrieve config for.
* @returns {Promise<FlatConfigArray>} A configuration object for the file.
* @throws {Error} If no configuration for `filePath` exists.
*/
async loadConfigArrayForFile(filePath) {
assertValidFilePath(filePath);
debug(`Calculating config for file ${filePath}`);
const configFilePath = await this.findConfigFileForPath(filePath);
assertConfigurationExists(configFilePath, this.#options);
return this.loadConfigArrayForDirectory(filePath);
}
/**
* Returns a configuration object for the given directory 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} dirPath The path of the directory to retrieve config for.
* @returns {Promise<FlatConfigArray>} A configuration object for the directory.
*/
async loadConfigArrayForDirectory(dirPath) {
assertValidFilePath(dirPath);
debug(`Calculating config for directory ${dirPath}`);
const absoluteDirPath = path.resolve(
this.#options.cwd,
path.dirname(dirPath),
);
const { configFilePath, basePath } =
await this.#locateConfigFileToUse(absoluteDirPath);
debug(`Using config file ${configFilePath} and base path ${basePath}`);
return this.#calculateConfigArray(configFilePath, basePath);
}
/**
* Returns a configuration array for the given file based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} filePath The path of the file to retrieve a config object for.
* @returns {FlatConfigArray} A configuration object for the file.
* @throws {Error} If `filePath` is not a non-empty string.
* @throws {Error} If `filePath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForFile(filePath) {
assertValidFilePath(filePath);
debug(`Looking up cached config for ${filePath}`);
return this.getCachedConfigArrayForPath(path.dirname(filePath));
}
/**
* Returns a configuration array for the given directory based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} fileOrDirPath The path of the directory to retrieve a config object for.
* @returns {FlatConfigArray} A configuration object for the directory.
* @throws {Error} If `dirPath` is not a non-empty string.
* @throws {Error} If `dirPath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForPath(fileOrDirPath) {
assertValidFilePath(fileOrDirPath);
debug(`Looking up cached config for ${fileOrDirPath}`);
const absoluteDirPath = path.resolve(this.#options.cwd, fileOrDirPath);
if (!this.#configFilePaths.has(absoluteDirPath)) {
throw new Error(`Could not find config file for ${fileOrDirPath}`);
}
const configFilePathInfo = this.#configFilePaths.get(absoluteDirPath);
if (typeof configFilePathInfo.then === "function") {
throw new Error(
`Config file path for ${fileOrDirPath} has not yet been calculated or an error occurred during the calculation`,
);
}
const { configFilePath } = configFilePathInfo;
const configArray = this.#configArrays.get(configFilePath);
if (!configArray || typeof configArray.then === "function") {
throw new Error(
`Config array for ${fileOrDirPath} has not yet been calculated or an error occurred during the calculation`,
);
}
return configArray;
}
/**
* Used to import the jiti dependency. This method is exposed internally for testing purposes.
* @returns {Promise<{createJiti: Function|undefined, version: string;}>} A promise that fulfills with an object containing the jiti module's createJiti function and version.
*/
static async loadJiti() {
const { createJiti } = await import("jiti");
const version = require("jiti/package.json").version;
return { createJiti, version };
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from `fromDirectory` for a file named `eslint.config.js`.
* This method is exposed internally for testing purposes.
* @param {Object} [options] the options object
* @param {string|false|undefined} options.useConfigFile The path to the config file to use.
* @param {string} options.cwd Path to a directory that should be considered as the current working directory.
* @param {string} [options.fromDirectory] The directory from which to start searching. Defaults to `cwd`.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
static async locateConfigFileToUse({
useConfigFile,
cwd,
fromDirectory = cwd,
}) {
// determine where to load config file from
let configFilePath;
let basePath = cwd;
if (typeof useConfigFile === "string") {
debug(`Override config file path is ${useConfigFile}`);
configFilePath = path.resolve(cwd, useConfigFile);
basePath = cwd;
} else if (useConfigFile !== false) {
debug("Searching for eslint.config.js");
configFilePath = await findUp(FLAT_CONFIG_FILENAMES, {
cwd: fromDirectory,
});
if (configFilePath) {
basePath = path.dirname(configFilePath);
}
}
return {
configFilePath,
basePath,
};
}
/**
* Calculates the config array for this run based on inputs.
* This method is exposed internally for testing purposes.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
static async calculateConfigArray(configFilePath, basePath, options) {
const {
cwd,
baseConfig,
ignoreEnabled,
ignorePatterns,
overrideConfig,
hasUnstableNativeNodeJsTSConfigFlag = false,
defaultConfigs = [],
warningService,
} = options;
debug(
`Calculating config array from config file ${configFilePath} and base path ${basePath}`,
);
const configs = new FlatConfigArray(baseConfig || [], {
basePath,
shouldIgnore: ignoreEnabled,
});
// load config file
if (configFilePath) {
debug(`Loading config file ${configFilePath}`);
const fileConfig = await loadConfigFile(
configFilePath,
hasUnstableNativeNodeJsTSConfigFlag,
);
/*
* It's possible that a config file could be empty or else
* have an empty object or array. In this case, we want to
* warn the user that they have an empty config.
*
* An empty CommonJS file exports an empty object while
* an empty ESM file exports undefined.
*/
let emptyConfig = typeof fileConfig === "undefined";
debug(
`Config file ${configFilePath} is ${emptyConfig ? "empty" : "not empty"}`,
);
if (!emptyConfig) {
if (Array.isArray(fileConfig)) {
if (fileConfig.length === 0) {
debug(
`Config file ${configFilePath} is an empty array`,
);
emptyConfig = true;
} else {
configs.push(...fileConfig);
}
} else {
if (
typeof fileConfig === "object" &&
fileConfig !== null &&
Object.keys(fileConfig).length === 0
) {
debug(
`Config file ${configFilePath} is an empty object`,
);
emptyConfig = true;
} else {
configs.push(fileConfig);
}
}
}
if (emptyConfig) {
warningService.emitEmptyConfigWarning(configFilePath);
}
}
// add in any configured defaults
configs.push(...defaultConfigs);
// append command line ignore patterns
if (ignorePatterns && ignorePatterns.length > 0) {
/*
* Ignore patterns are added to the end of the config array
* so they can override default ignores.
*/
configs.push({
basePath: cwd,
ignores: ignorePatterns,
});
}
if (overrideConfig) {
if (Array.isArray(overrideConfig)) {
configs.push(...overrideConfig);
} else {
configs.push(overrideConfig);
}
}
await configs.normalize();
return configs;
}
}
/**
* Encapsulates the loading and caching of configuration files when looking up
* from the current working directory.
*/
class LegacyConfigLoader extends ConfigLoader {
/**
* The options to use when loading configuration files.
* @type {ConfigLoaderOptions}
*/
#options;
/**
* The cached config file path for this instance.
* @type {Promise<{configFilePath:string,basePath:string}|undefined>}
*/
#configFilePath;
/**
* The cached config array for this instance.
* @type {FlatConfigArray|Promise<FlatConfigArray>}
*/
#configArray;
/**
* Creates a new instance.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
*/
constructor(options) {
const normalizedOptions = options.warningService
? options
: { ...options, warningService: new WarningService() };
super(normalizedOptions);
this.#options = normalizedOptions;
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from the cwd for a file named `eslint.config.js`.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
#locateConfigFileToUse() {
if (!this.#configFilePath) {
this.#configFilePath = ConfigLoader.locateConfigFileToUse({
useConfigFile: this.#options.configFile,
cwd: this.#options.cwd,
});
}
return this.#configFilePath;
}
/**
* Calculates the config array for this run based on inputs.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
async #calculateConfigArray(configFilePath, basePath) {
// check for cached version first
if (this.#configArray) {
return this.#configArray;
}
// ensure `ConfigLoader.calculateConfigArray` is called only once
this.#configArray = ConfigLoader.calculateConfigArray(
configFilePath,
basePath,
this.#options,
);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
this.#configArray = await this.#configArray;
return this.#configArray;
}
/**
* Returns the config file path for the given directory. This will either use
* the override config file that was specified in the constructor options or
* search for a config file from the directory of the file being linted.
* @param {string} dirPath The directory path to get the config file path for.
* @returns {Promise<string|undefined>} The config file path or `undefined` if not found.
* @throws {Error} If `fileOrDirPath` is not a non-empty string.
* @throws {Error} If `fileOrDirPath` is not an absolute path.
*/
async findConfigFileForPath(dirPath) {
assertValidFilePath(dirPath);
const { configFilePath } = await this.#locateConfigFileToUse();
return configFilePath;
}
/**
* 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} dirPath The path of the directory to retrieve config for.
* @returns {Promise<FlatConfigArray>} A configuration object for the file.
*/
async loadConfigArrayForDirectory(dirPath) {
assertValidFilePath(dirPath);
debug(`[Legacy]: Calculating config for ${dirPath}`);
const { configFilePath, basePath } =
await this.#locateConfigFileToUse();
debug(
`[Legacy]: Using config file ${configFilePath} and base path ${basePath}`,
);
return this.#calculateConfigArray(configFilePath, basePath);
}
/**
* Returns a configuration array for the given directory based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} dirPath The path of the directory to retrieve a config object for.
* @returns {FlatConfigArray} A configuration object for the file.
* @throws {Error} If `dirPath` is not a non-empty string.
* @throws {Error} If `dirPath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForPath(dirPath) {
assertValidFilePath(dirPath);
debug(`[Legacy]: Looking up cached config for ${dirPath}`);
if (!this.#configArray) {
throw new Error(`Could not find config file for ${dirPath}`);
}
if (typeof this.#configArray.then === "function") {
throw new Error(
`Config array for ${dirPath} has not yet been calculated or an error occurred during the calculation`,
);
}
return this.#configArray;
}
}
module.exports = { ConfigLoader, LegacyConfigLoader };
-674
View File
@@ -1,674 +0,0 @@
/**
* @fileoverview The `Config` class
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
const { flatConfigSchema, hasMethod } = require("./flat-config-schema");
const { ObjectSchema } = require("@eslint/config-array");
const ajvImport = require("../shared/ajv");
const ajv = ajvImport();
const ruleReplacements = require("../../conf/replacements.json");
//-----------------------------------------------------------------------------
// Typedefs
//-----------------------------------------------------------------------------
/**
* @import { RuleDefinition } from "@eslint/core";
* @import { Linter } from "eslint";
*/
//-----------------------------------------------------------------------------
// Private Members
//------------------------------------------------------------------------------
// JSON schema that disallows passing any options
const noOptionsSchema = Object.freeze({
type: "array",
minItems: 0,
maxItems: 0,
});
const severities = new Map([
[0, 0],
[1, 1],
[2, 2],
["off", 0],
["warn", 1],
["error", 2],
]);
/**
* A collection of compiled validators for rules that have already
* been validated.
* @type {WeakMap}
*/
const validators = new WeakMap();
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Throws a helpful error when a rule cannot be found.
* @param {Object} ruleId The rule identifier.
* @param {string} ruleId.pluginName The ID of the rule to find.
* @param {string} ruleId.ruleName The ID of the rule to find.
* @param {Object} config The config to search in.
* @throws {TypeError} For missing plugin or rule.
* @returns {void}
*/
function throwRuleNotFoundError({ pluginName, ruleName }, config) {
const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`;
const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}" in configuration.`;
const missingPluginErrorMessage = errorMessage;
// if the plugin exists then we need to check if the rule exists
if (config.plugins && config.plugins[pluginName]) {
const replacementRuleName = ruleReplacements.rules[ruleName];
if (pluginName === "@" && replacementRuleName) {
errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`;
} else {
errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;
// otherwise, let's see if we can find the rule name elsewhere
for (const [otherPluginName, otherPlugin] of Object.entries(
config.plugins,
)) {
if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
break;
}
}
}
// falls through to throw error
}
const error = new TypeError(errorMessage);
if (errorMessage === missingPluginErrorMessage) {
error.messageTemplate = "config-plugin-missing";
error.messageData = { pluginName, ruleId };
}
throw error;
}
/**
* The error type when a rule has an invalid `meta.schema`.
*/
class InvalidRuleOptionsSchemaError extends Error {
/**
* Creates a new instance.
* @param {string} ruleId Id of the rule that has an invalid `meta.schema`.
* @param {Error} processingError Error caught while processing the `meta.schema`.
*/
constructor(ruleId, processingError) {
super(
`Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`,
{ cause: processingError },
);
this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA";
}
}
/**
* Parses a ruleId into its plugin and rule parts.
* @param {string} ruleId The rule ID to parse.
* @returns {{pluginName:string,ruleName:string}} The plugin and rule
* parts of the ruleId;
*/
function parseRuleId(ruleId) {
let pluginName, ruleName;
// distinguish between core rules and plugin rules
if (ruleId.includes("/")) {
// mimic scoped npm packages
if (ruleId.startsWith("@")) {
pluginName = ruleId.slice(0, ruleId.lastIndexOf("/"));
} else {
pluginName = ruleId.slice(0, ruleId.indexOf("/"));
}
ruleName = ruleId.slice(pluginName.length + 1);
} else {
pluginName = "@";
ruleName = ruleId;
}
return {
pluginName,
ruleName,
};
}
/**
* Retrieves a rule instance from a given config based on the ruleId.
* @param {string} ruleId The rule ID to look for.
* @param {Linter.Config} config The config to search.
* @returns {RuleDefinition|undefined} The rule if found
* or undefined if not.
*/
function getRuleFromConfig(ruleId, config) {
const { pluginName, ruleName } = parseRuleId(ruleId);
return config.plugins?.[pluginName]?.rules?.[ruleName];
}
/**
* Gets a complete options schema for a rule.
* @param {RuleDefinition} rule A rule object
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
*/
function getRuleOptionsSchema(rule) {
if (!rule.meta) {
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
}
const schema = rule.meta.schema;
if (typeof schema === "undefined") {
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
}
// `schema:false` is an allowed explicit opt-out of options validation for the rule
if (schema === false) {
return null;
}
if (typeof schema !== "object" || schema === null) {
throw new TypeError("Rule's `meta.schema` must be an array or object");
}
// ESLint-specific array form needs to be converted into a valid JSON Schema definition
if (Array.isArray(schema)) {
if (schema.length) {
return {
type: "array",
items: schema,
minItems: 0,
maxItems: schema.length,
};
}
// `schema:[]` is an explicit way to specify that the rule does not accept any options
return { ...noOptionsSchema };
}
// `schema:<object>` is assumed to be a valid JSON Schema definition
return schema;
}
/**
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
* @param {string} identifier The identifier to parse.
* @returns {{objectName: string, pluginName: string}} The parts of the plugin
* name.
*/
function splitPluginIdentifier(identifier) {
const parts = identifier.split("/");
return {
objectName: parts.pop(),
pluginName: parts.join("/"),
};
}
/**
* Returns the name of an object in the config by reading its `meta` key.
* @param {Object} object The object to check.
* @returns {string?} The name of the object if found or `null` if there
* is no name.
*/
function getObjectId(object) {
// first check old-style name
let name = object.name;
if (!name) {
if (!object.meta) {
return null;
}
name = object.meta.name;
if (!name) {
return null;
}
}
// now check for old-style version
let version = object.version;
if (!version) {
version = object.meta && object.meta.version;
}
// if there's a version then append that
if (version) {
return `${name}@${version}`;
}
return name;
}
/**
* Asserts that a value is not a function.
* @param {any} value The value to check.
* @param {string} key The key of the value in the object.
* @param {string} objectKey The key of the object being checked.
* @returns {void}
* @throws {TypeError} If the value is a function.
*/
function assertNotFunction(value, key, objectKey) {
if (typeof value === "function") {
const error = new TypeError(
`Cannot serialize key "${key}" in "${objectKey}": Function values are not supported.`,
);
error.messageTemplate = "config-serialize-function";
error.messageData = { key, objectKey };
throw error;
}
}
/**
* Converts a languageOptions object to a JSON representation.
* @param {Record<string, any>} languageOptions The options to create a JSON
* representation of.
* @param {string} objectKey The key of the object being converted.
* @returns {Record<string, any>} The JSON representation of the languageOptions.
* @throws {TypeError} If a function is found in the languageOptions.
*/
function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
if (typeof languageOptions.toJSON === "function") {
const result = languageOptions.toJSON();
assertNotFunction(result, "toJSON", objectKey);
return result;
}
const result = {};
for (const [key, value] of Object.entries(languageOptions)) {
if (value) {
if (typeof value === "object") {
const name = getObjectId(value);
if (typeof value.toJSON === "function") {
result[key] = value.toJSON();
assertNotFunction(result[key], key, objectKey);
} else if (name && hasMethod(value)) {
result[key] = name;
} else {
result[key] = languageOptionsToJSON(value, key);
}
continue;
}
assertNotFunction(value, key, objectKey);
}
result[key] = value;
}
return result;
}
/**
* Gets or creates a validator for a rule.
* @param {Object} rule The rule to get a validator for.
* @param {string} ruleId The ID of the rule (for error reporting).
* @returns {Function|null} A validation function or null if no validation is needed.
* @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid.
*/
function getOrCreateValidator(rule, ruleId) {
if (!validators.has(rule)) {
try {
const schema = getRuleOptionsSchema(rule);
if (schema) {
validators.set(rule, ajv.compile(schema));
}
} catch (err) {
throw new InvalidRuleOptionsSchemaError(ruleId, err);
}
}
return validators.get(rule);
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Represents a normalized configuration object.
*/
class Config {
/**
* The name to use for the language when serializing to JSON.
* @type {string|undefined}
*/
#languageName;
/**
* The name to use for the processor when serializing to JSON.
* @type {string|undefined}
*/
#processorName;
/**
* Creates a new instance.
* @param {Object} config The configuration object.
*/
constructor(config) {
const { plugins, language, languageOptions, processor, ...otherKeys } =
config;
// Validate config object
const schema = new ObjectSchema(flatConfigSchema);
schema.validate(config);
// first, copy all the other keys over
Object.assign(this, otherKeys);
// ensure that a language is specified
if (!language) {
throw new TypeError("Key 'language' is required.");
}
// copy the rest over
this.plugins = plugins;
this.language = language;
// Check language value
const {
pluginName: languagePluginName,
objectName: localLanguageName,
} = splitPluginIdentifier(language);
this.#languageName = language;
if (
!plugins ||
!plugins[languagePluginName] ||
!plugins[languagePluginName].languages ||
!plugins[languagePluginName].languages[localLanguageName]
) {
throw new TypeError(
`Key "language": Could not find "${localLanguageName}" in plugin "${languagePluginName}".`,
);
}
this.language =
plugins[languagePluginName].languages[localLanguageName];
if (this.language.defaultLanguageOptions ?? languageOptions) {
this.languageOptions = flatConfigSchema.languageOptions.merge(
this.language.defaultLanguageOptions,
languageOptions,
);
} else {
this.languageOptions = {};
}
// Validate language options
try {
this.language.validateLanguageOptions(this.languageOptions);
} catch (error) {
throw new TypeError(`Key "languageOptions": ${error.message}`, {
cause: error,
});
}
// Normalize language options if necessary
if (this.language.normalizeLanguageOptions) {
this.languageOptions = this.language.normalizeLanguageOptions(
this.languageOptions,
);
}
// Check processor value
if (processor) {
this.processor = processor;
if (typeof processor === "string") {
const { pluginName, objectName: localProcessorName } =
splitPluginIdentifier(processor);
this.#processorName = processor;
if (
!plugins ||
!plugins[pluginName] ||
!plugins[pluginName].processors ||
!plugins[pluginName].processors[localProcessorName]
) {
throw new TypeError(
`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`,
);
}
this.processor =
plugins[pluginName].processors[localProcessorName];
} else if (typeof processor === "object") {
this.#processorName = getObjectId(processor);
this.processor = processor;
} else {
throw new TypeError(
"Key 'processor' must be a string or an object.",
);
}
}
// Process the rules
if (this.rules) {
this.#normalizeRulesConfig();
this.validateRulesConfig(this.rules);
}
}
/**
* Converts the configuration to a JSON representation.
* @returns {Record<string, any>} The JSON representation of the configuration.
* @throws {Error} If the configuration cannot be serialized.
*/
toJSON() {
if (this.processor && !this.#processorName) {
throw new Error(
"Could not serialize processor object (missing 'meta' object).",
);
}
if (!this.#languageName) {
throw new Error(
"Could not serialize language object (missing 'meta' object).",
);
}
return {
...this,
plugins: Object.entries(this.plugins).map(([namespace, plugin]) => {
const pluginId = getObjectId(plugin);
if (!pluginId) {
return namespace;
}
return `${namespace}:${pluginId}`;
}),
language: this.#languageName,
languageOptions: languageOptionsToJSON(this.languageOptions),
processor: this.#processorName,
};
}
/**
* Gets a rule configuration by its ID.
* @param {string} ruleId The ID of the rule to get.
* @returns {RuleDefinition|undefined} The rule definition from the plugin, or `undefined` if the rule is not found.
*/
getRuleDefinition(ruleId) {
return getRuleFromConfig(ruleId, this);
}
/**
* Normalizes the rules configuration. Ensures that each rule config is
* an array and that the severity is a number. Applies meta.defaultOptions.
* This function modifies `this.rules`.
* @returns {void}
*/
#normalizeRulesConfig() {
for (const [ruleId, originalConfig] of Object.entries(this.rules)) {
// ensure rule config is an array
let ruleConfig = Array.isArray(originalConfig)
? originalConfig
: [originalConfig];
// normalize severity
ruleConfig[0] = severities.get(ruleConfig[0]);
const rule = getRuleFromConfig(ruleId, this);
// apply meta.defaultOptions
const slicedOptions = ruleConfig.slice(1);
const mergedOptions = deepMergeArrays(
rule?.meta?.defaultOptions,
slicedOptions,
);
if (mergedOptions.length) {
ruleConfig = [ruleConfig[0], ...mergedOptions];
}
this.rules[ruleId] = ruleConfig;
}
}
/**
* Validates all of the rule configurations in the given rules config
* against the plugins in this instance. This is used primarily to
* validate inline configuration rules while inting.
* @param {Object} rulesConfig The rules config to validate.
* @returns {void}
* @throws {Error} If a rule's configuration does not match its schema.
* @throws {TypeError} If the rulesConfig is not provided or is invalid.
* @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid.
* @throws {TypeError} If a rule is not found in the plugins.
*/
validateRulesConfig(rulesConfig) {
if (!rulesConfig) {
throw new TypeError("Config is required for validation.");
}
for (const [ruleId, ruleOptions] of Object.entries(rulesConfig)) {
// check for edge case
if (ruleId === "__proto__") {
continue;
}
/*
* If a rule is disabled, we don't do any validation. This allows
* users to safely set any value to 0 or "off" without worrying
* that it will cause a validation error.
*
* Note: ruleOptions is always an array at this point because
* this validation occurs after FlatConfigArray has merged and
* normalized values.
*/
if (ruleOptions[0] === 0) {
continue;
}
const rule = getRuleFromConfig(ruleId, this);
if (!rule) {
throwRuleNotFoundError(parseRuleId(ruleId), this);
}
const validateRule = getOrCreateValidator(rule, ruleId);
if (validateRule) {
validateRule(ruleOptions.slice(1));
if (validateRule.errors) {
throw new Error(
`Key "rules": Key "${ruleId}":\n${validateRule.errors
.map(error => {
if (
error.keyword === "additionalProperties" &&
error.schema === false &&
typeof error.parentSchema?.properties ===
"object" &&
typeof error.params?.additionalProperty ===
"string"
) {
const expectedProperties = Object.keys(
error.parentSchema.properties,
).map(property => `"${property}"`);
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
}
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
})
.join("")}`,
);
}
}
}
}
/**
* Gets a complete options schema for a rule.
* @param {RuleDefinition} ruleDefinition A rule definition object.
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
*/
static getRuleOptionsSchema(ruleDefinition) {
return getRuleOptionsSchema(ruleDefinition);
}
/**
* 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.
*/
static getRuleNumericSeverity(ruleConfig) {
const severityValue = Array.isArray(ruleConfig)
? ruleConfig[0]
: ruleConfig;
if (severities.has(severityValue)) {
return severities.get(severityValue);
}
if (typeof severityValue === "string") {
return severities.get(severityValue.toLowerCase()) ?? 0;
}
return 0;
}
}
module.exports = { Config };
+49 -60
View File
@@ -15,64 +15,53 @@ const Rules = require("../rules");
// Helpers
//-----------------------------------------------------------------------------
const sharedDefaultConfig = [
// intentionally empty config to ensure these files are globbed by default
{
files: ["**/*.js", "**/*.mjs"],
},
{
files: ["**/*.cjs"],
languageOptions: {
sourceType: "commonjs",
ecmaVersion: "latest",
},
},
exports.defaultConfig = [
{
plugins: {
"@": {
/*
* Because we try to delay loading rules until absolutely
* necessary, a proxy allows us to hook into the lazy-loading
* aspect of the rules map while still keeping all of the
* relevant configuration inside of the config array.
*/
rules: new Proxy({}, {
get(target, property) {
return Rules.get(property);
},
has(target, property) {
return Rules.has(property);
}
})
}
},
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
parser: require("espree"),
parserOptions: {}
}
},
// default ignores are listed here
{
ignores: [
"**/node_modules/",
".git/"
]
},
// intentionally empty config to ensure these files are globbed by default
{
files: ["**/*.js", "**/*.mjs"]
},
{
files: ["**/*.cjs"],
languageOptions: {
sourceType: "commonjs",
ecmaVersion: "latest"
}
}
];
exports.defaultConfig = Object.freeze([
{
plugins: {
"@": {
languages: {
js: require("../languages/js"),
},
/*
* Because we try to delay loading rules until absolutely
* necessary, a proxy allows us to hook into the lazy-loading
* aspect of the rules map while still keeping all of the
* relevant configuration inside of the config array.
*/
rules: new Proxy(
{},
{
get(target, property) {
return Rules.get(property);
},
has(target, property) {
return Rules.has(property);
},
},
),
},
},
language: "@/js",
linterOptions: {
reportUnusedDisableDirectives: 1,
},
},
// default ignores are listed here
{
ignores: ["**/node_modules/", ".git/"],
},
...sharedDefaultConfig,
]);
exports.defaultRuleTesterConfig = Object.freeze([
{ files: ["**"] }, // Make sure the default config matches for all files
...sharedDefaultConfig,
]);
+318 -155
View File
@@ -9,10 +9,11 @@
// Requirements
//-----------------------------------------------------------------------------
const { ConfigArray, ConfigArraySymbol } = require("@eslint/config-array");
const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array");
const { flatConfigSchema } = require("./flat-config-schema");
const { RuleValidator } = require("./rule-validator");
const { defaultConfig } = require("./default-config");
const { Config } = require("./config");
const jsPlugin = require("@eslint/js");
//-----------------------------------------------------------------------------
// Helpers
@@ -21,7 +22,63 @@ const { Config } = require("./config");
/**
* Fields that are considered metadata and not part of the config object.
*/
const META_FIELDS = new Set(["name", "basePath"]);
const META_FIELDS = new Set(["name"]);
const ruleValidator = new RuleValidator();
/**
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
* @param {string} identifier The identifier to parse.
* @returns {{objectName: string, pluginName: string}} The parts of the plugin
* name.
*/
function splitPluginIdentifier(identifier) {
const parts = identifier.split("/");
return {
objectName: parts.pop(),
pluginName: parts.join("/")
};
}
/**
* Returns the name of an object in the config by reading its `meta` key.
* @param {Object} object The object to check.
* @returns {string?} The name of the object if found or `null` if there
* is no name.
*/
function getObjectId(object) {
// first check old-style name
let name = object.name;
if (!name) {
if (!object.meta) {
return null;
}
name = object.meta.name;
if (!name) {
return null;
}
}
// now check for old-style version
let version = object.version;
if (!version) {
version = object.meta && object.meta.version;
}
// if there's a version then append that
if (version) {
return `${name}@${version}`;
}
return name;
}
/**
* Wraps a config error with details about where the error occurred.
@@ -31,39 +88,40 @@ const META_FIELDS = new Set(["name", "basePath"]);
* @returns {TypeError} The new error with details.
*/
function wrapConfigErrorWithDetails(error, originalLength, baseLength) {
let location = "user-defined";
let configIndex = error.index;
/*
* A config array is set up in this order:
* 1. Base config
* 2. Original configs
* 3. User-defined configs
* 4. CLI-defined configs
*
* So we need to adjust the index to account for the base config.
*
* - If the index is less than the base length, it's in the base config
* (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
* - If the index is greater than the base length but less than the original
* length + base length, it's in the original config. The original config
* is passed to the `FlatConfigArray` constructor as the first argument.
* - Otherwise, it's in the user-defined config, which is loaded from the
* config file and merged with any command-line options.
*/
if (error.index < baseLength) {
location = "base";
} else if (error.index < originalLength + baseLength) {
location = "original";
configIndex = error.index - baseLength;
} else {
configIndex = error.index - originalLength - baseLength;
}
let location = "user-defined";
let configIndex = error.index;
return new TypeError(
`${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
{ cause: error },
);
/*
* A config array is set up in this order:
* 1. Base config
* 2. Original configs
* 3. User-defined configs
* 4. CLI-defined configs
*
* So we need to adjust the index to account for the base config.
*
* - If the index is less than the base length, it's in the base config
* (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
* - If the index is greater than the base length but less than the original
* length + base length, it's in the original config. The original config
* is passed to the `FlatConfigArray` constructor as the first argument.
* - Otherwise, it's in the user-defined config, which is loaded from the
* config file and merged with any command-line options.
*/
if (error.index < baseLength) {
location = "base";
} else if (error.index < originalLength + baseLength) {
location = "original";
configIndex = error.index - baseLength;
} else {
configIndex = error.index - originalLength - baseLength;
}
return new TypeError(
`${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
{ cause: error }
);
}
const originalBaseConfig = Symbol("originalBaseConfig");
@@ -78,140 +136,245 @@ const baseLength = Symbol("baseLength");
* Represents an array containing configuration information for ESLint.
*/
class FlatConfigArray extends ConfigArray {
/**
* Creates a new instance.
* @param {*[]} configs An array of configuration information.
* @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options
* to use for the config array instance.
*/
constructor(
configs,
{ basePath, shouldIgnore = true, baseConfig = defaultConfig } = {},
) {
super(configs, {
basePath,
schema: flatConfigSchema,
});
/**
* The original length of the array before any modifications.
* @type {number}
*/
this[originalLength] = this.length;
/**
* Creates a new instance.
* @param {*[]} configs An array of configuration information.
* @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options
* to use for the config array instance.
*/
constructor(configs, {
basePath,
shouldIgnore = true,
baseConfig = defaultConfig
} = {}) {
super(configs, {
basePath,
schema: flatConfigSchema
});
if (baseConfig[Symbol.iterator]) {
this.unshift(...baseConfig);
} else {
this.unshift(baseConfig);
}
/**
* The original length of the array before any modifications.
* @type {number}
*/
this[originalLength] = this.length;
/**
* The length of the array after applying the base config.
* @type {number}
*/
this[baseLength] = this.length - this[originalLength];
if (baseConfig[Symbol.iterator]) {
this.unshift(...baseConfig);
} else {
this.unshift(baseConfig);
}
/**
* The base config used to build the config array.
* @type {Array<FlatConfig>}
*/
this[originalBaseConfig] = baseConfig;
Object.defineProperty(this, originalBaseConfig, { writable: false });
/**
* The length of the array after applying the base config.
* @type {number}
*/
this[baseLength] = this.length - this[originalLength];
/**
* Determines if `ignores` fields should be honored.
* If true, then all `ignores` fields are honored.
* if false, then only `ignores` fields in the baseConfig are honored.
* @type {boolean}
*/
this.shouldIgnore = shouldIgnore;
Object.defineProperty(this, "shouldIgnore", { writable: false });
}
/**
* The base config used to build the config array.
* @type {Array<FlatConfig>}
*/
this[originalBaseConfig] = baseConfig;
Object.defineProperty(this, originalBaseConfig, { writable: false });
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
*/
normalize(context) {
return super.normalize(context).catch(error => {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(
error,
this[originalLength],
this[baseLength],
);
}
/**
* Determines if `ignores` fields should be honored.
* If true, then all `ignores` fields are honored.
* if false, then only `ignores` fields in the baseConfig are honored.
* @type {boolean}
*/
this.shouldIgnore = shouldIgnore;
Object.defineProperty(this, "shouldIgnore", { writable: false });
}
throw error;
});
}
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
*/
normalize(context) {
return super.normalize(context)
.catch(error => {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
}
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {FlatConfigArray} The current instance.
* @throws {TypeError} If the config is invalid.
*/
normalizeSync(context) {
try {
return super.normalizeSync(context);
} catch (error) {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(
error,
this[originalLength],
this[baseLength],
);
}
throw error;
throw error;
}
}
});
}
/* eslint-disable class-methods-use-this -- Desired as instance method */
/**
* Replaces a config with another config to allow us to put strings
* in the config array that will be replaced by objects before
* normalization.
* @param {Object} config The config to preprocess.
* @returns {Object} The preprocessed config.
*/
[ConfigArraySymbol.preprocessConfig](config) {
/*
* If a config object has `ignores` and no other non-meta fields, then it's an object
* for global ignores. If `shouldIgnore` is false, that object shouldn't apply,
* so we'll remove its `ignores`.
*/
if (
!this.shouldIgnore &&
!this[originalBaseConfig].includes(config) &&
config.ignores &&
Object.keys(config).filter(key => !META_FIELDS.has(key)).length ===
1
) {
/* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
const { ignores, ...otherKeys } = config;
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {FlatConfigArray} The current instance.
* @throws {TypeError} If the config is invalid.
*/
normalizeSync(context) {
return otherKeys;
}
try {
return config;
}
return super.normalizeSync(context);
} catch (error) {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
}
throw error;
}
}
/* eslint-disable class-methods-use-this -- Desired as instance method */
/**
* Replaces a config with another config to allow us to put strings
* in the config array that will be replaced by objects before
* normalization.
* @param {Object} config The config to preprocess.
* @returns {Object} The preprocessed config.
*/
[ConfigArraySymbol.preprocessConfig](config) {
if (config === "eslint:recommended") {
// if we are in a Node.js environment warn the user
if (typeof process !== "undefined" && process.emitWarning) {
process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config.");
}
return jsPlugin.configs.recommended;
}
if (config === "eslint:all") {
// if we are in a Node.js environment warn the user
if (typeof process !== "undefined" && process.emitWarning) {
process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config.");
}
return jsPlugin.configs.all;
}
/*
* If a config object has `ignores` and no other non-meta fields, then it's an object
* for global ignores. If `shouldIgnore` is false, that object shouldn't apply,
* so we'll remove its `ignores`.
*/
if (
!this.shouldIgnore &&
!this[originalBaseConfig].includes(config) &&
config.ignores &&
Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1
) {
/* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
const { ignores, ...otherKeys } = config;
return otherKeys;
}
return config;
}
/**
* Finalizes the config by replacing plugin references with their objects
* and validating rule option schemas.
* @param {Object} config The config to finalize.
* @returns {Object} The finalized config.
* @throws {TypeError} If the config is invalid.
*/
[ConfigArraySymbol.finalizeConfig](config) {
const { plugins, languageOptions, processor } = config;
let parserName, processorName;
let invalidParser = false,
invalidProcessor = false;
// Check parser value
if (languageOptions && languageOptions.parser) {
const { parser } = languageOptions;
if (typeof parser === "object") {
parserName = getObjectId(parser);
if (!parserName) {
invalidParser = true;
}
} else {
invalidParser = true;
}
}
// Check processor value
if (processor) {
if (typeof processor === "string") {
const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);
processorName = processor;
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
}
config.processor = plugins[pluginName].processors[localProcessorName];
} else if (typeof processor === "object") {
processorName = getObjectId(processor);
if (!processorName) {
invalidProcessor = true;
}
} else {
invalidProcessor = true;
}
}
ruleValidator.validate(config);
// apply special logic for serialization into JSON
/* eslint-disable object-shorthand -- shorthand would change "this" value */
Object.defineProperty(config, "toJSON", {
value: function() {
if (invalidParser) {
throw new Error("Could not serialize parser object (missing 'meta' object).");
}
if (invalidProcessor) {
throw new Error("Could not serialize processor object (missing 'meta' object).");
}
return {
...this,
plugins: Object.entries(plugins).map(([namespace, plugin]) => {
const pluginId = getObjectId(plugin);
if (!pluginId) {
return namespace;
}
return `${namespace}:${pluginId}`;
}),
languageOptions: {
...languageOptions,
parser: parserName
},
processor: processorName
};
}
});
/* eslint-enable object-shorthand -- ok to enable now */
return config;
}
/* eslint-enable class-methods-use-this -- Desired as instance method */
/**
* Finalizes the config by replacing plugin references with their objects
* and validating rule option schemas.
* @param {Object} config The config to finalize.
* @returns {Object} The finalized config.
* @throws {TypeError} If the config is invalid.
*/
[ConfigArraySymbol.finalizeConfig](config) {
return new Config(config);
}
/* eslint-enable class-methods-use-this -- Desired as instance method */
}
exports.FlatConfigArray = FlatConfigArray;
+111
View File
@@ -0,0 +1,111 @@
/**
* @fileoverview Shared functions to work with configs.
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------------
/**
* Parses a ruleId into its plugin and rule parts.
* @param {string} ruleId The rule ID to parse.
* @returns {{pluginName:string,ruleName:string}} The plugin and rule
* parts of the ruleId;
*/
function parseRuleId(ruleId) {
let pluginName, ruleName;
// distinguish between core rules and plugin rules
if (ruleId.includes("/")) {
// mimic scoped npm packages
if (ruleId.startsWith("@")) {
pluginName = ruleId.slice(0, ruleId.lastIndexOf("/"));
} else {
pluginName = ruleId.slice(0, ruleId.indexOf("/"));
}
ruleName = ruleId.slice(pluginName.length + 1);
} else {
pluginName = "@";
ruleName = ruleId;
}
return {
pluginName,
ruleName
};
}
/**
* Retrieves a rule instance from a given config based on the ruleId.
* @param {string} ruleId The rule ID to look for.
* @param {FlatConfig} config The config to search.
* @returns {import("../shared/types").Rule|undefined} The rule if found
* or undefined if not.
*/
function getRuleFromConfig(ruleId, config) {
const { pluginName, ruleName } = parseRuleId(ruleId);
const plugin = config.plugins && config.plugins[pluginName];
let rule = plugin && plugin.rules && plugin.rules[ruleName];
// normalize function rules into objects
if (rule && typeof rule === "function") {
rule = {
create: rule
};
}
return rule;
}
/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
* @returns {Object} JSON Schema for the rule's options.
*/
function getRuleOptionsSchema(rule) {
if (!rule) {
return null;
}
const schema = rule.schema || rule.meta && rule.meta.schema;
if (Array.isArray(schema)) {
if (schema.length) {
return {
type: "array",
items: schema,
minItems: 0,
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
}
// Given a full schema, leave it alone
return schema || null;
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
module.exports = {
parseRuleId,
getRuleFromConfig,
getRuleOptionsSchema
};
+355 -355
View File
@@ -9,6 +9,11 @@
// Requirements
//-----------------------------------------------------------------------------
/*
* Note: This can be removed in ESLint v9 because structuredClone is available globally
* starting in Node.js v17.
*/
const structuredClone = require("@ungap/structured-clone").default;
const { normalizeSeverityToNumber } = require("../shared/severity");
//-----------------------------------------------------------------------------
@@ -28,12 +33,15 @@ const { normalizeSeverityToNumber } = require("../shared/severity");
//-----------------------------------------------------------------------------
const ruleSeverities = new Map([
[0, 0],
["off", 0],
[1, 1],
["warn", 1],
[2, 2],
["error", 2],
[0, 0], ["off", 0],
[1, 1], ["warn", 1],
[2, 2], ["error", 2]
]);
const globalVariablesValues = new Set([
true, "true", "writable", "writeable",
false, "false", "readonly", "readable", null,
"off"
]);
/**
@@ -42,7 +50,7 @@ const ruleSeverities = new Map([
* @returns {boolean} `true` if the value is a non-null object.
*/
function isNonNullObject(value) {
return typeof value === "object" && value !== null;
return typeof value === "object" && value !== null;
}
/**
@@ -51,7 +59,7 @@ function isNonNullObject(value) {
* @returns {boolean} `true` if the value is a non-null non-array object.
*/
function isNonArrayObject(value) {
return isNonNullObject(value) && !Array.isArray(value);
return isNonNullObject(value) && !Array.isArray(value);
}
/**
@@ -60,7 +68,7 @@ function isNonArrayObject(value) {
* @returns {boolean} `true` if the value is undefined.
*/
function isUndefined(value) {
return typeof value === "undefined";
return typeof value === "undefined";
}
/**
@@ -71,56 +79,57 @@ function isUndefined(value) {
* @returns {Object} An object with properties from both first and second.
*/
function deepMerge(first, second, mergeMap = new Map()) {
let secondMergeMap = mergeMap.get(first);
if (secondMergeMap) {
const result = secondMergeMap.get(second);
let secondMergeMap = mergeMap.get(first);
if (result) {
// If this combination of first and second arguments has been already visited, return the previously created result.
return result;
}
} else {
secondMergeMap = new Map();
mergeMap.set(first, secondMergeMap);
}
if (secondMergeMap) {
const result = secondMergeMap.get(second);
/*
* First create a result object where properties from the second object
* overwrite properties from the first. This sets up a baseline to use
* later rather than needing to inspect and change every property
* individually.
*/
const result = {
...first,
...second,
};
if (result) {
delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__"
// If this combination of first and second arguments has been already visited, return the previously created result.
return result;
}
} else {
secondMergeMap = new Map();
mergeMap.set(first, secondMergeMap);
}
// Store the pending result for this combination of first and second arguments.
secondMergeMap.set(second, result);
/*
* First create a result object where properties from the second object
* overwrite properties from the first. This sets up a baseline to use
* later rather than needing to inspect and change every property
* individually.
*/
const result = {
...first,
...second
};
for (const key of Object.keys(second)) {
// avoid hairy edge case
if (
key === "__proto__" ||
!Object.prototype.propertyIsEnumerable.call(first, key)
) {
continue;
}
delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__"
const firstValue = first[key];
const secondValue = second[key];
// Store the pending result for this combination of first and second arguments.
secondMergeMap.set(second, result);
if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) {
result[key] = deepMerge(firstValue, secondValue, mergeMap);
} else if (isUndefined(secondValue)) {
result[key] = firstValue;
}
}
for (const key of Object.keys(second)) {
// avoid hairy edge case
if (key === "__proto__" || !Object.prototype.propertyIsEnumerable.call(first, key)) {
continue;
}
const firstValue = first[key];
const secondValue = second[key];
if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) {
result[key] = deepMerge(firstValue, secondValue, mergeMap);
} else if (isUndefined(secondValue)) {
result[key] = firstValue;
}
}
return result;
return result;
}
/**
@@ -130,27 +139,13 @@ function deepMerge(first, second, mergeMap = new Map()) {
* @returns {Array} An array of rule options.
*/
function normalizeRuleOptions(ruleOptions) {
const finalOptions = Array.isArray(ruleOptions)
? ruleOptions.slice(0)
: [ruleOptions];
finalOptions[0] = ruleSeverities.get(finalOptions[0]);
return structuredClone(finalOptions);
}
const finalOptions = Array.isArray(ruleOptions)
? ruleOptions.slice(0)
: [ruleOptions];
/**
* Determines if an object has any methods.
* @param {Object} object The object to check.
* @returns {boolean} `true` if the object has any methods.
*/
function hasMethod(object) {
for (const key of Object.keys(object)) {
if (typeof object[key] === "function") {
return true;
}
}
return false;
finalOptions[0] = ruleSeverities.get(finalOptions[0]);
return structuredClone(finalOptions);
}
//-----------------------------------------------------------------------------
@@ -161,17 +156,16 @@ function hasMethod(object) {
* The error type when a rule's options are configured with an invalid type.
*/
class InvalidRuleOptionsError extends Error {
/**
* @param {string} ruleId Rule name being configured.
* @param {any} value The invalid value.
*/
constructor(ruleId, value) {
super(
`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`,
);
this.messageTemplate = "invalid-rule-options";
this.messageData = { ruleId, value };
}
/**
* @param {string} ruleId Rule name being configured.
* @param {any} value The invalid value.
*/
constructor(ruleId, value) {
super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
this.messageTemplate = "invalid-rule-options";
this.messageData = { ruleId, value };
}
}
/**
@@ -182,30 +176,25 @@ class InvalidRuleOptionsError extends Error {
* @throws {InvalidRuleOptionsError} If the value isn't a valid rule options.
*/
function assertIsRuleOptions(ruleId, value) {
if (
typeof value !== "string" &&
typeof value !== "number" &&
!Array.isArray(value)
) {
throw new InvalidRuleOptionsError(ruleId, value);
}
if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
throw new InvalidRuleOptionsError(ruleId, value);
}
}
/**
* The error type when a rule's severity is invalid.
*/
class InvalidRuleSeverityError extends Error {
/**
* @param {string} ruleId Rule name being configured.
* @param {any} value The invalid value.
*/
constructor(ruleId, value) {
super(
`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`,
);
this.messageTemplate = "invalid-rule-severity";
this.messageData = { ruleId, value };
}
/**
* @param {string} ruleId Rule name being configured.
* @param {any} value The invalid value.
*/
constructor(ruleId, value) {
super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
this.messageTemplate = "invalid-rule-severity";
this.messageData = { ruleId, value };
}
}
/**
@@ -216,11 +205,11 @@ class InvalidRuleSeverityError extends Error {
* @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
*/
function assertIsRuleSeverity(ruleId, value) {
const severity = ruleSeverities.get(value);
const severity = ruleSeverities.get(value);
if (typeof severity === "undefined") {
throw new InvalidRuleSeverityError(ruleId, value);
}
if (typeof severity === "undefined") {
throw new InvalidRuleSeverityError(ruleId, value);
}
}
/**
@@ -230,11 +219,9 @@ function assertIsRuleSeverity(ruleId, value) {
* @throws {TypeError} If the string isn't in the correct format.
*/
function assertIsPluginMemberName(value) {
if (!/[\w\-@$]+(?:\/[\w\-$]+)+$/iu.test(value)) {
throw new TypeError(
`Expected string in the form "pluginName/objectName" but found "${value}".`,
);
}
if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
}
}
/**
@@ -244,98 +231,79 @@ function assertIsPluginMemberName(value) {
* @throws {TypeError} If the value isn't an object.
*/
function assertIsObject(value) {
if (!isNonNullObject(value)) {
throw new TypeError("Expected an object.");
}
if (!isNonNullObject(value)) {
throw new TypeError("Expected an object.");
}
}
/**
* The error type when there's an eslintrc-style options in a flat config.
*/
class IncompatibleKeyError extends Error {
/**
* @param {string} key The invalid key.
*/
constructor(key) {
super(
"This appears to be in eslintrc format rather than flat config format.",
);
this.messageTemplate = "eslintrc-incompat";
this.messageData = { key };
}
/**
* @param {string} key The invalid key.
*/
constructor(key) {
super("This appears to be in eslintrc format rather than flat config format.");
this.messageTemplate = "eslintrc-incompat";
this.messageData = { key };
}
}
/**
* The error type when there's an eslintrc-style plugins array found.
*/
class IncompatiblePluginsError extends Error {
/**
* Creates a new instance.
* @param {Array<string>} plugins The plugins array.
*/
constructor(plugins) {
super(
"This appears to be in eslintrc format (array of strings) rather than flat config format (object).",
);
this.messageTemplate = "eslintrc-plugins";
this.messageData = { plugins };
}
/**
* Creates a new instance.
* @param {Array<string>} plugins The plugins array.
*/
constructor(plugins) {
super("This appears to be in eslintrc format (array of strings) rather than flat config format (object).");
this.messageTemplate = "eslintrc-plugins";
this.messageData = { plugins };
}
}
//-----------------------------------------------------------------------------
// Low-Level Schemas
//-----------------------------------------------------------------------------
/** @type {ObjectPropertySchema} */
const booleanSchema = {
merge: "replace",
validate: "boolean",
merge: "replace",
validate: "boolean"
};
const ALLOWED_SEVERITIES = new Set(["error", "warn", "off", 2, 1, 0]);
/** @type {ObjectPropertySchema} */
const disableDirectiveSeveritySchema = {
merge(first, second) {
const value = second === void 0 ? first : second;
merge(first, second) {
const value = second === void 0 ? first : second;
if (typeof value === "boolean") {
return value ? "warn" : "off";
}
if (typeof value === "boolean") {
return value ? "warn" : "off";
}
return normalizeSeverityToNumber(value);
},
validate(value) {
if (!(ALLOWED_SEVERITIES.has(value) || typeof value === "boolean")) {
throw new TypeError(
'Expected one of: "error", "warn", "off", 0, 1, 2, or a boolean.',
);
}
},
};
/** @type {ObjectPropertySchema} */
const unusedInlineConfigsSeveritySchema = {
merge(first, second) {
const value = second === void 0 ? first : second;
return normalizeSeverityToNumber(value);
},
validate(value) {
if (!ALLOWED_SEVERITIES.has(value)) {
throw new TypeError(
'Expected one of: "error", "warn", "off", 0, 1, or 2.',
);
}
},
return normalizeSeverityToNumber(value);
},
validate(value) {
if (!(ALLOWED_SEVERITIES.has(value) || typeof value === "boolean")) {
throw new TypeError("Expected one of: \"error\", \"warn\", \"off\", 0, 1, 2, or a boolean.");
}
}
};
/** @type {ObjectPropertySchema} */
const deepObjectAssignSchema = {
merge(first = {}, second = {}) {
return deepMerge(first, second);
},
validate: "object",
merge(first = {}, second = {}) {
return deepMerge(first, second);
},
validate: "object"
};
//-----------------------------------------------------------------------------
@@ -343,194 +311,221 @@ const deepObjectAssignSchema = {
//-----------------------------------------------------------------------------
/** @type {ObjectPropertySchema} */
const languageOptionsSchema = {
merge(first = {}, second = {}) {
const result = deepMerge(first, second);
const globalsSchema = {
merge: "assign",
validate(value) {
for (const [key, value] of Object.entries(result)) {
/*
* Special case: Because the `parser` property is an object, it should
* not be deep merged. Instead, it should be replaced if it exists in
* the second object. To make this more generic, we just check for
* objects with methods and replace them if they exist in the second
* object.
*/
if (isNonArrayObject(value)) {
if (hasMethod(value)) {
result[key] = second[key] ?? first[key];
continue;
}
assertIsObject(value);
// for other objects, make sure we aren't reusing the same object
result[key] = { ...result[key] };
continue;
}
}
for (const key of Object.keys(value)) {
return result;
},
validate: "object",
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (key !== key.trim()) {
throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
}
if (!globalVariablesValues.has(value[key])) {
throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
}
}
}
};
/** @type {ObjectPropertySchema} */
const languageSchema = {
merge: "replace",
validate: assertIsPluginMemberName,
const parserSchema = {
merge: "replace",
validate(value) {
if (!value || typeof value !== "object" ||
(typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
) {
throw new TypeError("Expected object with parse() or parseForESLint() method.");
}
}
};
/** @type {ObjectPropertySchema} */
const pluginsSchema = {
merge(first = {}, second = {}) {
const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
const result = {};
merge(first = {}, second = {}) {
const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
const result = {};
// manually validate that plugins are not redefined
for (const key of keys) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
// manually validate that plugins are not redefined
for (const key of keys) {
if (key in first && key in second && first[key] !== second[key]) {
throw new TypeError(`Cannot redefine plugin "${key}".`);
}
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
result[key] = second[key] || first[key];
}
if (key in first && key in second && first[key] !== second[key]) {
throw new TypeError(`Cannot redefine plugin "${key}".`);
}
return result;
},
validate(value) {
// first check the value to be sure it's an object
if (value === null || typeof value !== "object") {
throw new TypeError("Expected an object.");
}
result[key] = second[key] || first[key];
}
// make sure it's not an array, which would mean eslintrc-style is used
if (Array.isArray(value)) {
throw new IncompatiblePluginsError(value);
}
return result;
},
validate(value) {
// second check the keys to make sure they are objects
for (const key of Object.keys(value)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
// first check the value to be sure it's an object
if (value === null || typeof value !== "object") {
throw new TypeError("Expected an object.");
}
if (value[key] === null || typeof value[key] !== "object") {
throw new TypeError(`Key "${key}": Expected an object.`);
}
}
},
// make sure it's not an array, which would mean eslintrc-style is used
if (Array.isArray(value)) {
throw new IncompatiblePluginsError(value);
}
// second check the keys to make sure they are objects
for (const key of Object.keys(value)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (value[key] === null || typeof value[key] !== "object") {
throw new TypeError(`Key "${key}": Expected an object.`);
}
}
}
};
/** @type {ObjectPropertySchema} */
const processorSchema = {
merge: "replace",
validate(value) {
if (typeof value === "string") {
assertIsPluginMemberName(value);
} else if (value && typeof value === "object") {
if (
typeof value.preprocess !== "function" ||
typeof value.postprocess !== "function"
) {
throw new TypeError(
"Object must have a preprocess() and a postprocess() method.",
);
}
} else {
throw new TypeError("Expected an object or a string.");
}
},
merge: "replace",
validate(value) {
if (typeof value === "string") {
assertIsPluginMemberName(value);
} else if (value && typeof value === "object") {
if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
throw new TypeError("Object must have a preprocess() and a postprocess() method.");
}
} else {
throw new TypeError("Expected an object or a string.");
}
}
};
/** @type {ObjectPropertySchema} */
const rulesSchema = {
merge(first = {}, second = {}) {
const result = {
...first,
...second,
};
merge(first = {}, second = {}) {
for (const ruleId of Object.keys(result)) {
try {
// avoid hairy edge case
if (ruleId === "__proto__") {
/* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
delete result.__proto__;
continue;
}
const result = {
...first,
...second
};
result[ruleId] = normalizeRuleOptions(result[ruleId]);
/*
* If either rule config is missing, then the correct
* config is already present and we just need to normalize
* the severity.
*/
if (!(ruleId in first) || !(ruleId in second)) {
continue;
}
for (const ruleId of Object.keys(result)) {
const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
try {
/*
* If the second rule config only has a severity (length of 1),
* then use that severity and keep the rest of the options from
* the first rule config.
*/
if (secondRuleOptions.length === 1) {
result[ruleId] = [
secondRuleOptions[0],
...firstRuleOptions.slice(1),
];
continue;
}
// avoid hairy edge case
if (ruleId === "__proto__") {
/*
* In any other situation, then the second rule config takes
* precedence. That means the value at `result[ruleId]` is
* already correct and no further work is necessary.
*/
} catch (ex) {
throw new Error(`Key "${ruleId}": ${ex.message}`, {
cause: ex,
});
}
}
/* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
delete result.__proto__;
continue;
}
return result;
},
result[ruleId] = normalizeRuleOptions(result[ruleId]);
validate(value) {
assertIsObject(value);
/*
* If either rule config is missing, then the correct
* config is already present and we just need to normalize
* the severity.
*/
if (!(ruleId in first) || !(ruleId in second)) {
continue;
}
/*
* We are not checking the rule schema here because there is no
* guarantee that the rule definition is present at this point. Instead
* we wait and check the rule schema during the finalization step
* of calculating a config.
*/
for (const ruleId of Object.keys(value)) {
// avoid hairy edge case
if (ruleId === "__proto__") {
continue;
}
const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
const ruleOptions = value[ruleId];
/*
* If the second rule config only has a severity (length of 1),
* then use that severity and keep the rest of the options from
* the first rule config.
*/
if (secondRuleOptions.length === 1) {
result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
continue;
}
assertIsRuleOptions(ruleId, ruleOptions);
/*
* In any other situation, then the second rule config takes
* precedence. That means the value at `result[ruleId]` is
* already correct and no further work is necessary.
*/
} catch (ex) {
throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex });
}
if (Array.isArray(ruleOptions)) {
assertIsRuleSeverity(ruleId, ruleOptions[0]);
} else {
assertIsRuleSeverity(ruleId, ruleOptions);
}
}
},
}
return result;
},
validate(value) {
assertIsObject(value);
/*
* We are not checking the rule schema here because there is no
* guarantee that the rule definition is present at this point. Instead
* we wait and check the rule schema during the finalization step
* of calculating a config.
*/
for (const ruleId of Object.keys(value)) {
// avoid hairy edge case
if (ruleId === "__proto__") {
continue;
}
const ruleOptions = value[ruleId];
assertIsRuleOptions(ruleId, ruleOptions);
if (Array.isArray(ruleOptions)) {
assertIsRuleSeverity(ruleId, ruleOptions[0]);
} else {
assertIsRuleSeverity(ruleId, ruleOptions);
}
}
}
};
/** @type {ObjectPropertySchema} */
const ecmaVersionSchema = {
merge: "replace",
validate(value) {
if (typeof value === "number" || value === "latest") {
return;
}
throw new TypeError("Expected a number or \"latest\".");
}
};
/** @type {ObjectPropertySchema} */
const sourceTypeSchema = {
merge: "replace",
validate(value) {
if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
}
}
};
/**
@@ -540,25 +535,25 @@ const rulesSchema = {
* @returns {ObjectPropertySchema} The schema.
*/
function createEslintrcErrorSchema(key) {
return {
merge: "replace",
validate() {
throw new IncompatibleKeyError(key);
},
};
return {
merge: "replace",
validate() {
throw new IncompatibleKeyError(key);
}
};
}
const eslintrcKeys = [
"env",
"extends",
"globals",
"ignorePatterns",
"noInlineConfig",
"overrides",
"parser",
"parserOptions",
"reportUnusedDisableDirectives",
"root",
"env",
"extends",
"globals",
"ignorePatterns",
"noInlineConfig",
"overrides",
"parser",
"parserOptions",
"reportUnusedDisableDirectives",
"root"
];
//-----------------------------------------------------------------------------
@@ -566,25 +561,30 @@ const eslintrcKeys = [
//-----------------------------------------------------------------------------
const flatConfigSchema = {
// eslintrc-style keys that should always error
...Object.fromEntries(
eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)]),
),
// flat config keys
settings: deepObjectAssignSchema,
linterOptions: {
schema: {
noInlineConfig: booleanSchema,
reportUnusedDisableDirectives: disableDirectiveSeveritySchema,
reportUnusedInlineConfigs: unusedInlineConfigsSeveritySchema,
},
},
language: languageSchema,
languageOptions: languageOptionsSchema,
processor: processorSchema,
plugins: pluginsSchema,
rules: rulesSchema,
// eslintrc-style keys that should always error
...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])),
// flat config keys
settings: deepObjectAssignSchema,
linterOptions: {
schema: {
noInlineConfig: booleanSchema,
reportUnusedDisableDirectives: disableDirectiveSeveritySchema
}
},
languageOptions: {
schema: {
ecmaVersion: ecmaVersionSchema,
sourceType: sourceTypeSchema,
globals: globalsSchema,
parser: parserSchema,
parserOptions: deepObjectAssignSchema
}
},
processor: processorSchema,
plugins: pluginsSchema,
rules: rulesSchema
};
//-----------------------------------------------------------------------------
@@ -592,7 +592,7 @@ const flatConfigSchema = {
//-----------------------------------------------------------------------------
module.exports = {
flatConfigSchema,
hasMethod,
assertIsRuleSeverity,
flatConfigSchema,
assertIsRuleSeverity,
assertIsRuleOptions
};
+158
View File
@@ -0,0 +1,158 @@
/**
* @fileoverview Rule Validator
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const ajvImport = require("../shared/ajv");
const ajv = ajvImport();
const {
parseRuleId,
getRuleFromConfig,
getRuleOptionsSchema
} = require("./flat-config-helpers");
const ruleReplacements = require("../../conf/replacements.json");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Throws a helpful error when a rule cannot be found.
* @param {Object} ruleId The rule identifier.
* @param {string} ruleId.pluginName The ID of the rule to find.
* @param {string} ruleId.ruleName The ID of the rule to find.
* @param {Object} config The config to search in.
* @throws {TypeError} For missing plugin or rule.
* @returns {void}
*/
function throwRuleNotFoundError({ pluginName, ruleName }, config) {
const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`;
const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}".`;
// if the plugin exists then we need to check if the rule exists
if (config.plugins && config.plugins[pluginName]) {
const replacementRuleName = ruleReplacements.rules[ruleName];
if (pluginName === "@" && replacementRuleName) {
errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`;
} else {
errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;
// otherwise, let's see if we can find the rule name elsewhere
for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) {
if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
break;
}
}
}
// falls through to throw error
}
throw new TypeError(errorMessage);
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Implements validation functionality for the rules portion of a config.
*/
class RuleValidator {
/**
* Creates a new instance.
*/
constructor() {
/**
* A collection of compiled validators for rules that have already
* been validated.
* @type {WeakMap}
*/
this.validators = new WeakMap();
}
/**
* Validates all of the rule configurations in a config against each
* rule's schema.
* @param {Object} config The full config to validate. This object must
* contain both the rules section and the plugins section.
* @returns {void}
* @throws {Error} If a rule's configuration does not match its schema.
*/
validate(config) {
if (!config.rules) {
return;
}
for (const [ruleId, ruleOptions] of Object.entries(config.rules)) {
// check for edge case
if (ruleId === "__proto__") {
continue;
}
/*
* If a rule is disabled, we don't do any validation. This allows
* users to safely set any value to 0 or "off" without worrying
* that it will cause a validation error.
*
* Note: ruleOptions is always an array at this point because
* this validation occurs after FlatConfigArray has merged and
* normalized values.
*/
if (ruleOptions[0] === 0) {
continue;
}
const rule = getRuleFromConfig(ruleId, config);
if (!rule) {
throwRuleNotFoundError(parseRuleId(ruleId), config);
}
// Precompile and cache validator the first time
if (!this.validators.has(rule)) {
const schema = getRuleOptionsSchema(rule);
if (schema) {
this.validators.set(rule, ajv.compile(schema));
}
}
const validateRule = this.validators.get(rule);
if (validateRule) {
validateRule(ruleOptions.slice(1));
if (validateRule.errors) {
throw new Error(`Key "rules": Key "${ruleId}": ${
validateRule.errors.map(
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
).join("")
}`);
}
}
}
}
}
exports.RuleValidator = RuleValidator;
File diff suppressed because it is too large. Load diff
+616 -1271
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,9 +1,9 @@
"use strict";
const { ESLint } = require("./eslint");
const { LegacyESLint } = require("./legacy-eslint");
const { FlatESLint } = require("./flat-eslint");
module.exports = {
ESLint,
LegacyESLint,
ESLint,
FlatESLint
};
-786
View File
@@ -1,786 +0,0 @@
/**
* @fileoverview Main API Class
* @author Kai Cataldo
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("node:path");
const fs = require("node:fs");
const { promisify } = require("node:util");
const {
CLIEngine,
getCLIEngineInternalSlots,
} = require("../cli-engine/cli-engine");
const BuiltinRules = require("../rules");
const {
Legacy: {
ConfigOps: { getRuleSeverity },
},
} = require("@eslint/eslintrc");
const { version } = require("../../package.json");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */
/** @typedef {import("../types").ESLint.ConfigData} ConfigData */
/** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
/** @typedef {import("../types").ESLint.LintResult} LintResult */
/** @typedef {import("../types").ESLint.Plugin} Plugin */
/** @typedef {import("../types").ESLint.ResultsMeta} ResultsMeta */
/** @typedef {import("../types").Rule.RuleModule} Rule */
/** @typedef {import("../types").Linter.SuppressedLintMessage} SuppressedLintMessage */
/**
* The main formatter object.
* @typedef LoadedFormatter
* @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function.
*/
/**
* The options with which to configure the LegacyESLint instance.
* @typedef {Object} LegacyESLintOptions
* @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments.
* @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance
* @property {boolean} [cache] Enable result caching.
* @property {string} [cacheLocation] The cache file to use instead of .eslintcache.
* @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
* @property {string} [cwd] The value to use for the current working directory.
* @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`.
* @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 {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] False disables use of .eslintignore.
* @property {string} [ignorePath] The ignore file to use instead of .eslintignore.
* @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance
* @property {string} [overrideConfigFile] The configuration file to use.
* @property {Record<string,Plugin>|null} [plugins] Preloaded plugins. This is a map-like object, keys are plugin IDs and each value is implementation.
* @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives.
* @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD.
* @property {string[]} [rulePaths] An array of directories to load custom rules from.
* @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files.
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
*/
/**
* A rules metadata object.
* @typedef {Object} RulesMeta
* @property {string} id The plugin ID.
* @property {Object} definition The plugin definition.
*/
/**
* Private members for the `ESLint` instance.
* @typedef {Object} ESLintPrivateMembers
* @property {CLIEngine} cliEngine The wrapped CLIEngine instance.
* @property {LegacyESLintOptions} options The options used to instantiate the ESLint instance.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const writeFile = promisify(fs.writeFile);
/**
* The map with which to store private class members.
* @type {WeakMap<ESLint, ESLintPrivateMembers>}
*/
const privateMembersMap = new WeakMap();
/**
* Check if a given value is a non-empty string or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is a non-empty string.
*/
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}
/**
* Check if a given value is an array of non-empty strings or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of non-empty strings.
*/
function isArrayOfNonEmptyString(value) {
return (
Array.isArray(value) && value.length && value.every(isNonEmptyString)
);
}
/**
* Check if a given value is an empty array or an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
* strings.
*/
function isEmptyArrayOrArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.every(isNonEmptyString);
}
/**
* Check if a given value is a valid fix type or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is valid fix type.
*/
function isFixType(value) {
return (
value === "directive" ||
value === "problem" ||
value === "suggestion" ||
value === "layout"
);
}
/**
* Check if a given value is an array of fix types or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of fix types.
*/
function isFixTypeArray(value) {
return Array.isArray(value) && value.every(isFixType);
}
/**
* The error for invalid options.
*/
class ESLintInvalidOptionsError extends Error {
constructor(messages) {
super(`Invalid Options:\n- ${messages.join("\n- ")}`);
this.code = "ESLINT_INVALID_OPTIONS";
Error.captureStackTrace(this, ESLintInvalidOptionsError);
}
}
/**
* Validates and normalizes options for the wrapped CLIEngine instance.
* @param {LegacyESLintOptions} options The options to process.
* @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
* @returns {LegacyESLintOptions} The normalized options.
*/
function processOptions({
allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
baseConfig = null,
cache = false,
cacheLocation = ".eslintcache",
cacheStrategy = "metadata",
cwd = process.cwd(),
errorOnUnmatchedPattern = true,
extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature.
fix = false,
fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
flags /* eslint-disable-line no-unused-vars -- leaving for compatibility with ESLint#hasFlag */,
globInputPaths = true,
ignore = true,
ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT.
overrideConfig = null,
overrideConfigFile = null,
plugins = {},
reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that.
resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature.
rulePaths = [],
useEslintrc = true,
passOnNoPatterns = false,
...unknownOptions
}) {
const errors = [];
const unknownOptionKeys = Object.keys(unknownOptions);
if (unknownOptionKeys.length >= 1) {
errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
if (unknownOptionKeys.includes("cacheFile")) {
errors.push(
"'cacheFile' has been removed. Please use the 'cacheLocation' option instead.",
);
}
if (unknownOptionKeys.includes("configFile")) {
errors.push(
"'configFile' has been removed. Please use the 'overrideConfigFile' option instead.",
);
}
if (unknownOptionKeys.includes("envs")) {
errors.push(
"'envs' has been removed. Please use the 'overrideConfig.env' option instead.",
);
}
if (unknownOptionKeys.includes("globals")) {
errors.push(
"'globals' has been removed. Please use the 'overrideConfig.globals' option instead.",
);
}
if (unknownOptionKeys.includes("ignorePattern")) {
errors.push(
"'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.",
);
}
if (unknownOptionKeys.includes("parser")) {
errors.push(
"'parser' has been removed. Please use the 'overrideConfig.parser' option instead.",
);
}
if (unknownOptionKeys.includes("parserOptions")) {
errors.push(
"'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead.",
);
}
if (unknownOptionKeys.includes("rules")) {
errors.push(
"'rules' has been removed. Please use the 'overrideConfig.rules' option instead.",
);
}
}
if (typeof allowInlineConfig !== "boolean") {
errors.push("'allowInlineConfig' must be a boolean.");
}
if (typeof baseConfig !== "object") {
errors.push("'baseConfig' must be an object or null.");
}
if (typeof cache !== "boolean") {
errors.push("'cache' must be a boolean.");
}
if (!isNonEmptyString(cacheLocation)) {
errors.push("'cacheLocation' must be a non-empty string.");
}
if (cacheStrategy !== "metadata" && cacheStrategy !== "content") {
errors.push('\'cacheStrategy\' must be any of "metadata", "content".');
}
if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
errors.push("'cwd' must be an absolute path.");
}
if (typeof errorOnUnmatchedPattern !== "boolean") {
errors.push("'errorOnUnmatchedPattern' must be a boolean.");
}
if (
!isEmptyArrayOrArrayOfNonEmptyString(extensions) &&
extensions !== null
) {
errors.push(
"'extensions' must be an array of non-empty strings or null.",
);
}
if (typeof fix !== "boolean" && typeof fix !== "function") {
errors.push("'fix' must be a boolean or a function.");
}
if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
errors.push(
'\'fixTypes\' must be an array of any of "directive", "problem", "suggestion", and "layout".',
);
}
if (typeof globInputPaths !== "boolean") {
errors.push("'globInputPaths' must be a boolean.");
}
if (typeof ignore !== "boolean") {
errors.push("'ignore' must be a boolean.");
}
if (!isNonEmptyString(ignorePath) && ignorePath !== null) {
errors.push("'ignorePath' must be a non-empty string or null.");
}
if (typeof overrideConfig !== "object") {
errors.push("'overrideConfig' must be an object or null.");
}
if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) {
errors.push("'overrideConfigFile' must be a non-empty string or null.");
}
if (typeof plugins !== "object") {
errors.push("'plugins' must be an object or null.");
} else if (plugins !== null && Object.keys(plugins).includes("")) {
errors.push("'plugins' must not include an empty string.");
}
if (Array.isArray(plugins)) {
errors.push(
"'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.",
);
}
if (
reportUnusedDisableDirectives !== "error" &&
reportUnusedDisableDirectives !== "warn" &&
reportUnusedDisableDirectives !== "off" &&
reportUnusedDisableDirectives !== null
) {
errors.push(
'\'reportUnusedDisableDirectives\' must be any of "error", "warn", "off", and null.',
);
}
if (
!isNonEmptyString(resolvePluginsRelativeTo) &&
resolvePluginsRelativeTo !== null
) {
errors.push(
"'resolvePluginsRelativeTo' must be a non-empty string or null.",
);
}
if (!isEmptyArrayOrArrayOfNonEmptyString(rulePaths)) {
errors.push("'rulePaths' must be an array of non-empty strings.");
}
if (typeof useEslintrc !== "boolean") {
errors.push("'useEslintrc' must be a boolean.");
}
if (typeof passOnNoPatterns !== "boolean") {
errors.push("'passOnNoPatterns' must be a boolean.");
}
if (errors.length > 0) {
throw new ESLintInvalidOptionsError(errors);
}
return {
allowInlineConfig,
baseConfig,
cache,
cacheLocation,
cacheStrategy,
configFile: overrideConfigFile,
cwd: path.normalize(cwd),
errorOnUnmatchedPattern,
extensions,
fix,
fixTypes,
flags: [], // LegacyESLint does not support flags, so just ignore them.
globInputPaths,
ignore,
ignorePath,
reportUnusedDisableDirectives,
resolvePluginsRelativeTo,
rulePaths,
useEslintrc,
passOnNoPatterns,
};
}
/**
* Check if a value has one or more properties and that value is not undefined.
* @param {any} obj The value to check.
* @returns {boolean} `true` if `obj` has one or more properties that value is not undefined.
*/
function hasDefinedProperty(obj) {
if (typeof obj === "object" && obj !== null) {
for (const key in obj) {
if (typeof obj[key] !== "undefined") {
return true;
}
}
}
return false;
}
/**
* Create rulesMeta object.
* @param {Map<string,Rule>} rules a map of rules from which to generate the object.
* @returns {Object} metadata for all enabled rules.
*/
function createRulesMeta(rules) {
return Array.from(rules).reduce((retVal, [id, rule]) => {
retVal[id] = rule.meta;
return retVal;
}, {});
}
/** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
const usedDeprecatedRulesCache = new WeakMap();
/**
* Create used deprecated rule list.
* @param {CLIEngine} cliEngine The CLIEngine instance.
* @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`.
* @returns {DeprecatedRuleInfo[]} The used deprecated rule list.
*/
function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) {
const {
configArrayFactory,
options: { cwd },
} = getCLIEngineInternalSlots(cliEngine);
const filePath = path.isAbsolute(maybeFilePath)
? maybeFilePath
: path.join(cwd, "__placeholder__.js");
const configArray = configArrayFactory.getConfigArrayForFile(filePath);
const config = configArray.extractConfig(filePath);
// Most files use the same config, so cache it.
if (!usedDeprecatedRulesCache.has(config)) {
const pluginRules = configArray.pluginRules;
const retv = [];
for (const [ruleId, ruleConf] of Object.entries(config.rules)) {
if (getRuleSeverity(ruleConf) === 0) {
continue;
}
const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId);
const meta = rule && rule.meta;
if (meta && meta.deprecated) {
retv.push({ ruleId, replacedBy: meta.replacedBy || [] });
}
}
usedDeprecatedRulesCache.set(config, Object.freeze(retv));
}
return usedDeprecatedRulesCache.get(config);
}
/**
* Processes the linting results generated by a CLIEngine linting report to
* match the ESLint class's API.
* @param {CLIEngine} cliEngine The CLIEngine instance.
* @param {CLIEngineLintReport} report The CLIEngine linting report to process.
* @returns {LintResult[]} The processed linting results.
*/
function processCLIEngineLintReport(cliEngine, { results }) {
const descriptor = {
configurable: true,
enumerable: true,
get() {
return getOrFindUsedDeprecatedRules(cliEngine, this.filePath);
},
};
for (const result of results) {
Object.defineProperty(result, "usedDeprecatedRules", descriptor);
}
return results;
}
/**
* An Array.prototype.sort() compatible compare function to order results by their file path.
* @param {LintResult} a The first lint result.
* @param {LintResult} b The second lint result.
* @returns {number} An integer representing the order in which the two results should occur.
*/
function compareResultsByFilePath(a, b) {
if (a.filePath < b.filePath) {
return -1;
}
if (a.filePath > b.filePath) {
return 1;
}
return 0;
}
/**
* Main API.
*/
class LegacyESLint {
/**
* The type of configuration used by this class.
* @type {string}
*/
static configType = "eslintrc";
/**
* Creates a new instance of the main ESLint API.
* @param {LegacyESLintOptions} options The options for this instance.
*/
constructor(options = {}) {
const processedOptions = processOptions(options);
const cliEngine = new CLIEngine(processedOptions, {
preloadedPlugins: options.plugins,
});
const { configArrayFactory, lastConfigArrays } =
getCLIEngineInternalSlots(cliEngine);
let updated = false;
/*
* Address `overrideConfig` to set override config.
* Operate the `configArrayFactory` internal slot directly because this
* functionality doesn't exist as the public API of CLIEngine.
*/
if (hasDefinedProperty(options.overrideConfig)) {
configArrayFactory.setOverrideConfig(options.overrideConfig);
updated = true;
}
// Update caches.
if (updated) {
configArrayFactory.clearCache();
lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile();
}
// Initialize private properties.
privateMembersMap.set(this, {
cliEngine,
options: processedOptions,
});
}
/**
* The version text.
* @type {string}
*/
static get version() {
return version;
}
/**
* Outputs fixes from the given results to files.
* @param {LintResult[]} results The lint results.
* @returns {Promise<void>} Returns a promise that is used to track side effects.
*/
static async outputFixes(results) {
if (!Array.isArray(results)) {
throw new Error("'results' must be an array");
}
await Promise.all(
results
.filter(result => {
if (typeof result !== "object" || result === null) {
throw new Error("'results' must include only objects");
}
return (
typeof result.output === "string" &&
path.isAbsolute(result.filePath)
);
})
.map(r => writeFile(r.filePath, r.output)),
);
}
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
static getErrorResults(results) {
return CLIEngine.getErrorResults(results);
}
/**
* Returns meta objects for each rule represented in the lint results.
* @param {LintResult[]} results The results to fetch rules meta for.
* @returns {Object} A mapping of ruleIds to rule meta objects.
*/
getRulesMetaForResults(results) {
const resultRuleIds = new Set();
// first gather all ruleIds from all results
for (const result of results) {
for (const { ruleId } of result.messages) {
resultRuleIds.add(ruleId);
}
for (const { ruleId } of result.suppressedMessages) {
resultRuleIds.add(ruleId);
}
}
// create a map of all rules in the results
const { cliEngine } = privateMembersMap.get(this);
const rules = cliEngine.getRules();
const resultRules = new Map();
for (const [ruleId, rule] of rules) {
if (resultRuleIds.has(ruleId)) {
resultRules.set(ruleId, rule);
}
}
return createRulesMeta(resultRules);
}
/* eslint-disable no-unused-vars, class-methods-use-this -- leaving for compatibility with ESLint#hasFlag */
/**
* Indicates if the given feature flag is enabled for this instance. For this
* class, this always returns `false` because it does not support feature flags.
* @param {string} flag The feature flag to check.
* @returns {boolean} Always false.
*/
hasFlag(flag) {
return false;
}
/* eslint-enable no-unused-vars, class-methods-use-this -- reenable rules for the rest of the file */
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
* @returns {Promise<LintResult[]>} The results of linting the file patterns given.
*/
async lintFiles(patterns) {
const { cliEngine, options } = privateMembersMap.get(this);
if (
options.passOnNoPatterns &&
(patterns === "" ||
(Array.isArray(patterns) && patterns.length === 0))
) {
return [];
}
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error(
"'patterns' must be a non-empty string or an array of non-empty strings",
);
}
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnFiles(patterns),
);
}
/**
* Executes the current configuration on text.
* @param {string} code A string of JavaScript code to lint.
* @param {Object} [options] The options.
* @param {string} [options.filePath] The path to the file of the source code.
* @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path.
* @returns {Promise<LintResult[]>} The results of linting the string of code given.
*/
async lintText(code, options = {}) {
if (typeof code !== "string") {
throw new Error("'code' must be a string");
}
if (typeof options !== "object") {
throw new Error("'options' must be an object, null, or undefined");
}
const {
filePath,
warnIgnored = false,
...unknownOptions
} = options || {};
const unknownOptionKeys = Object.keys(unknownOptions);
if (unknownOptionKeys.length > 0) {
throw new Error(
`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`,
);
}
if (filePath !== void 0 && !isNonEmptyString(filePath)) {
throw new Error(
"'options.filePath' must be a non-empty string or undefined",
);
}
if (typeof warnIgnored !== "boolean") {
throw new Error(
"'options.warnIgnored' must be a boolean or undefined",
);
}
const { cliEngine } = privateMembersMap.get(this);
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnText(code, filePath, warnIgnored),
);
}
/**
* Returns the formatter representing the given formatter name.
* @param {string} [name] The name of the formatter to load.
* The following values are allowed:
* - `undefined` ... Load `stylish` builtin formatter.
* - A builtin formatter name ... Load the builtin formatter.
* - A third-party formatter name:
* - `foo` → `eslint-formatter-foo`
* - `@foo` → `@foo/eslint-formatter`
* - `@foo/bar` → `@foo/eslint-formatter-bar`
* - A file path ... Load the file.
* @returns {Promise<LoadedFormatter>} A promise resolving to the formatter object.
* This promise will be rejected if the given formatter was not found or not
* a function.
*/
async loadFormatter(name = "stylish") {
if (typeof name !== "string") {
throw new Error("'name' must be a string");
}
const { cliEngine, options } = privateMembersMap.get(this);
const formatter = cliEngine.getFormatter(name);
if (typeof formatter !== "function") {
throw new Error(
`Formatter must be a function, but got a ${typeof formatter}.`,
);
}
return {
/**
* The main formatter method.
* @param {LintResult[]} results The lint results to format.
* @param {ResultsMeta} resultsMeta Warning count and max threshold.
* @returns {string | Promise<string>} The formatted lint results.
*/
format(results, resultsMeta) {
let rulesMeta = null;
results.sort(compareResultsByFilePath);
return formatter(results, {
...resultsMeta,
get cwd() {
return options.cwd;
},
get rulesMeta() {
if (!rulesMeta) {
rulesMeta = createRulesMeta(cliEngine.getRules());
}
return rulesMeta;
},
});
},
};
}
/**
* 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 {Promise<ConfigData>} A configuration object for the file.
*/
async calculateConfigForFile(filePath) {
if (!isNonEmptyString(filePath)) {
throw new Error("'filePath' must be a non-empty string");
}
const { cliEngine } = privateMembersMap.get(this);
return cliEngine.getConfigForFile(filePath);
}
/**
* Checks if a given path is ignored by ESLint.
* @param {string} filePath The path of the file to check.
* @returns {Promise<boolean>} Whether or not the given path is ignored.
*/
async isPathIgnored(filePath) {
if (!isNonEmptyString(filePath)) {
throw new Error("'filePath' must be a non-empty string");
}
const { cliEngine } = privateMembersMap.get(this);
return cliEngine.isPathIgnored(filePath);
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
LegacyESLint,
/**
* Get the private class members of a given ESLint instance for tests.
* @param {ESLint} instance The ESLint instance to get.
* @returns {ESLintPrivateMembers} The instance's private class members.
*/
getESLintPrivateMembers(instance) {
return privateMembersMap.get(instance);
},
};
-173
View File
@@ -1,173 +0,0 @@
/**
* @fileoverview Worker thread for multithread linting.
* @author Francesco Trotta
*/
"use strict";
const hrtimeBigint = process.hrtime.bigint;
const startTime = hrtimeBigint();
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- enable V8's code cache if supported
require("node:module").enableCompileCache?.();
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { parentPort, threadId, workerData } = require("node:worker_threads");
const {
createConfigLoader,
createDebug,
createDefaultConfigs,
createLinter,
createLintResultCache,
getCacheFile,
lintFile,
loadOptionsFromModule,
processOptions,
} = require("./eslint-helpers");
const { WarningService } = require("../services/warning-service");
const timing = require("../linter/timing");
const depsLoadedTime = hrtimeBigint();
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../types").ESLint.LintResult} LintResult */
/** @typedef {import("../types").ESLint.Options} ESLintOptions */
/** @typedef {LintResult & { index?: number; }} IndexedLintResult */
/** @typedef {IndexedLintResult[] & { netLintingDuration: bigint; timings?: Record<string, number>; }} WorkerLintResults */
/**
* @typedef {Object} WorkerData - Data passed to the worker thread.
* @property {ESLintOptions | string} eslintOptionsOrURL - The unprocessed ESLint options or the URL of the options module.
* @property {Uint32Array<SharedArrayBuffer>} filePathIndexArray - Shared counter used to track the next file to lint.
* @property {string[]} filePaths - File paths to lint.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const debug = createDebug(`eslint:worker:thread-${threadId}`);
//------------------------------------------------------------------------------
// Main
//------------------------------------------------------------------------------
/*
* Prevent timing module from printing profiling output from worker threads.
* The main thread is responsible for displaying any aggregated timings.
*/
timing.disableDisplay();
debug("Dependencies loaded in %t", depsLoadedTime - startTime);
(async () => {
/** @type {WorkerData} */
const { eslintOptionsOrURL, filePathIndexArray, filePaths } = workerData;
const eslintOptions =
typeof eslintOptionsOrURL === "object"
? eslintOptionsOrURL
: await loadOptionsFromModule(eslintOptionsOrURL);
const processedESLintOptions = processOptions(eslintOptions);
const warningService = new WarningService();
// These warnings are always emitted by the controlling thread.
warningService.emitEmptyConfigWarning =
warningService.emitInactiveFlagWarning = () => {};
const linter = createLinter(processedESLintOptions, warningService);
const cacheFilePath = getCacheFile(
processedESLintOptions.cacheLocation,
processedESLintOptions.cwd,
);
const lintResultCache = createLintResultCache(
processedESLintOptions,
cacheFilePath,
);
const defaultConfigs = createDefaultConfigs(eslintOptions.plugins);
const configLoader = createConfigLoader(
processedESLintOptions,
defaultConfigs,
linter,
warningService,
);
/** @type {WorkerLintResults} */
const indexedResults = [];
let loadConfigTotalDuration = 0n;
const readFileCounter = { duration: 0n };
const lintingStartTime = hrtimeBigint();
debug(
"Linting started %t after dependencies loaded",
lintingStartTime - depsLoadedTime,
);
for (;;) {
const fileLintingStartTime = hrtimeBigint();
// It seems hard to produce an arithmetic overflow under realistic conditions here.
const index = Atomics.add(filePathIndexArray, 0, 1);
const filePath = filePaths[index];
if (!filePath) {
break;
}
const loadConfigEnterTime = hrtimeBigint();
const configs = await configLoader.loadConfigArrayForFile(filePath);
const loadConfigExitTime = hrtimeBigint();
const loadConfigDuration = loadConfigExitTime - loadConfigEnterTime;
debug(
'Config array for file "%s" loaded in %t',
filePath,
loadConfigDuration,
);
loadConfigTotalDuration += loadConfigDuration;
/** @type {IndexedLintResult} */
const result = await lintFile(
filePath,
configs,
processedESLintOptions,
linter,
lintResultCache,
readFileCounter,
);
if (result) {
result.index = index;
indexedResults.push(result);
}
const fileLintingEndTime = hrtimeBigint();
debug(
'File "%s" processed in %t',
filePath,
fileLintingEndTime - fileLintingStartTime,
);
}
const lintingDuration = hrtimeBigint() - lintingStartTime;
/*
* The net linting duration is the total linting time minus the time spent loading configs and reading files.
* It captures the processing time dedicated to computation-intensive tasks that are highly parallelizable and not repeated across threads.
*/
indexedResults.netLintingDuration =
lintingDuration - loadConfigTotalDuration - readFileCounter.duration;
if (timing.enabled) {
indexedResults.timings = timing.getData();
}
parentPort.postMessage(indexedResults);
})();
-336
View File
@@ -1,336 +0,0 @@
/**
* @fileoverview JavaScript Language Object
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const { SourceCode } = require("./source-code");
const createDebug = require("debug");
const astUtils = require("../../shared/ast-utils");
const espree = require("espree");
const eslintScope = require("eslint-scope");
const evk = require("eslint-visitor-keys");
const { validateLanguageOptions } = require("./validate-language-options");
const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version");
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/** @typedef {import("@eslint/core").File} File */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").OkParseResult} OkParseResult */
/** @typedef {import("../../types").Linter.LanguageOptions} JSLanguageOptions */
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const debug = createDebug("eslint:languages:js");
const DEFAULT_ECMA_VERSION = 5;
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
/**
* Analyze scope of the given AST.
* @param {ASTNode} ast The `Program` node to analyze.
* @param {JSLanguageOptions} languageOptions The parser options.
* @param {Record<string, string[]>} visitorKeys The visitor keys.
* @returns {ScopeManager} The analysis result.
*/
function analyzeScope(ast, languageOptions, visitorKeys) {
const parserOptions = languageOptions.parserOptions;
const ecmaFeatures = parserOptions.ecmaFeatures || {};
const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
return eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
sourceType: languageOptions.sourceType || "script",
childVisitorKeys: visitorKeys || evk.KEYS,
fallback: evk.getKeys,
});
}
/**
* Determines if a given object is Espree.
* @param {Object} parser The parser to check.
* @returns {boolean} True if the parser is Espree or false if not.
*/
function isEspree(parser) {
return !!(parser === espree || parser[parserSymbol] === espree);
}
/**
* Normalize ECMAScript version from the initial config into languageOptions (year)
* format.
* @param {any} [ecmaVersion] ECMAScript version from the initial config
* @returns {number} normalized ECMAScript version
*/
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) {
switch (ecmaVersion) {
case 3:
return 3;
// void 0 = no ecmaVersion specified so use the default
case 5:
case void 0:
return 5;
default:
if (typeof ecmaVersion === "number") {
return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009;
}
}
/*
* We default to the latest supported ecmaVersion for everything else.
* Remember, this is for languageOptions.ecmaVersion, which sets the version
* that is used for a number of processes inside of ESLint. It's normally
* safe to assume people want the latest unless otherwise specified.
*/
return LATEST_ECMA_VERSION;
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* @type {Language}
*/
module.exports = {
fileType: "text",
lineStart: 1,
columnStart: 0,
nodeTypeKey: "type",
visitorKeys: evk.KEYS,
defaultLanguageOptions: {
sourceType: "module",
ecmaVersion: "latest",
parser: espree,
parserOptions: {},
},
validateLanguageOptions,
/**
* Normalizes the language options.
* @param {Object} languageOptions The language options to normalize.
* @returns {Object} The normalized language options.
*/
normalizeLanguageOptions(languageOptions) {
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
languageOptions.ecmaVersion,
);
// Espree expects this information to be passed in
if (isEspree(languageOptions.parser)) {
const parserOptions = languageOptions.parserOptions;
if (languageOptions.sourceType) {
parserOptions.sourceType = languageOptions.sourceType;
if (
parserOptions.sourceType === "module" &&
parserOptions.ecmaFeatures &&
parserOptions.ecmaFeatures.globalReturn
) {
parserOptions.ecmaFeatures.globalReturn = false;
}
}
}
return languageOptions;
},
/**
* Determines if a given node matches a given selector class.
* @param {string} className The class name to check.
* @param {ASTNode} node The node to check.
* @param {Array<ASTNode>} ancestry The ancestry of the node.
* @returns {boolean} True if there's a match, false if not.
* @throws {Error} When an unknown class name is passed.
*/
matchesSelectorClass(className, node, ancestry) {
/*
* Copyright (c) 2013, Joel Feenstra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the ESQuery nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL JOEL FEENSTRA BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
switch (className.toLowerCase()) {
case "statement":
if (node.type.slice(-9) === "Statement") {
return true;
}
// fallthrough: interface Declaration <: Statement { }
case "declaration":
return node.type.slice(-11) === "Declaration";
case "pattern":
if (node.type.slice(-7) === "Pattern") {
return true;
}
// fallthrough: interface Expression <: Node, Pattern { }
case "expression":
return (
node.type.slice(-10) === "Expression" ||
node.type.slice(-7) === "Literal" ||
(node.type === "Identifier" &&
(ancestry.length === 0 ||
ancestry[0].type !== "MetaProperty")) ||
node.type === "MetaProperty"
);
case "function":
return (
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression" ||
node.type === "ArrowFunctionExpression"
);
default:
throw new Error(`Unknown class name: ${className}`);
}
},
/**
* Parses the given file into an AST.
* @param {File} file The virtual file to parse.
* @param {Object} options Additional options passed from ESLint.
* @param {JSLanguageOptions} options.languageOptions The language options.
* @returns {Object} The result of parsing.
*/
parse(file, { languageOptions }) {
// Note: BOM already removed
const { body: text, path: filePath } = file;
const textToParse = text.replace(
astUtils.shebangPattern,
(match, captured) => `//${captured}`,
);
const { ecmaVersion, sourceType, parser } = languageOptions;
const parserOptions = Object.assign(
{ ecmaVersion, sourceType },
languageOptions.parserOptions,
{
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
eslintVisitorKeys: true,
eslintScopeManager: true,
filePath,
},
);
/*
* Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error
* from this method - it's just considered a fatal error message, a
* problem that ESLint identified just like any other.
*/
try {
debug("Parsing:", filePath);
const parseResult =
typeof parser.parseForESLint === "function"
? parser.parseForESLint(textToParse, parserOptions)
: { ast: parser.parse(textToParse, parserOptions) };
debug("Parsing successful:", filePath);
const {
ast,
services: parserServices = {},
visitorKeys = evk.KEYS,
scopeManager,
} = parseResult;
return {
ok: true,
ast,
parserServices,
visitorKeys,
scopeManager,
};
} catch (ex) {
// If the message includes a leading line number, strip it:
const message = ex.message.replace(/^line \d+:/iu, "").trim();
debug("%s\n%s", message, ex.stack);
return {
ok: false,
errors: [
{
message,
line: ex.lineNumber,
column: ex.column,
},
],
};
}
},
/**
* Creates a new `SourceCode` object from the given information.
* @param {File} file The virtual file to create a `SourceCode` object from.
* @param {OkParseResult} parseResult The result returned from `parse()`.
* @param {Object} options Additional options passed from ESLint.
* @param {JSLanguageOptions} options.languageOptions The language options.
* @returns {SourceCode} The new `SourceCode` object.
*/
createSourceCode(file, parseResult, { languageOptions }) {
const { body: text, path: filePath, bom: hasBOM } = file;
const { ast, parserServices, visitorKeys } = parseResult;
debug("Scope analysis:", filePath);
const scopeManager =
parseResult.scopeManager ||
analyzeScope(ast, languageOptions, visitorKeys);
debug("Scope analysis successful:", filePath);
return new SourceCode({
text,
ast,
hasBOM,
parserServices,
scopeManager,
visitorKeys,
});
},
};
-7
View File
@@ -1,7 +0,0 @@
"use strict";
const SourceCode = require("./source-code");
module.exports = {
SourceCode,
};
File diff suppressed because it is too large. Load diff
@@ -1,61 +0,0 @@
/**
* @fileoverview Define the cursor which iterates tokens and comments in reverse.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Cursor = require("./cursor");
const utils = require("./utils");
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The cursor which iterates tokens and comments in reverse.
*/
module.exports = class BackwardTokenCommentCursor extends Cursor {
/**
* Initializes this cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
*/
constructor(tokens, comments, indexMap, startLoc, endLoc) {
super();
this.tokens = tokens;
this.comments = comments;
this.tokenIndex = utils.getLastIndex(tokens, indexMap, endLoc);
this.commentIndex = utils.search(comments, endLoc) - 1;
this.border = startLoc;
}
/** @inheritdoc */
moveNext() {
const token =
this.tokenIndex >= 0 ? this.tokens[this.tokenIndex] : null;
const comment =
this.commentIndex >= 0 ? this.comments[this.commentIndex] : null;
if (token && (!comment || token.range[1] > comment.range[1])) {
this.current = token;
this.tokenIndex -= 1;
} else if (comment) {
this.current = comment;
this.commentIndex -= 1;
} else {
this.current = null;
}
return (
Boolean(this.current) &&
(this.border === -1 || this.current.range[0] >= this.border)
);
}
};
@@ -1,57 +0,0 @@
/**
* @fileoverview Define the cursor which iterates tokens only in reverse.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Cursor = require("./cursor");
const { getLastIndex, getFirstIndex } = require("./utils");
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The cursor which iterates tokens only in reverse.
*/
module.exports = class BackwardTokenCursor extends Cursor {
/**
* Initializes this cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
*/
constructor(tokens, comments, indexMap, startLoc, endLoc) {
super();
this.tokens = tokens;
this.index = getLastIndex(tokens, indexMap, endLoc);
this.indexEnd = getFirstIndex(tokens, indexMap, startLoc);
}
/** @inheritdoc */
moveNext() {
if (this.index >= this.indexEnd) {
this.current = this.tokens[this.index];
this.index -= 1;
return true;
}
return false;
}
/*
*
* Shorthand for performance.
*
*/
/** @inheritdoc */
getOneToken() {
return this.index >= this.indexEnd ? this.tokens[this.index] : null;
}
};
@@ -1,120 +0,0 @@
/**
* @fileoverview Define 2 token factories; forward and backward.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const BackwardTokenCommentCursor = require("./backward-token-comment-cursor");
const BackwardTokenCursor = require("./backward-token-cursor");
const FilterCursor = require("./filter-cursor");
const ForwardTokenCommentCursor = require("./forward-token-comment-cursor");
const ForwardTokenCursor = require("./forward-token-cursor");
const LimitCursor = require("./limit-cursor");
const SkipCursor = require("./skip-cursor");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* The cursor factory.
* @private
*/
class CursorFactory {
/**
* Initializes this cursor.
* @param {Function} TokenCursor The class of the cursor which iterates tokens only.
* @param {Function} TokenCommentCursor The class of the cursor which iterates the mix of tokens and comments.
*/
constructor(TokenCursor, TokenCommentCursor) {
this.TokenCursor = TokenCursor;
this.TokenCommentCursor = TokenCommentCursor;
}
/**
* Creates a base cursor instance that can be decorated by createCursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {boolean} includeComments The flag to iterate comments as well.
* @returns {Cursor} The created base cursor.
*/
createBaseCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
includeComments,
) {
const Cursor = includeComments
? this.TokenCommentCursor
: this.TokenCursor;
return new Cursor(tokens, comments, indexMap, startLoc, endLoc);
}
/**
* Creates a cursor that iterates tokens with normalized options.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {boolean} includeComments The flag to iterate comments as well.
* @param {Function|null} filter The predicate function to choose tokens.
* @param {number} skip The count of tokens the cursor skips.
* @param {number} count The maximum count of tokens the cursor iterates. Zero is no iteration for backward compatibility.
* @returns {Cursor} The created cursor.
*/
createCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
includeComments,
filter,
skip,
count,
) {
let cursor = this.createBaseCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
includeComments,
);
if (filter) {
cursor = new FilterCursor(cursor, filter);
}
if (skip >= 1) {
cursor = new SkipCursor(cursor, skip);
}
if (count >= 0) {
cursor = new LimitCursor(cursor, count);
}
return cursor;
}
}
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
module.exports = {
forward: new CursorFactory(ForwardTokenCursor, ForwardTokenCommentCursor),
backward: new CursorFactory(
BackwardTokenCursor,
BackwardTokenCommentCursor,
),
};
@@ -1,65 +0,0 @@
/**
* @fileoverview Define the cursor which iterates tokens and comments.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Cursor = require("./cursor");
const { getFirstIndex, search } = require("./utils");
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The cursor which iterates tokens and comments.
*/
module.exports = class ForwardTokenCommentCursor extends Cursor {
/**
* Initializes this cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
*/
constructor(tokens, comments, indexMap, startLoc, endLoc) {
super();
this.tokens = tokens;
this.comments = comments;
this.tokenIndex = getFirstIndex(tokens, indexMap, startLoc);
this.commentIndex = search(comments, startLoc);
this.border = endLoc;
}
/** @inheritdoc */
moveNext() {
const token =
this.tokenIndex < this.tokens.length
? this.tokens[this.tokenIndex]
: null;
const comment =
this.commentIndex < this.comments.length
? this.comments[this.commentIndex]
: null;
if (token && (!comment || token.range[0] < comment.range[0])) {
this.current = token;
this.tokenIndex += 1;
} else if (comment) {
this.current = comment;
this.commentIndex += 1;
} else {
this.current = null;
}
return (
Boolean(this.current) &&
(this.border === -1 || this.current.range[1] <= this.border)
);
}
};
@@ -1,62 +0,0 @@
/**
* @fileoverview Define the cursor which iterates tokens only.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Cursor = require("./cursor");
const { getFirstIndex, getLastIndex } = require("./utils");
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The cursor which iterates tokens only.
*/
module.exports = class ForwardTokenCursor extends Cursor {
/**
* Initializes this cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
*/
constructor(tokens, comments, indexMap, startLoc, endLoc) {
super();
this.tokens = tokens;
this.index = getFirstIndex(tokens, indexMap, startLoc);
this.indexEnd = getLastIndex(tokens, indexMap, endLoc);
}
/** @inheritdoc */
moveNext() {
if (this.index <= this.indexEnd) {
this.current = this.tokens[this.index];
this.index += 1;
return true;
}
return false;
}
/*
*
* Shorthand for performance.
*
*/
/** @inheritdoc */
getOneToken() {
return this.index <= this.indexEnd ? this.tokens[this.index] : null;
}
/** @inheritdoc */
getAllTokens() {
return this.tokens.slice(this.index, this.indexEnd + 1);
}
};
@@ -1,721 +0,0 @@
/**
* @fileoverview Object to handle access and retrieval of tokens.
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { isCommentToken } = require("@eslint-community/eslint-utils");
const assert = require("../../../../shared/assert");
const cursors = require("./cursors");
const ForwardTokenCursor = require("./forward-token-cursor");
const PaddedTokenCursor = require("./padded-token-cursor");
const utils = require("./utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const TOKENS = Symbol("tokens");
const COMMENTS = Symbol("comments");
const INDEX_MAP = Symbol("indexMap");
/**
* Creates the map from locations to indices in `tokens`.
*
* The first/last location of tokens is mapped to the index of the token.
* The first/last location of comments is mapped to the index of the next token of each comment.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @returns {Object} The map from locations to indices in `tokens`.
* @private
*/
function createIndexMap(tokens, comments) {
const map = Object.create(null);
let tokenIndex = 0;
let commentIndex = 0;
let nextStart;
let range;
while (tokenIndex < tokens.length || commentIndex < comments.length) {
nextStart =
commentIndex < comments.length
? comments[commentIndex].range[0]
: Number.MAX_SAFE_INTEGER;
while (
tokenIndex < tokens.length &&
(range = tokens[tokenIndex].range)[0] < nextStart
) {
map[range[0]] = tokenIndex;
map[range[1] - 1] = tokenIndex;
tokenIndex += 1;
}
nextStart =
tokenIndex < tokens.length
? tokens[tokenIndex].range[0]
: Number.MAX_SAFE_INTEGER;
while (
commentIndex < comments.length &&
(range = comments[commentIndex].range)[0] < nextStart
) {
map[range[0]] = tokenIndex;
map[range[1] - 1] = tokenIndex;
commentIndex += 1;
}
}
return map;
}
/**
* Creates the cursor iterates tokens with options.
* @param {CursorFactory} factory The cursor factory to initialize cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {number|Function|Object} [opts=0] The option object. If this is a number then it's `opts.skip`. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments=false] The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] The predicate function to choose tokens.
* @param {number} [opts.skip=0] The count of tokens the cursor skips.
* @returns {Cursor} The created cursor.
* @private
*/
function createCursorWithSkip(
factory,
tokens,
comments,
indexMap,
startLoc,
endLoc,
opts,
) {
let includeComments = false;
let skip = 0;
let filter = null;
if (typeof opts === "number") {
skip = opts | 0;
} else if (typeof opts === "function") {
filter = opts;
} else if (opts) {
includeComments = !!opts.includeComments;
skip = opts.skip | 0;
filter = opts.filter || null;
}
assert(skip >= 0, "options.skip should be zero or a positive integer.");
assert(
!filter || typeof filter === "function",
"options.filter should be a function.",
);
return factory.createCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
includeComments,
filter,
skip,
-1,
);
}
/**
* Creates the cursor iterates tokens with options.
* @param {CursorFactory} factory The cursor factory to initialize cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {number|Function|Object} [opts=0] The option object. If this is a number then it's `opts.count`. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments] The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] The predicate function to choose tokens.
* @param {number} [opts.count=0] The maximum count of tokens the cursor iterates. Zero is no iteration for backward compatibility.
* @returns {Cursor} The created cursor.
* @private
*/
function createCursorWithCount(
factory,
tokens,
comments,
indexMap,
startLoc,
endLoc,
opts,
) {
let includeComments = false;
let count = 0;
let countExists = false;
let filter = null;
if (typeof opts === "number") {
count = opts | 0;
countExists = true;
} else if (typeof opts === "function") {
filter = opts;
} else if (opts) {
includeComments = !!opts.includeComments;
count = opts.count | 0;
countExists = typeof opts.count === "number";
filter = opts.filter || null;
}
assert(count >= 0, "options.count should be zero or a positive integer.");
assert(
!filter || typeof filter === "function",
"options.filter should be a function.",
);
return factory.createCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
includeComments,
filter,
0,
countExists ? count : -1,
);
}
/**
* Creates the cursor iterates tokens with options.
* This is overload function of the below.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {Function|Object} opts The option object. If this is a function then it's `opts.filter`.
* @param {boolean} [opts.includeComments] The flag to iterate comments as well.
* @param {Function|null} [opts.filter=null] The predicate function to choose tokens.
* @param {number} [opts.count=0] The maximum count of tokens the cursor iterates. Zero is no iteration for backward compatibility.
* @returns {Cursor} The created cursor.
* @private
*/
/**
* Creates the cursor iterates tokens with options.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {number} [beforeCount=0] The number of tokens before the node to retrieve.
* @param {boolean} [afterCount=0] The number of tokens after the node to retrieve.
* @returns {Cursor} The created cursor.
* @private
*/
function createCursorWithPadding(
tokens,
comments,
indexMap,
startLoc,
endLoc,
beforeCount,
afterCount,
) {
if (
typeof beforeCount === "undefined" &&
typeof afterCount === "undefined"
) {
return new ForwardTokenCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
);
}
if (typeof beforeCount === "number" || typeof beforeCount === "undefined") {
return new PaddedTokenCursor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
beforeCount | 0,
afterCount | 0,
);
}
return createCursorWithCount(
cursors.forward,
tokens,
comments,
indexMap,
startLoc,
endLoc,
beforeCount,
);
}
/**
* Gets comment tokens that are adjacent to the current cursor position.
* @param {Cursor} cursor A cursor instance.
* @returns {Array} An array of comment tokens adjacent to the current cursor position.
* @private
*/
function getAdjacentCommentTokensFromCursor(cursor) {
const tokens = [];
let currentToken = cursor.getOneToken();
while (currentToken && isCommentToken(currentToken)) {
tokens.push(currentToken);
currentToken = cursor.getOneToken();
}
return tokens;
}
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The token store.
*
* This class provides methods to get tokens by locations as fast as possible.
* The methods are a part of public API, so we should be careful if it changes this class.
*
* People can get tokens in O(1) by the hash map which is mapping from the location of tokens/comments to tokens.
* Also people can get a mix of tokens and comments in O(log k), the k is the number of comments.
* Assuming that comments to be much fewer than tokens, this does not make hash map from token's locations to comments to reduce memory cost.
* This uses binary-searching instead for comments.
*/
module.exports = class TokenStore {
/**
* Initializes this token store.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
*/
constructor(tokens, comments) {
this[TOKENS] = tokens;
this[COMMENTS] = comments;
this[INDEX_MAP] = createIndexMap(tokens, comments);
}
//--------------------------------------------------------------------------
// Gets single token.
//--------------------------------------------------------------------------
/**
* Gets the token starting at the specified index.
* @param {number} offset Index of the start of the token's range.
* @param {Object} [options=0] The option object.
* @param {boolean} [options.includeComments=false] The flag to iterate comments as well.
* @returns {Token|null} The token starting at index, or null if no such token.
*/
getTokenByRangeStart(offset, options) {
const includeComments = options && options.includeComments;
const token = cursors.forward
.createBaseCursor(
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
offset,
-1,
includeComments,
)
.getOneToken();
if (token && token.range[0] === offset) {
return token;
}
return null;
}
/**
* Gets the first token of the given node.
* @param {ASTNode} node The AST node.
* @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] The predicate function to choose tokens.
* @param {number} [options.skip=0] The count of tokens the cursor skips.
* @returns {Token|null} An object representing the token.
*/
getFirstToken(node, options) {
return createCursorWithSkip(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[0],
node.range[1],
options,
).getOneToken();
}
/**
* Gets the last token of the given node.
* @param {ASTNode} node The AST node.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken()
* @returns {Token|null} An object representing the token.
*/
getLastToken(node, options) {
return createCursorWithSkip(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[0],
node.range[1],
options,
).getOneToken();
}
/**
* Gets the token that precedes a given node or token.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken()
* @returns {Token|null} An object representing the token.
*/
getTokenBefore(node, options) {
return createCursorWithSkip(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
-1,
node.range[0],
options,
).getOneToken();
}
/**
* Gets the token that follows a given node or token.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken()
* @returns {Token|null} An object representing the token.
*/
getTokenAfter(node, options) {
return createCursorWithSkip(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[1],
-1,
options,
).getOneToken();
}
/**
* Gets the first token between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken()
* @returns {Token|null} An object representing the token.
*/
getFirstTokenBetween(left, right, options) {
return createCursorWithSkip(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
left.range[1],
right.range[0],
options,
).getOneToken();
}
/**
* Gets the last token between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstToken()
* @returns {Token|null} An object representing the token.
*/
getLastTokenBetween(left, right, options) {
return createCursorWithSkip(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
left.range[1],
right.range[0],
options,
).getOneToken();
}
/**
* Gets the token that precedes a given node or token in the token stream.
* This is defined for backward compatibility. Use `includeComments` option instead.
* TODO: We have a plan to remove this in a future major version.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number} [skip=0] A number of tokens to skip.
* @returns {Token|null} An object representing the token.
* @deprecated
*/
getTokenOrCommentBefore(node, skip) {
return this.getTokenBefore(node, { includeComments: true, skip });
}
/**
* Gets the token that follows a given node or token in the token stream.
* This is defined for backward compatibility. Use `includeComments` option instead.
* TODO: We have a plan to remove this in a future major version.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number} [skip=0] A number of tokens to skip.
* @returns {Token|null} An object representing the token.
* @deprecated
*/
getTokenOrCommentAfter(node, skip) {
return this.getTokenAfter(node, { includeComments: true, skip });
}
//--------------------------------------------------------------------------
// Gets multiple tokens.
//--------------------------------------------------------------------------
/**
* Gets the first `count` tokens of the given node.
* @param {ASTNode} node The AST node.
* @param {number|Function|Object} [options=0] The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] The predicate function to choose tokens.
* @param {number} [options.count=0] The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens.
*/
getFirstTokens(node, options) {
return createCursorWithCount(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[0],
node.range[1],
options,
).getAllTokens();
}
/**
* Gets the last `count` tokens of the given node.
* @param {ASTNode} node The AST node.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens()
* @returns {Token[]} Tokens.
*/
getLastTokens(node, options) {
return createCursorWithCount(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[0],
node.range[1],
options,
)
.getAllTokens()
.reverse();
}
/**
* Gets the `count` tokens that precedes a given node or token.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens()
* @returns {Token[]} Tokens.
*/
getTokensBefore(node, options) {
return createCursorWithCount(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
-1,
node.range[0],
options,
)
.getAllTokens()
.reverse();
}
/**
* Gets the `count` tokens that follows a given node or token.
* @param {ASTNode|Token|Comment} node The AST node or token.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens()
* @returns {Token[]} Tokens.
*/
getTokensAfter(node, options) {
return createCursorWithCount(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[1],
-1,
options,
).getAllTokens();
}
/**
* Gets the first `count` tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens()
* @returns {Token[]} Tokens between left and right.
*/
getFirstTokensBetween(left, right, options) {
return createCursorWithCount(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
left.range[1],
right.range[0],
options,
).getAllTokens();
}
/**
* Gets the last `count` tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number|Function|Object} [options=0] The option object. Same options as getFirstTokens()
* @returns {Token[]} Tokens between left and right.
*/
getLastTokensBetween(left, right, options) {
return createCursorWithCount(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
left.range[1],
right.range[0],
options,
)
.getAllTokens()
.reverse();
}
/**
* Gets all tokens that are related to the given node.
* @param {ASTNode} node The AST node.
* @param {Function|Object} options The option object. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] The predicate function to choose tokens.
* @param {number} [options.count=0] The maximum count of tokens the cursor iterates.
* @returns {Token[]} Array of objects representing tokens.
*/
/**
* Gets all tokens that are related to the given node.
* @param {ASTNode} node The AST node.
* @param {number} [beforeCount=0] The number of tokens before the node to retrieve.
* @param {number} [afterCount=0] The number of tokens after the node to retrieve.
* @returns {Token[]} Array of objects representing tokens.
*/
getTokens(node, beforeCount, afterCount) {
return createCursorWithPadding(
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
node.range[0],
node.range[1],
beforeCount,
afterCount,
).getAllTokens();
}
/**
* Gets all of the tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {Function|Object} options The option object. If this is a function then it's `options.filter`.
* @param {boolean} [options.includeComments=false] The flag to iterate comments as well.
* @param {Function|null} [options.filter=null] The predicate function to choose tokens.
* @param {number} [options.count=0] The maximum count of tokens the cursor iterates.
* @returns {Token[]} Tokens between left and right.
*/
/**
* Gets all of the tokens between two non-overlapping nodes.
* @param {ASTNode|Token|Comment} left Node before the desired token range.
* @param {ASTNode|Token|Comment} right Node after the desired token range.
* @param {number} [padding=0] Number of extra tokens on either side of center.
* @returns {Token[]} Tokens between left and right.
*/
getTokensBetween(left, right, padding) {
return createCursorWithPadding(
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
left.range[1],
right.range[0],
padding,
padding,
).getAllTokens();
}
//--------------------------------------------------------------------------
// Others.
//--------------------------------------------------------------------------
/**
* Checks whether any comments exist or not between the given 2 nodes.
* @param {ASTNode} left The node to check.
* @param {ASTNode} right The node to check.
* @returns {boolean} `true` if one or more comments exist.
*/
commentsExistBetween(left, right) {
const index = utils.search(this[COMMENTS], left.range[1]);
return (
index < this[COMMENTS].length &&
this[COMMENTS][index].range[1] <= right.range[0]
);
}
/**
* Gets all comment tokens directly before the given node or token.
* @param {ASTNode|token} nodeOrToken The AST node or token to check for adjacent comment tokens.
* @returns {Array} An array of comments in occurrence order.
*/
getCommentsBefore(nodeOrToken) {
const cursor = createCursorWithCount(
cursors.backward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
-1,
nodeOrToken.range[0],
{ includeComments: true },
);
return getAdjacentCommentTokensFromCursor(cursor).reverse();
}
/**
* Gets all comment tokens directly after the given node or token.
* @param {ASTNode|token} nodeOrToken The AST node or token to check for adjacent comment tokens.
* @returns {Array} An array of comments in occurrence order.
*/
getCommentsAfter(nodeOrToken) {
const cursor = createCursorWithCount(
cursors.forward,
this[TOKENS],
this[COMMENTS],
this[INDEX_MAP],
nodeOrToken.range[1],
-1,
{ includeComments: true },
);
return getAdjacentCommentTokensFromCursor(cursor);
}
/**
* Gets all comment tokens inside the given node.
* @param {ASTNode} node The AST node to get the comments for.
* @returns {Array} An array of comments in occurrence order.
*/
getCommentsInside(node) {
return this.getTokens(node, {
includeComments: true,
filter: isCommentToken,
});
}
};
@@ -1,45 +0,0 @@
/**
* @fileoverview Define the cursor which iterates tokens only, with inflated range.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const ForwardTokenCursor = require("./forward-token-cursor");
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* The cursor which iterates tokens only, with inflated range.
* This is for the backward compatibility of padding options.
*/
module.exports = class PaddedTokenCursor extends ForwardTokenCursor {
/**
* Initializes this cursor.
* @param {Token[]} tokens The array of tokens.
* @param {Comment[]} comments The array of comments.
* @param {Object} indexMap The map from locations to indices in `tokens`.
* @param {number} startLoc The start location of the iteration range.
* @param {number} endLoc The end location of the iteration range.
* @param {number} beforeCount The number of tokens this cursor iterates before start.
* @param {number} afterCount The number of tokens this cursor iterates after end.
*/
constructor(
tokens,
comments,
indexMap,
startLoc,
endLoc,
beforeCount,
afterCount,
) {
super(tokens, comments, indexMap, startLoc, endLoc);
this.index = Math.max(0, this.index - beforeCount);
this.indexEnd = Math.min(tokens.length - 1, this.indexEnd + afterCount);
}
};
@@ -1,110 +0,0 @@
/**
* @fileoverview Define utility functions for token store.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* Finds the index of the first token which is after the given location.
* If it was not found, this returns `tokens.length`.
* @param {(Token|Comment)[]} tokens It searches the token in this list.
* @param {number} location The location to search.
* @returns {number} The found index or `tokens.length`.
*/
exports.search = function search(tokens, location) {
for (
let minIndex = 0, maxIndex = tokens.length - 1;
minIndex <= maxIndex;
) {
/*
* Calculate the index in the middle between minIndex and maxIndex.
* `| 0` is used to round a fractional value down to the nearest integer: this is similar to
* using `Math.trunc()` or `Math.floor()`, but performance tests have shown this method to
* be faster.
*/
const index = ((minIndex + maxIndex) / 2) | 0;
const token = tokens[index];
const tokenStartLocation = token.range[0];
if (location <= tokenStartLocation) {
if (index === minIndex) {
return index;
}
maxIndex = index;
} else {
minIndex = index + 1;
}
}
return tokens.length;
};
/**
* Gets the index of the `startLoc` in `tokens`.
* `startLoc` can be the value of `node.range[1]`, so this checks about `startLoc - 1` as well.
* @param {(Token|Comment)[]} tokens The tokens to find an index.
* @param {Object} indexMap The map from locations to indices.
* @param {number} startLoc The location to get an index.
* @returns {number} The index.
*/
exports.getFirstIndex = function getFirstIndex(tokens, indexMap, startLoc) {
if (startLoc in indexMap) {
return indexMap[startLoc];
}
if (startLoc - 1 in indexMap) {
const index = indexMap[startLoc - 1];
const token = tokens[index];
// If the mapped index is out of bounds, the returned cursor index will point after the end of the tokens array.
if (!token) {
return tokens.length;
}
/*
* For the map of "comment's location -> token's index", it points the next token of a comment.
* In that case, +1 is unnecessary.
*/
if (token.range[0] >= startLoc) {
return index;
}
return index + 1;
}
return 0;
};
/**
* Gets the index of the `endLoc` in `tokens`.
* The information of end locations are recorded at `endLoc - 1` in `indexMap`, so this checks about `endLoc - 1` as well.
* @param {(Token|Comment)[]} tokens The tokens to find an index.
* @param {Object} indexMap The map from locations to indices.
* @param {number} endLoc The location to get an index.
* @returns {number} The index.
*/
exports.getLastIndex = function getLastIndex(tokens, indexMap, endLoc) {
if (endLoc in indexMap) {
return indexMap[endLoc] - 1;
}
if (endLoc - 1 in indexMap) {
const index = indexMap[endLoc - 1];
const token = tokens[index];
// If the mapped index is out of bounds, the returned cursor index will point before the end of the tokens array.
if (!token) {
return tokens.length - 1;
}
/*
* For the map of "comment's location -> token's index", it points the next token of a comment.
* In that case, -1 is necessary.
*/
if (token.range[1] > endLoc) {
return index - 1;
}
return index;
}
return tokens.length - 1;
};
@@ -1,196 +0,0 @@
/**
* @fileoverview The schema to validate language options
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Data
//-----------------------------------------------------------------------------
const globalVariablesValues = new Set([
true,
"true",
"writable",
"writeable",
false,
"false",
"readonly",
"readable",
null,
"off",
]);
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Check if a value is a non-null object.
* @param {any} value The value to check.
* @returns {boolean} `true` if the value is a non-null object.
*/
function isNonNullObject(value) {
return typeof value === "object" && value !== null;
}
/**
* Check if a value is a non-null non-array object.
* @param {any} value The value to check.
* @returns {boolean} `true` if the value is a non-null non-array object.
*/
function isNonArrayObject(value) {
return isNonNullObject(value) && !Array.isArray(value);
}
/**
* Check if a value is undefined.
* @param {any} value The value to check.
* @returns {boolean} `true` if the value is undefined.
*/
function isUndefined(value) {
return typeof value === "undefined";
}
//-----------------------------------------------------------------------------
// Schemas
//-----------------------------------------------------------------------------
/**
* Validates the ecmaVersion property.
* @param {string|number} ecmaVersion The value to check.
* @returns {void}
* @throws {TypeError} If the value is invalid.
*/
function validateEcmaVersion(ecmaVersion) {
if (isUndefined(ecmaVersion)) {
throw new TypeError(
'Key "ecmaVersion": Expected an "ecmaVersion" property.',
);
}
if (typeof ecmaVersion !== "number" && ecmaVersion !== "latest") {
throw new TypeError(
'Key "ecmaVersion": Expected a number or "latest".',
);
}
}
/**
* Validates the sourceType property.
* @param {string} sourceType The value to check.
* @returns {void}
* @throws {TypeError} If the value is invalid.
*/
function validateSourceType(sourceType) {
if (
typeof sourceType !== "string" ||
!/^(?:script|module|commonjs)$/u.test(sourceType)
) {
throw new TypeError(
'Key "sourceType": Expected "script", "module", or "commonjs".',
);
}
}
/**
* Validates the globals property.
* @param {Object} globals The value to check.
* @returns {void}
* @throws {TypeError} If the value is invalid.
*/
function validateGlobals(globals) {
if (!isNonArrayObject(globals)) {
throw new TypeError('Key "globals": Expected an object.');
}
for (const key of Object.keys(globals)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (key !== key.trim()) {
throw new TypeError(
`Key "globals": Global "${key}" has leading or trailing whitespace.`,
);
}
if (!globalVariablesValues.has(globals[key])) {
throw new TypeError(
`Key "globals": Key "${key}": Expected "readonly", "writable", or "off".`,
);
}
}
}
/**
* Validates the parser property.
* @param {Object} parser The value to check.
* @returns {void}
* @throws {TypeError} If the value is invalid.
*/
function validateParser(parser) {
if (
!parser ||
typeof parser !== "object" ||
(typeof parser.parse !== "function" &&
typeof parser.parseForESLint !== "function")
) {
throw new TypeError(
'Key "parser": Expected object with parse() or parseForESLint() method.',
);
}
}
/**
* Validates the language options.
* @param {Object} languageOptions The language options to validate.
* @returns {void}
* @throws {TypeError} If the language options are invalid.
*/
function validateLanguageOptions(languageOptions) {
if (!isNonArrayObject(languageOptions)) {
throw new TypeError("Expected an object.");
}
const {
ecmaVersion,
sourceType,
globals,
parser,
parserOptions,
...otherOptions
} = languageOptions;
if ("ecmaVersion" in languageOptions) {
validateEcmaVersion(ecmaVersion);
}
if ("sourceType" in languageOptions) {
validateSourceType(sourceType);
}
if ("globals" in languageOptions) {
validateGlobals(globals);
}
if ("parser" in languageOptions) {
validateParser(parser);
}
if ("parserOptions" in languageOptions) {
if (!isNonArrayObject(parserOptions)) {
throw new TypeError('Key "parserOptions": Expected an object.');
}
}
const otherOptionKeys = Object.keys(otherOptions);
if (otherOptionKeys.length > 0) {
throw new TypeError(`Unexpected key "${otherOptionKeys[0]}" found.`);
}
}
module.exports = { validateLanguageOptions };
+328 -447
View File
@@ -9,27 +9,23 @@
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").Position} Position */
/** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
//------------------------------------------------------------------------------
// Module Definition
//------------------------------------------------------------------------------
const escapeRegExp = require("escape-string-regexp");
const { Config } = require("../config/config.js");
/**
* Compares the locations of two objects in a source file
* @param {Position} itemA The first object
* @param {Position} itemB The second object
* @param {{line: number, column: number}} itemA The first object
* @param {{line: number, column: number}} itemB The second object
* @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
* itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
*/
function compareLocations(itemA, itemB) {
return itemA.line - itemB.line || itemA.column - itemB.column;
return itemA.line - itemB.line || itemA.column - itemB.column;
}
/**
@@ -37,180 +33,170 @@ function compareLocations(itemA, itemB) {
* @param {Iterable<Directive>} directives Unused directives to be removed.
* @returns {Directive[][]} Directives grouped by their parent comment.
*/
function groupByParentDirective(directives) {
const groups = new Map();
function groupByParentComment(directives) {
const groups = new Map();
for (const directive of directives) {
const {
unprocessedDirective: { parentDirective },
} = directive;
for (const directive of directives) {
const { unprocessedDirective: { parentComment } } = directive;
if (groups.has(parentDirective)) {
groups.get(parentDirective).push(directive);
} else {
groups.set(parentDirective, [directive]);
}
}
if (groups.has(parentComment)) {
groups.get(parentComment).push(directive);
} else {
groups.set(parentComment, [directive]);
}
}
return [...groups.values()];
return [...groups.values()];
}
/**
* Creates removal details for a set of directives within the same comment.
* @param {Directive[]} directives Unused directives to be removed.
* @param {{node: Token, value: string}} parentDirective Data about the backing directive.
* @param {SourceCode} sourceCode The source code object for the file being linted.
* @param {Token} commentToken The backing Comment token.
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
*/
function createIndividualDirectivesRemoval(
directives,
parentDirective,
sourceCode,
) {
/*
* Get the list of the rules text without any surrounding whitespace. In order to preserve the original
* formatting, we don't want to change that whitespace.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*/
const listText = parentDirective.value.trim();
function createIndividualDirectivesRemoval(directives, commentToken) {
// Calculate where it starts in the source code text
const listStart = sourceCode.text.indexOf(
listText,
sourceCode.getRange(parentDirective.node)[0],
);
/*
* `commentToken.value` starts right after `//` or `/*`.
* All calculated offsets will be relative to this index.
*/
const commentValueStart = commentToken.range[0] + "//".length;
/*
* We can assume that `listText` contains multiple elements.
* Otherwise, this function wouldn't be called - if there is
* only one rule in the list, then the whole comment must be removed.
*/
// Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
const listStartOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
return directives.map(directive => {
const { ruleId } = directive;
/*
* Get the list text without any surrounding whitespace. In order to preserve the original
* formatting, we don't want to change that whitespace.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*/
const listText = commentToken.value
.slice(listStartOffset) // remove directive name and all whitespace before the list
.split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
.trimEnd(); // remove all whitespace after the list
const regex = new RegExp(
String.raw`(?:^|\s*,\s*)(?<quote>['"]?)${escapeRegExp(ruleId)}\k<quote>(?:\s*,\s*|$)`,
"u",
);
const match = regex.exec(listText);
const matchedText = match[0];
const matchStart = listStart + match.index;
const matchEnd = matchStart + matchedText.length;
/*
* We can assume that `listText` contains multiple elements.
* Otherwise, this function wouldn't be called - if there is
* only one rule in the list, then the whole comment must be removed.
*/
const firstIndexOfComma = matchedText.indexOf(",");
const lastIndexOfComma = matchedText.lastIndexOf(",");
return directives.map(directive => {
const { ruleId } = directive;
let removalStart, removalEnd;
const regex = new RegExp(String.raw`(?:^|\s*,\s*)(?<quote>['"]?)${escapeRegExp(ruleId)}\k<quote>(?:\s*,\s*|$)`, "u");
const match = regex.exec(listText);
const matchedText = match[0];
const matchStartOffset = listStartOffset + match.index;
const matchEndOffset = matchStartOffset + matchedText.length;
if (firstIndexOfComma !== lastIndexOfComma) {
/*
* Since there are two commas, this must one of the elements in the middle of the list.
* Matched range starts where the previous rule name ends, and ends where the next rule name starts.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^^
*
* We want to remove only the content between the two commas, and also one of the commas.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^
*/
removalStart = matchStart + firstIndexOfComma;
removalEnd = matchStart + lastIndexOfComma;
} else {
/*
* This is either the first element or the last element.
*
* If this is the first element, matched range starts where the first rule name starts
* and ends where the second rule name starts. This is exactly the range we want
* to remove so that the second rule name will start where the first one was starting
* and thus preserve the original formatting.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^
*
* Similarly, if this is the last element, we've already matched the range we want to
* remove. The previous rule name will end where the last one was ending, relative
* to the content on the right side.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^
*/
removalStart = matchStart;
removalEnd = matchEnd;
}
const firstIndexOfComma = matchedText.indexOf(",");
const lastIndexOfComma = matchedText.lastIndexOf(",");
return {
description: `'${ruleId}'`,
fix: {
range: [removalStart, removalEnd],
text: "",
},
unprocessedDirective: directive.unprocessedDirective,
};
});
let removalStartOffset, removalEndOffset;
if (firstIndexOfComma !== lastIndexOfComma) {
/*
* Since there are two commas, this must one of the elements in the middle of the list.
* Matched range starts where the previous rule name ends, and ends where the next rule name starts.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^^
*
* We want to remove only the content between the two commas, and also one of the commas.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^
*/
removalStartOffset = matchStartOffset + firstIndexOfComma;
removalEndOffset = matchStartOffset + lastIndexOfComma;
} else {
/*
* This is either the first element or the last element.
*
* If this is the first element, matched range starts where the first rule name starts
* and ends where the second rule name starts. This is exactly the range we want
* to remove so that the second rule name will start where the first one was starting
* and thus preserve the original formatting.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^
*
* Similarly, if this is the last element, we've already matched the range we want to
* remove. The previous rule name will end where the last one was ending, relative
* to the content on the right side.
*
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^
*/
removalStartOffset = matchStartOffset;
removalEndOffset = matchEndOffset;
}
return {
description: `'${ruleId}'`,
fix: {
range: [
commentValueStart + removalStartOffset,
commentValueStart + removalEndOffset
],
text: ""
},
unprocessedDirective: directive.unprocessedDirective
};
});
}
/**
* Creates a description of deleting an entire unused disable directive.
* Creates a description of deleting an entire unused disable comment.
* @param {Directive[]} directives Unused directives to be removed.
* @param {Token} node The backing Comment token.
* @param {SourceCode} sourceCode The source code object for the file being linted.
* @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output problem.
* @param {Token} commentToken The backing Comment token.
* @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output Problem.
*/
function createDirectiveRemoval(directives, node, sourceCode) {
const range = sourceCode.getRange(node);
const ruleIds = directives
.filter(directive => directive.ruleId)
.map(directive => `'${directive.ruleId}'`);
function createCommentRemoval(directives, commentToken) {
const { range } = commentToken;
const ruleIds = directives.filter(directive => directive.ruleId).map(directive => `'${directive.ruleId}'`);
return {
description:
ruleIds.length <= 2
? ruleIds.join(" or ")
: `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds.at(-1)}`,
fix: {
range,
text: " ",
},
unprocessedDirective: directives[0].unprocessedDirective,
};
return {
description: ruleIds.length <= 2
? ruleIds.join(" or ")
: `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds[ruleIds.length - 1]}`,
fix: {
range,
text: " "
},
unprocessedDirective: directives[0].unprocessedDirective
};
}
/**
* Parses details from directives to create output Problems.
* @param {Iterable<Directive>} allDirectives Unused directives to be removed.
* @param {SourceCode} sourceCode The source code object for the file being linted.
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
*/
function processUnusedDirectives(allDirectives, sourceCode) {
const directiveGroups = groupByParentDirective(allDirectives);
function processUnusedDirectives(allDirectives) {
const directiveGroups = groupByParentComment(allDirectives);
return directiveGroups.flatMap(directives => {
const { parentDirective } = directives[0].unprocessedDirective;
const remainingRuleIds = new Set(parentDirective.ruleIds);
return directiveGroups.flatMap(
directives => {
const { parentComment } = directives[0].unprocessedDirective;
const remainingRuleIds = new Set(parentComment.ruleIds);
for (const directive of directives) {
remainingRuleIds.delete(directive.ruleId);
}
for (const directive of directives) {
remainingRuleIds.delete(directive.ruleId);
}
return remainingRuleIds.size
? createIndividualDirectivesRemoval(
directives,
parentDirective,
sourceCode,
)
: [
createDirectiveRemoval(
directives,
parentDirective.node,
sourceCode,
),
];
});
return remainingRuleIds.size
? createIndividualDirectivesRemoval(directives, parentComment.commentToken)
: [createCommentRemoval(directives, parentComment.commentToken)];
}
);
}
/**
@@ -219,83 +205,87 @@ function processUnusedDirectives(allDirectives, sourceCode) {
* @returns {Set<Directive>} The used eslint-enable comments
*/
function collectUsedEnableDirectives(directives) {
/**
* A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
* If `eslint-enable` does not have a ruleId, the key will be `null`.
* @type {Map<string|null, Directive>}
*/
const enabledRules = new Map();
/**
* A Set of `eslint-enable` marked as used.
* It is also the return value of `collectUsedEnableDirectives` function.
* @type {Set<Directive>}
*/
const usedEnableDirectives = new Set();
/**
* A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
* If `eslint-enable` does not have a ruleId, the key will be `null`.
* @type {Map<string|null, Directive>}
*/
const enabledRules = new Map();
/*
* Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
* and if so, stores the `eslint-enable` in `usedEnableDirectives`.
*/
for (let index = directives.length - 1; index >= 0; index--) {
const directive = directives[index];
/**
* A Set of `eslint-enable` marked as used.
* It is also the return value of `collectUsedEnableDirectives` function.
* @type {Set<Directive>}
*/
const usedEnableDirectives = new Set();
if (directive.type === "disable") {
if (enabledRules.size === 0) {
continue;
}
if (directive.ruleId === null) {
// If encounter `eslint-disable` without ruleId,
// mark all `eslint-enable` currently held in enabledRules as used.
// e.g.
// /* eslint-disable */ <- current directive
// /* eslint-enable rule-id1 */ <- used
// /* eslint-enable rule-id2 */ <- used
// /* eslint-enable */ <- used
for (const enableDirective of enabledRules.values()) {
usedEnableDirectives.add(enableDirective);
}
enabledRules.clear();
} else {
const enableDirective = enabledRules.get(directive.ruleId);
/*
* Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
* and if so, stores the `eslint-enable` in `usedEnableDirectives`.
*/
for (let index = directives.length - 1; index >= 0; index--) {
const directive = directives[index];
if (enableDirective) {
// If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` with ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable rule-id */ <- used
usedEnableDirectives.add(enableDirective);
} else {
const enabledDirectiveWithoutRuleId =
enabledRules.get(null);
if (directive.type === "disable") {
if (enabledRules.size === 0) {
continue;
}
if (directive.ruleId === null) {
if (enabledDirectiveWithoutRuleId) {
// If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` without ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable */ <- used
usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
}
}
}
} else if (directive.type === "enable") {
if (directive.ruleId === null) {
// If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
// So clear enabledRules.
// e.g.
// /* eslint-enable */ <- current directive
// /* eslint-enable rule-id *// <- unused
// /* eslint-enable */ <- unused
enabledRules.clear();
enabledRules.set(null, directive);
} else {
enabledRules.set(directive.ruleId, directive);
}
}
}
return usedEnableDirectives;
// If encounter `eslint-disable` without ruleId,
// mark all `eslint-enable` currently held in enabledRules as used.
// e.g.
// /* eslint-disable */ <- current directive
// /* eslint-enable rule-id1 */ <- used
// /* eslint-enable rule-id2 */ <- used
// /* eslint-enable */ <- used
for (const enableDirective of enabledRules.values()) {
usedEnableDirectives.add(enableDirective);
}
enabledRules.clear();
} else {
const enableDirective = enabledRules.get(directive.ruleId);
if (enableDirective) {
// If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` with ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable rule-id */ <- used
usedEnableDirectives.add(enableDirective);
} else {
const enabledDirectiveWithoutRuleId = enabledRules.get(null);
if (enabledDirectiveWithoutRuleId) {
// If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` without ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable */ <- used
usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
}
}
}
} else if (directive.type === "enable") {
if (directive.ruleId === null) {
// If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
// So clear enabledRules.
// e.g.
// /* eslint-enable */ <- current directive
// /* eslint-enable rule-id *// <- unused
// /* eslint-enable */ <- unused
enabledRules.clear();
enabledRules.set(null, directive);
} else {
enabledRules.set(directive.ruleId, directive);
}
}
}
return usedEnableDirectives;
}
/**
@@ -309,140 +299,105 @@ function collectUsedEnableDirectives(directives) {
* of problems (including suppressed ones) and unused eslint-disable directives
*/
function applyDirectives(options) {
const problems = [];
const usedDisableDirectives = new Set();
const { sourceCode } = options;
const problems = [];
const usedDisableDirectives = new Set();
for (const problem of options.problems) {
let disableDirectivesForProblem = [];
let nextDirectiveIndex = 0;
for (const problem of options.problems) {
let disableDirectivesForProblem = [];
let nextDirectiveIndex = 0;
while (
nextDirectiveIndex < options.directives.length &&
compareLocations(options.directives[nextDirectiveIndex], problem) <=
0
) {
const directive = options.directives[nextDirectiveIndex++];
while (
nextDirectiveIndex < options.directives.length &&
compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
) {
const directive = options.directives[nextDirectiveIndex++];
if (
directive.ruleId === null ||
directive.ruleId === problem.ruleId
) {
switch (directive.type) {
case "disable":
disableDirectivesForProblem.push(directive);
break;
if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
switch (directive.type) {
case "disable":
disableDirectivesForProblem.push(directive);
break;
case "enable":
disableDirectivesForProblem = [];
break;
case "enable":
disableDirectivesForProblem = [];
break;
// no default
}
}
}
// no default
}
}
}
if (disableDirectivesForProblem.length > 0) {
const suppressions = disableDirectivesForProblem.map(directive => ({
kind: "directive",
justification: directive.unprocessedDirective.justification,
}));
if (disableDirectivesForProblem.length > 0) {
const suppressions = disableDirectivesForProblem.map(directive => ({
kind: "directive",
justification: directive.unprocessedDirective.justification
}));
if (problem.suppressions) {
problem.suppressions =
problem.suppressions.concat(suppressions);
} else {
problem.suppressions = suppressions;
usedDisableDirectives.add(disableDirectivesForProblem.at(-1));
}
}
if (problem.suppressions) {
problem.suppressions = problem.suppressions.concat(suppressions);
} else {
problem.suppressions = suppressions;
usedDisableDirectives.add(disableDirectivesForProblem[disableDirectivesForProblem.length - 1]);
}
}
problems.push(problem);
}
problems.push(problem);
}
const unusedDisableDirectivesToReport = options.directives.filter(
directive =>
directive.type === "disable" &&
!usedDisableDirectives.has(directive) &&
!options.rulesToIgnore.has(directive.ruleId),
);
const unusedDisableDirectivesToReport = options.directives
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));
const unusedEnableDirectivesToReport = new Set(
options.directives.filter(
directive =>
directive.unprocessedDirective.type === "enable" &&
!options.rulesToIgnore.has(directive.ruleId),
),
);
/*
* If directives has the eslint-enable directive,
* check whether the eslint-enable comment is used.
*/
if (unusedEnableDirectivesToReport.size > 0) {
for (const directive of collectUsedEnableDirectives(
options.directives,
)) {
unusedEnableDirectivesToReport.delete(directive);
}
}
const unusedEnableDirectivesToReport = new Set(
options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
);
const processed = processUnusedDirectives(
unusedDisableDirectivesToReport,
sourceCode,
).concat(
processUnusedDirectives(unusedEnableDirectivesToReport, sourceCode),
);
const columnOffset = options.language.columnStart === 1 ? 0 : 1;
const lineOffset = options.language.lineStart === 1 ? 0 : 1;
/*
* If directives has the eslint-enable directive,
* check whether the eslint-enable comment is used.
*/
if (unusedEnableDirectivesToReport.size > 0) {
for (const directive of collectUsedEnableDirectives(options.directives)) {
unusedEnableDirectivesToReport.delete(directive);
}
}
const unusedDirectives = processed.map(
({ description, fix, unprocessedDirective }) => {
const { parentDirective, type, line, column } =
unprocessedDirective;
const processed = processUnusedDirectives(unusedDisableDirectivesToReport)
.concat(processUnusedDirectives(unusedEnableDirectivesToReport));
let message;
const unusedDirectives = processed
.map(({ description, fix, unprocessedDirective }) => {
const { parentComment, type, line, column } = unprocessedDirective;
if (type === "enable") {
message = description
? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
: "Unused eslint-enable directive (no matching eslint-disable directives were found).";
} else {
message = description
? `Unused eslint-disable directive (no problems were reported from ${description}).`
: "Unused eslint-disable directive (no problems were reported).";
}
let message;
const loc = sourceCode.getLoc(parentDirective.node);
if (type === "enable") {
message = description
? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
: "Unused eslint-enable directive (no matching eslint-disable directives were found).";
} else {
message = description
? `Unused eslint-disable directive (no problems were reported from ${description}).`
: "Unused eslint-disable directive (no problems were reported).";
}
return {
ruleId: null,
message,
line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
nodeType: null,
...options.disableFixes ? {} : { fix }
};
});
return {
ruleId: null,
message,
line:
type === "disable-next-line"
? loc.start.line + lineOffset
: line,
column:
type === "disable-next-line"
? loc.start.column + columnOffset
: column,
severity:
options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
nodeType: null,
...(options.disableFixes ? {} : { fix }),
};
},
);
return { problems, unusedDirectives };
return { problems, unusedDirectives };
}
/**
* Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
* of reported problems, adds the suppression information to the problems.
* @param {Object} options Information about directives and problems
* @param {Language} options.language The language being linted.
* @param {SourceCode} options.sourceCode The source code object for the file being linted.
* @param {{
* type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
* ruleId: (string|null),
@@ -455,130 +410,56 @@ function applyDirectives(options) {
* @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
* A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
* @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
* @param {RulesConfig} options.configuredRules The rules configuration.
* @param {Function} options.ruleFilter A predicate function to filter which rules should be executed.
* @param {boolean} options.disableFixes If true, it doesn't make `fix` properties.
* @returns {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]}
* An object with a list of reported problems, the suppressed of which contain the suppression information.
*/
module.exports = ({
language,
sourceCode,
directives,
disableFixes,
problems,
configuredRules,
ruleFilter,
reportUnusedDisableDirectives = "off",
}) => {
const blockDirectives = directives
.filter(
directive =>
directive.type === "disable" || directive.type === "enable",
)
.map(directive =>
Object.assign({}, directive, { unprocessedDirective: directive }),
)
.sort(compareLocations);
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => {
const blockDirectives = directives
.filter(directive => directive.type === "disable" || directive.type === "enable")
.map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
.sort(compareLocations);
const lineDirectives = directives
.flatMap(directive => {
switch (directive.type) {
case "disable":
case "enable":
return [];
const lineDirectives = directives.flatMap(directive => {
switch (directive.type) {
case "disable":
case "enable":
return [];
case "disable-line":
return [
{
type: "disable",
line: directive.line,
column: 1,
ruleId: directive.ruleId,
unprocessedDirective: directive,
},
{
type: "enable",
line: directive.line + 1,
column: 0,
ruleId: directive.ruleId,
unprocessedDirective: directive,
},
];
case "disable-line":
return [
{ type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
{ type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
];
case "disable-next-line":
return [
{
type: "disable",
line: directive.line + 1,
column: 1,
ruleId: directive.ruleId,
unprocessedDirective: directive,
},
{
type: "enable",
line: directive.line + 2,
column: 0,
ruleId: directive.ruleId,
unprocessedDirective: directive,
},
];
case "disable-next-line":
return [
{ type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
{ type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
];
default:
throw new TypeError(
`Unrecognized directive type '${directive.type}'`,
);
}
})
.sort(compareLocations);
default:
throw new TypeError(`Unrecognized directive type '${directive.type}'`);
}
}).sort(compareLocations);
// This determines a list of rules that are not being run by the given ruleFilter, if present.
const rulesToIgnore =
configuredRules && ruleFilter
? new Set(
Object.keys(configuredRules).filter(ruleId => {
const severity = Config.getRuleNumericSeverity(
configuredRules[ruleId],
);
const blockDirectivesResult = applyDirectives({
problems,
directives: blockDirectives,
disableFixes,
reportUnusedDisableDirectives
});
const lineDirectivesResult = applyDirectives({
problems: blockDirectivesResult.problems,
directives: lineDirectives,
disableFixes,
reportUnusedDisableDirectives
});
// Ignore for disabled rules.
if (severity === 0) {
return false;
}
return !ruleFilter({ severity, ruleId });
}),
)
: new Set();
// If no ruleId is supplied that means this directive is applied to all rules, so we can't determine if it's unused if any rules are filtered out.
if (rulesToIgnore.size > 0) {
rulesToIgnore.add(null);
}
const blockDirectivesResult = applyDirectives({
language,
sourceCode,
problems,
directives: blockDirectives,
disableFixes,
reportUnusedDisableDirectives,
rulesToIgnore,
});
const lineDirectivesResult = applyDirectives({
language,
sourceCode,
problems: blockDirectivesResult.problems,
directives: lineDirectives,
disableFixes,
reportUnusedDisableDirectives,
rulesToIgnore,
});
return reportUnusedDisableDirectives !== "off"
? lineDirectivesResult.problems
.concat(blockDirectivesResult.unusedDirectives)
.concat(lineDirectivesResult.unusedDirectives)
.sort(compareLocations)
: lineDirectivesResult.problems;
return reportUnusedDisableDirectives !== "off"
? lineDirectivesResult.problems
.concat(blockDirectivesResult.unusedDirectives)
.concat(lineDirectivesResult.unusedDirectives)
.sort(compareLocations)
: lineDirectivesResult.problems;
};
File diff suppressed because it is too large. Load diff
+190 -189
View File
@@ -21,7 +21,7 @@ const debug = require("./debug-helpers");
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
return segment.reachable;
}
//------------------------------------------------------------------------------
@@ -43,220 +43,221 @@ function isReachable(segment) {
* that this new segment follows.
*/
class CodePathSegment {
/**
* Creates a new instance.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* This array includes unreachable segments.
* @param {boolean} reachable A flag which shows this is reachable.
*/
constructor(id, allPrevSegments, reachable) {
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* An array of the next reachable segments.
* @type {CodePathSegment[]}
*/
this.nextSegments = [];
/**
* Creates a new instance.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* This array includes unreachable segments.
* @param {boolean} reachable A flag which shows this is reachable.
*/
constructor(id, allPrevSegments, reachable) {
/**
* An array of the previous reachable segments.
* @type {CodePathSegment[]}
*/
this.prevSegments = allPrevSegments.filter(isReachable);
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* An array of all next segments including reachable and unreachable.
* @type {CodePathSegment[]}
*/
this.allNextSegments = [];
/**
* An array of the next reachable segments.
* @type {CodePathSegment[]}
*/
this.nextSegments = [];
/**
* An array of all previous segments including reachable and unreachable.
* @type {CodePathSegment[]}
*/
this.allPrevSegments = allPrevSegments;
/**
* An array of the previous reachable segments.
* @type {CodePathSegment[]}
*/
this.prevSegments = allPrevSegments.filter(isReachable);
/**
* A flag which shows this is reachable.
* @type {boolean}
*/
this.reachable = reachable;
/**
* An array of all next segments including reachable and unreachable.
* @type {CodePathSegment[]}
*/
this.allNextSegments = [];
// Internal data.
Object.defineProperty(this, "internal", {
value: {
// determines if the segment has been attached to the code path
used: false,
/**
* An array of all previous segments including reachable and unreachable.
* @type {CodePathSegment[]}
*/
this.allPrevSegments = allPrevSegments;
// array of previous segments coming from the end of a loop
loopedPrevSegments: [],
},
});
/**
* A flag which shows this is reachable.
* @type {boolean}
*/
this.reachable = reachable;
/* c8 ignore start */
if (debug.enabled) {
this.internal.nodes = [];
} /* c8 ignore stop */
}
// Internal data.
Object.defineProperty(this, "internal", {
value: {
/**
* Checks a given previous segment is coming from the end of a loop.
* @param {CodePathSegment} segment A previous segment to check.
* @returns {boolean} `true` if the segment is coming from the end of a loop.
*/
isLoopedPrevSegment(segment) {
return this.internal.loopedPrevSegments.includes(segment);
}
// determines if the segment has been attached to the code path
used: false,
/**
* Creates the root segment.
* @param {string} id An identifier.
* @returns {CodePathSegment} The created segment.
*/
static newRoot(id) {
return new CodePathSegment(id, [], true);
}
// array of previous segments coming from the end of a loop
loopedPrevSegments: []
}
});
/**
* Creates a new segment and appends it after the given segments.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments
* to append to.
* @returns {CodePathSegment} The created segment.
*/
static newNext(id, allPrevSegments) {
return new CodePathSegment(
id,
CodePathSegment.flattenUnusedSegments(allPrevSegments),
allPrevSegments.some(isReachable),
);
}
/* c8 ignore start */
if (debug.enabled) {
this.internal.nodes = [];
}/* c8 ignore stop */
}
/**
* Creates an unreachable segment and appends it after the given segments.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
static newUnreachable(id, allPrevSegments) {
const segment = new CodePathSegment(
id,
CodePathSegment.flattenUnusedSegments(allPrevSegments),
false,
);
/**
* Checks a given previous segment is coming from the end of a loop.
* @param {CodePathSegment} segment A previous segment to check.
* @returns {boolean} `true` if the segment is coming from the end of a loop.
*/
isLoopedPrevSegment(segment) {
return this.internal.loopedPrevSegments.includes(segment);
}
/*
* In `if (a) return a; foo();` case, the unreachable segment preceded by
* the return statement is not used but must not be removed.
*/
CodePathSegment.markUsed(segment);
/**
* Creates the root segment.
* @param {string} id An identifier.
* @returns {CodePathSegment} The created segment.
*/
static newRoot(id) {
return new CodePathSegment(id, [], true);
}
return segment;
}
/**
* Creates a new segment and appends it after the given segments.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments
* to append to.
* @returns {CodePathSegment} The created segment.
*/
static newNext(id, allPrevSegments) {
return new CodePathSegment(
id,
CodePathSegment.flattenUnusedSegments(allPrevSegments),
allPrevSegments.some(isReachable)
);
}
/**
* Creates a segment that follows given segments.
* This factory method does not connect with `allPrevSegments`.
* But this inherits `reachable` flag.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
static newDisconnected(id, allPrevSegments) {
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
}
/**
* Creates an unreachable segment and appends it after the given segments.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
static newUnreachable(id, allPrevSegments) {
const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false);
/**
* Marks a given segment as used.
*
* And this function registers the segment into the previous segments as a next.
* @param {CodePathSegment} segment A segment to mark.
* @returns {void}
*/
static markUsed(segment) {
if (segment.internal.used) {
return;
}
segment.internal.used = true;
/*
* In `if (a) return a; foo();` case, the unreachable segment preceded by
* the return statement is not used but must not be removed.
*/
CodePathSegment.markUsed(segment);
let i;
return segment;
}
if (segment.reachable) {
/*
* If the segment is reachable, then it's officially part of the
* code path. This loops through all previous segments to update
* their list of next segments. Because the segment is reachable,
* it's added to both `nextSegments` and `allNextSegments`.
*/
for (i = 0; i < segment.allPrevSegments.length; ++i) {
const prevSegment = segment.allPrevSegments[i];
/**
* Creates a segment that follows given segments.
* This factory method does not connect with `allPrevSegments`.
* But this inherits `reachable` flag.
* @param {string} id An identifier.
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
* @returns {CodePathSegment} The created segment.
*/
static newDisconnected(id, allPrevSegments) {
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
}
prevSegment.allNextSegments.push(segment);
prevSegment.nextSegments.push(segment);
}
} else {
/*
* If the segment is not reachable, then it's not officially part of the
* code path. This loops through all previous segments to update
* their list of next segments. Because the segment is not reachable,
* it's added only to `allNextSegments`.
*/
for (i = 0; i < segment.allPrevSegments.length; ++i) {
segment.allPrevSegments[i].allNextSegments.push(segment);
}
}
}
/**
* Marks a given segment as used.
*
* And this function registers the segment into the previous segments as a next.
* @param {CodePathSegment} segment A segment to mark.
* @returns {void}
*/
static markUsed(segment) {
if (segment.internal.used) {
return;
}
segment.internal.used = true;
/**
* Marks a previous segment as looped.
* @param {CodePathSegment} segment A segment.
* @param {CodePathSegment} prevSegment A previous segment to mark.
* @returns {void}
*/
static markPrevSegmentAsLooped(segment, prevSegment) {
segment.internal.loopedPrevSegments.push(prevSegment);
}
let i;
/**
* Creates a new array based on an array of segments. If any segment in the
* array is unused, then it is replaced by all of its previous segments.
* All used segments are returned as-is without replacement.
* @param {CodePathSegment[]} segments The array of segments to flatten.
* @returns {CodePathSegment[]} The flattened array.
*/
static flattenUnusedSegments(segments) {
const done = new Set();
if (segment.reachable) {
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
/*
* If the segment is reachable, then it's officially part of the
* code path. This loops through all previous segments to update
* their list of next segments. Because the segment is reachable,
* it's added to both `nextSegments` and `allNextSegments`.
*/
for (i = 0; i < segment.allPrevSegments.length; ++i) {
const prevSegment = segment.allPrevSegments[i];
// Ignores duplicated.
if (done.has(segment)) {
continue;
}
prevSegment.allNextSegments.push(segment);
prevSegment.nextSegments.push(segment);
}
} else {
// Use previous segments if unused.
if (!segment.internal.used) {
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
const prevSegment = segment.allPrevSegments[j];
/*
* If the segment is not reachable, then it's not officially part of the
* code path. This loops through all previous segments to update
* their list of next segments. Because the segment is not reachable,
* it's added only to `allNextSegments`.
*/
for (i = 0; i < segment.allPrevSegments.length; ++i) {
segment.allPrevSegments[i].allNextSegments.push(segment);
}
}
}
if (!done.has(prevSegment)) {
done.add(prevSegment);
}
}
} else {
done.add(segment);
}
}
/**
* Marks a previous segment as looped.
* @param {CodePathSegment} segment A segment.
* @param {CodePathSegment} prevSegment A previous segment to mark.
* @returns {void}
*/
static markPrevSegmentAsLooped(segment, prevSegment) {
segment.internal.loopedPrevSegments.push(prevSegment);
}
return [...done];
}
/**
* Creates a new array based on an array of segments. If any segment in the
* array is unused, then it is replaced by all of its previous segments.
* All used segments are returned as-is without replacement.
* @param {CodePathSegment[]} segments The array of segments to flatten.
* @returns {CodePathSegment[]} The flattened array.
*/
static flattenUnusedSegments(segments) {
const done = new Set();
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
// Ignores duplicated.
if (done.has(segment)) {
continue;
}
// Use previous segments if unused.
if (!segment.internal.used) {
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
const prevSegment = segment.allPrevSegments[j];
if (!done.has(prevSegment)) {
done.add(prevSegment);
}
}
} else {
done.add(segment);
}
}
return [...done];
}
}
module.exports = CodePathSegment;
File diff suppressed because it is too large. Load diff
+286 -276
View File
@@ -20,313 +20,323 @@ const IdGenerator = require("./id-generator");
* A code path.
*/
class CodePath {
/**
* Creates a new instance.
* @param {Object} options Options for the function (see below).
* @param {string} options.id An identifier.
* @param {string} options.origin The type of code path origin.
* @param {CodePath|null} options.upper The code path of the upper function scope.
* @param {Function} options.onLooped A callback function to notify looping.
*/
constructor({ id, origin, upper, onLooped }) {
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* The reason that this code path was started. May be "program",
* "function", "class-field-initializer", or "class-static-block".
* @type {string}
*/
this.origin = origin;
/**
* Creates a new instance.
* @param {Object} options Options for the function (see below).
* @param {string} options.id An identifier.
* @param {string} options.origin The type of code path origin.
* @param {CodePath|null} options.upper The code path of the upper function scope.
* @param {Function} options.onLooped A callback function to notify looping.
*/
constructor({ id, origin, upper, onLooped }) {
/**
* The code path of the upper function scope.
* @type {CodePath|null}
*/
this.upper = upper;
/**
* The identifier of this code path.
* Rules use it to store additional information of each rule.
* @type {string}
*/
this.id = id;
/**
* The code paths of nested function scopes.
* @type {CodePath[]}
*/
this.childCodePaths = [];
/**
* The reason that this code path was started. May be "program",
* "function", "class-field-initializer", or "class-static-block".
* @type {string}
*/
this.origin = origin;
// Initializes internal state.
Object.defineProperty(this, "internal", {
value: new CodePathState(new IdGenerator(`${id}_`), onLooped),
});
/**
* The code path of the upper function scope.
* @type {CodePath|null}
*/
this.upper = upper;
// Adds this into `childCodePaths` of `upper`.
if (upper) {
upper.childCodePaths.push(this);
}
}
/**
* The code paths of nested function scopes.
* @type {CodePath[]}
*/
this.childCodePaths = [];
/**
* Gets the state of a given code path.
* @param {CodePath} codePath A code path to get.
* @returns {CodePathState} The state of the code path.
*/
static getState(codePath) {
return codePath.internal;
}
// Initializes internal state.
Object.defineProperty(
this,
"internal",
{ value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }
);
/**
* The initial code path segment. This is the segment that is at the head
* of the code path.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment}
*/
get initialSegment() {
return this.internal.initialSegment;
}
// Adds this into `childCodePaths` of `upper`.
if (upper) {
upper.childCodePaths.push(this);
}
}
/**
* Final code path segments. These are the terminal (tail) segments in the
* code path, which is the combination of `returnedSegments` and `thrownSegments`.
* All segments in this array are reachable.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
*/
get finalSegments() {
return this.internal.finalSegments;
}
/**
* Gets the state of a given code path.
* @param {CodePath} codePath A code path to get.
* @returns {CodePathState} The state of the code path.
*/
static getState(codePath) {
return codePath.internal;
}
/**
* Final code path segments that represent normal completion of the code path.
* For functions, this means both explicit `return` statements and implicit returns,
* such as the last reachable segment in a function that does not have an
* explicit `return` as this implicitly returns `undefined`. For scripts,
* modules, class field initializers, and class static blocks, this means
* all lines of code have been executed.
* These segments are also present in `finalSegments`.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
*/
get returnedSegments() {
return this.internal.returnedForkContext;
}
/**
* The initial code path segment. This is the segment that is at the head
* of the code path.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment}
*/
get initialSegment() {
return this.internal.initialSegment;
}
/**
* Final code path segments that represent `throw` statements.
* This is a passthrough to the underlying `CodePathState`.
* These segments are also present in `finalSegments`.
* @type {CodePathSegment[]}
*/
get thrownSegments() {
return this.internal.thrownForkContext;
}
/**
* Final code path segments. These are the terminal (tail) segments in the
* code path, which is the combination of `returnedSegments` and `thrownSegments`.
* All segments in this array are reachable.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
*/
get finalSegments() {
return this.internal.finalSegments;
}
/**
* Traverses all segments in this code path.
*
* codePath.traverseSegments((segment, controller) => {
* // do something.
* });
*
* This method enumerates segments in order from the head.
*
* The `controller` argument has two methods:
*
* - `skip()` - skips the following segments in this branch
* - `break()` - skips all following segments in the traversal
*
* A note on the parameters: the `options` argument is optional. This means
* the first argument might be an options object or the callback function.
* @param {Object} [optionsOrCallback] Optional first and last segments to traverse.
* @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse.
* @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse.
* @param {Function} callback A callback function.
* @returns {void}
*/
traverseSegments(optionsOrCallback, callback) {
// normalize the arguments into a callback and options
let resolvedOptions;
let resolvedCallback;
/**
* Final code path segments that represent normal completion of the code path.
* For functions, this means both explicit `return` statements and implicit returns,
* such as the last reachable segment in a function that does not have an
* explicit `return` as this implicitly returns `undefined`. For scripts,
* modules, class field initializers, and class static blocks, this means
* all lines of code have been executed.
* These segments are also present in `finalSegments`.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
*/
get returnedSegments() {
return this.internal.returnedForkContext;
}
if (typeof optionsOrCallback === "function") {
resolvedCallback = optionsOrCallback;
resolvedOptions = {};
} else {
resolvedOptions = optionsOrCallback || {};
resolvedCallback = callback;
}
/**
* Final code path segments that represent `throw` statements.
* This is a passthrough to the underlying `CodePathState`.
* These segments are also present in `finalSegments`.
* @type {CodePathSegment[]}
*/
get thrownSegments() {
return this.internal.thrownForkContext;
}
// determine where to start traversing from based on the options
const startSegment =
resolvedOptions.first || this.internal.initialSegment;
const lastSegment = resolvedOptions.last;
/**
* Tracks the traversal of the code path through each segment. This array
* starts empty and segments are added or removed as the code path is
* traversed. This array always ends up empty at the end of a code path
* traversal. The `CodePathState` uses this to track its progress through
* the code path.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
* @deprecated
*/
get currentSegments() {
return this.internal.currentSegments;
}
// set up initial location information
let record;
let index;
let end;
let segment = null;
/**
* Traverses all segments in this code path.
*
* codePath.traverseSegments((segment, controller) => {
* // do something.
* });
*
* This method enumerates segments in order from the head.
*
* The `controller` argument has two methods:
*
* - `skip()` - skips the following segments in this branch
* - `break()` - skips all following segments in the traversal
*
* A note on the parameters: the `options` argument is optional. This means
* the first argument might be an options object or the callback function.
* @param {Object} [optionsOrCallback] Optional first and last segments to traverse.
* @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse.
* @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse.
* @param {Function} callback A callback function.
* @returns {void}
*/
traverseSegments(optionsOrCallback, callback) {
// segments that have already been visited during traversal
const visited = new Set();
// normalize the arguments into a callback and options
let resolvedOptions;
let resolvedCallback;
// tracks the traversal steps
const stack = [[startSegment, 0]];
if (typeof optionsOrCallback === "function") {
resolvedCallback = optionsOrCallback;
resolvedOptions = {};
} else {
resolvedOptions = optionsOrCallback || {};
resolvedCallback = callback;
}
// segments that have been skipped during traversal
const skipped = new Set();
// determine where to start traversing from based on the options
const startSegment = resolvedOptions.first || this.internal.initialSegment;
const lastSegment = resolvedOptions.last;
// indicates if we exited early from the traversal
let broken = false;
// set up initial location information
let record = null;
let index = 0;
let end = 0;
let segment = null;
/**
* Maintains traversal state.
*/
const controller = {
/**
* Skip the following segments in this branch.
* @returns {void}
*/
skip() {
skipped.add(segment);
},
// segments that have already been visited during traversal
const visited = new Set();
/**
* Stop traversal completely - do not traverse to any
* other segments.
* @returns {void}
*/
break() {
broken = true;
},
};
// tracks the traversal steps
const stack = [[startSegment, 0]];
/**
* Checks if a given previous segment has been visited.
* @param {CodePathSegment} prevSegment A previous segment to check.
* @returns {boolean} `true` if the segment has been visited.
*/
function isVisited(prevSegment) {
return (
visited.has(prevSegment) ||
segment.isLoopedPrevSegment(prevSegment)
);
}
// tracks the last skipped segment during traversal
let skippedSegment = null;
/**
* Checks if a given previous segment has been skipped.
* @param {CodePathSegment} prevSegment A previous segment to check.
* @returns {boolean} `true` if the segment has been skipped.
*/
function isSkipped(prevSegment) {
return (
skipped.has(prevSegment) ||
segment.isLoopedPrevSegment(prevSegment)
);
}
// indicates if we exited early from the traversal
let broken = false;
// the traversal
while (stack.length > 0) {
/*
* This isn't a pure stack. We use the top record all the time
* but don't always pop it off. The record is popped only if
* one of the following is true:
*
* 1) We have already visited the segment.
* 2) We have not visited *all* of the previous segments.
* 3) We have traversed past the available next segments.
*
* Otherwise, we just read the value and sometimes modify the
* record as we traverse.
*/
record = stack.at(-1);
segment = record[0];
index = record[1];
/**
* Maintains traversal state.
*/
const controller = {
if (index === 0) {
// Skip if this segment has been visited already.
if (visited.has(segment)) {
stack.pop();
continue;
}
/**
* Skip the following segments in this branch.
* @returns {void}
*/
skip() {
if (stack.length <= 1) {
broken = true;
} else {
skippedSegment = stack[stack.length - 2][0];
}
},
// Skip if all previous segments have not been visited.
if (
segment !== startSegment &&
segment.prevSegments.length > 0 &&
!segment.prevSegments.every(isVisited)
) {
stack.pop();
continue;
}
/**
* Stop traversal completely - do not traverse to any
* other segments.
* @returns {void}
*/
break() {
broken = true;
}
};
visited.add(segment);
/**
* Checks if a given previous segment has been visited.
* @param {CodePathSegment} prevSegment A previous segment to check.
* @returns {boolean} `true` if the segment has been visited.
*/
function isVisited(prevSegment) {
return (
visited.has(prevSegment) ||
segment.isLoopedPrevSegment(prevSegment)
);
}
// Skips the segment if all previous segments have been skipped.
const shouldSkip =
skipped.size > 0 &&
segment.prevSegments.length > 0 &&
segment.prevSegments.every(isSkipped);
// the traversal
while (stack.length > 0) {
/*
* If the most recent segment hasn't been skipped, then we call
* the callback, passing in the segment and the controller.
*/
if (!shouldSkip) {
resolvedCallback.call(this, segment, controller);
/*
* This isn't a pure stack. We use the top record all the time
* but don't always pop it off. The record is popped only if
* one of the following is true:
*
* 1) We have already visited the segment.
* 2) We have not visited *all* of the previous segments.
* 3) We have traversed past the available next segments.
*
* Otherwise, we just read the value and sometimes modify the
* record as we traverse.
*/
record = stack[stack.length - 1];
segment = record[0];
index = record[1];
// exit if we're at the last segment
if (segment === lastSegment) {
controller.skip();
}
if (index === 0) {
/*
* If the previous statement was executed, or if the callback
* called a method on the controller, we might need to exit the
* loop, so check for that and break accordingly.
*/
if (broken) {
break;
}
} else {
// If the most recent segment has been skipped, then mark it as skipped.
skipped.add(segment);
}
}
// Skip if this segment has been visited already.
if (visited.has(segment)) {
stack.pop();
continue;
}
// Update the stack.
end = segment.nextSegments.length - 1;
if (index < end) {
/*
* If we haven't yet visited all of the next segments, update
* the current top record on the stack to the next index to visit
* and then push a record for the current segment on top.
*
* Setting the current top record's index lets us know how many
* times we've been here and ensures that the segment won't be
* reprocessed (because we only process segments with an index
* of 0).
*/
record[1] += 1;
stack.push([segment.nextSegments[index], 0]);
} else if (index === end) {
/*
* If we are at the last next segment, then reset the top record
* in the stack to next segment and set its index to 0 so it will
* be processed next.
*/
record[0] = segment.nextSegments[index];
record[1] = 0;
} else {
/*
* If index > end, that means we have no more segments that need
* processing. So, we pop that record off of the stack in order to
* continue traversing at the next level up.
*/
stack.pop();
}
}
}
// Skip if all previous segments have not been visited.
if (segment !== startSegment &&
segment.prevSegments.length > 0 &&
!segment.prevSegments.every(isVisited)
) {
stack.pop();
continue;
}
// Reset the skipping flag if all branches have been skipped.
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
skippedSegment = null;
}
visited.add(segment);
/*
* If the most recent segment hasn't been skipped, then we call
* the callback, passing in the segment and the controller.
*/
if (!skippedSegment) {
resolvedCallback.call(this, segment, controller);
// exit if we're at the last segment
if (segment === lastSegment) {
controller.skip();
}
/*
* If the previous statement was executed, or if the callback
* called a method on the controller, we might need to exit the
* loop, so check for that and break accordingly.
*/
if (broken) {
break;
}
}
}
// Update the stack.
end = segment.nextSegments.length - 1;
if (index < end) {
/*
* If we haven't yet visited all of the next segments, update
* the current top record on the stack to the next index to visit
* and then push a record for the current segment on top.
*
* Setting the current top record's index lets us know how many
* times we've been here and ensures that the segment won't be
* reprocessed (because we only process segments with an index
* of 0).
*/
record[1] += 1;
stack.push([segment.nextSegments[index], 0]);
} else if (index === end) {
/*
* If we are at the last next segment, then reset the top record
* in the stack to next segment and set its index to 0 so it will
* be processed next.
*/
record[0] = segment.nextSegments[index];
record[1] = 0;
} else {
/*
* If index > end, that means we have no more segments that need
* processing. So, we pop that record off of the stack in order to
* continue traversing at the next level up.
*/
stack.pop();
}
}
}
}
module.exports = CodePath;
+138 -158
View File
@@ -21,9 +21,8 @@ const debug = require("debug")("eslint:code-path");
* @returns {string} Id of the segment.
*/
/* c8 ignore next */
// eslint-disable-next-line jsdoc/require-jsdoc -- Ignoring
function getId(segment) {
return segment.id + (segment.reachable ? "" : "!");
function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc -- Ignoring
return segment.id + (segment.reachable ? "" : "!");
}
/**
@@ -33,16 +32,13 @@ function getId(segment) {
* @returns {string} The string representation.
*/
function nodeToString(node, label) {
const suffix = label ? `:${label}` : "";
const suffix = label ? `:${label}` : "";
switch (node.type) {
case "Identifier":
return `${node.type}${suffix} (${node.name})`;
case "Literal":
return `${node.type}${suffix} (${node.value})`;
default:
return `${node.type}${suffix}`;
}
switch (node.type) {
case "Identifier": return `${node.type}${suffix} (${node.name})`;
case "Literal": return `${node.type}${suffix} (${node.value})`;
default: return `${node.type}${suffix}`;
}
}
//------------------------------------------------------------------------------
@@ -50,174 +46,158 @@ function nodeToString(node, label) {
//------------------------------------------------------------------------------
module.exports = {
/**
* A flag that debug dumping is enabled or not.
* @type {boolean}
*/
enabled: debug.enabled,
/**
* Dumps given objects.
* @param {...any} args objects to dump.
* @returns {void}
*/
dump: debug,
/**
* A flag that debug dumping is enabled or not.
* @type {boolean}
*/
enabled: debug.enabled,
/**
* Dumps the current analyzing state.
* @param {ASTNode} node A node to dump.
* @param {CodePathState} state A state to dump.
* @param {boolean} leaving A flag whether or not it's leaving
* @returns {void}
*/
dumpState: !debug.enabled
? debug
: /* c8 ignore next */ function (node, state, leaving) {
for (let i = 0; i < state.currentSegments.length; ++i) {
const segInternal = state.currentSegments[i].internal;
/**
* Dumps given objects.
* @param {...any} args objects to dump.
* @returns {void}
*/
dump: debug,
if (leaving) {
const last = segInternal.nodes.length - 1;
/**
* Dumps the current analyzing state.
* @param {ASTNode} node A node to dump.
* @param {CodePathState} state A state to dump.
* @param {boolean} leaving A flag whether or not it's leaving
* @returns {void}
*/
dumpState: !debug.enabled ? debug : /* c8 ignore next */ function(node, state, leaving) {
for (let i = 0; i < state.currentSegments.length; ++i) {
const segInternal = state.currentSegments[i].internal;
if (
last >= 0 &&
segInternal.nodes[last] ===
nodeToString(node, "enter")
) {
segInternal.nodes[last] = nodeToString(
node,
void 0,
);
} else {
segInternal.nodes.push(nodeToString(node, "exit"));
}
} else {
segInternal.nodes.push(nodeToString(node, "enter"));
}
}
if (leaving) {
const last = segInternal.nodes.length - 1;
debug(
[
`${state.currentSegments.map(getId).join(",")})`,
`${node.type}${leaving ? ":exit" : ""}`,
].join(" "),
);
},
if (last >= 0 && segInternal.nodes[last] === nodeToString(node, "enter")) {
segInternal.nodes[last] = nodeToString(node, void 0);
} else {
segInternal.nodes.push(nodeToString(node, "exit"));
}
} else {
segInternal.nodes.push(nodeToString(node, "enter"));
}
}
/**
* Dumps a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to dump.
* @returns {void}
* @see http://www.graphviz.org
* @see http://www.webgraphviz.com
*/
dumpDot: !debug.enabled
? debug
: /* c8 ignore next */ function (codePath) {
let text =
"\n" +
"digraph {\n" +
'node[shape=box,style="rounded,filled",fillcolor=white];\n' +
'initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
debug([
`${state.currentSegments.map(getId).join(",")})`,
`${node.type}${leaving ? ":exit" : ""}`
].join(" "));
},
if (codePath.returnedSegments.length > 0) {
text +=
'final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
}
if (codePath.thrownSegments.length > 0) {
text +=
'thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true];\n';
}
/**
* Dumps a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to dump.
* @returns {void}
* @see http://www.graphviz.org
* @see http://www.webgraphviz.com
*/
dumpDot: !debug.enabled ? debug : /* c8 ignore next */ function(codePath) {
let text =
"\n" +
"digraph {\n" +
"node[shape=box,style=\"rounded,filled\",fillcolor=white];\n" +
"initial[label=\"\",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
const traceMap = Object.create(null);
const arrows = this.makeDotArrows(codePath, traceMap);
if (codePath.returnedSegments.length > 0) {
text += "final[label=\"\",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
}
if (codePath.thrownSegments.length > 0) {
text += "thrown[label=\"✘\",shape=circle,width=0.3,height=0.3,fixedsize=true];\n";
}
// eslint-disable-next-line guard-for-in -- Want ability to traverse prototype
for (const id in traceMap) {
const segment = traceMap[id];
const traceMap = Object.create(null);
const arrows = this.makeDotArrows(codePath, traceMap);
text += `${id}[`;
for (const id in traceMap) { // eslint-disable-line guard-for-in -- Want ability to traverse prototype
const segment = traceMap[id];
if (segment.reachable) {
text += 'label="';
} else {
text +=
'style="rounded,dashed,filled",fillcolor="#FF9800",label="<<unreachable>>\\n';
}
text += `${id}[`;
if (segment.internal.nodes.length > 0) {
text += segment.internal.nodes.join("\\n");
} else {
text += "????";
}
if (segment.reachable) {
text += "label=\"";
} else {
text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<<unreachable>>\\n";
}
text += '"];\n';
}
if (segment.internal.nodes.length > 0) {
text += segment.internal.nodes.join("\\n");
} else {
text += "????";
}
text += `${arrows}\n`;
text += "}";
debug("DOT", text);
},
text += "\"];\n";
}
/**
* Makes a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to make DOT.
* @param {Object} traceMap Optional. A map to check whether or not segments had been done.
* @returns {string} A DOT code of the code path.
*/
makeDotArrows(codePath, traceMap) {
const stack = [[codePath.initialSegment, 0]];
const done = traceMap || Object.create(null);
let lastId = codePath.initialSegment.id;
let text = `initial->${codePath.initialSegment.id}`;
text += `${arrows}\n`;
text += "}";
debug("DOT", text);
},
while (stack.length > 0) {
const item = stack.pop();
const segment = item[0];
const index = item[1];
/**
* Makes a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to make DOT.
* @param {Object} traceMap Optional. A map to check whether or not segments had been done.
* @returns {string} A DOT code of the code path.
*/
makeDotArrows(codePath, traceMap) {
const stack = [[codePath.initialSegment, 0]];
const done = traceMap || Object.create(null);
let lastId = codePath.initialSegment.id;
let text = `initial->${codePath.initialSegment.id}`;
if (done[segment.id] && index === 0) {
continue;
}
done[segment.id] = segment;
while (stack.length > 0) {
const item = stack.pop();
const segment = item[0];
const index = item[1];
const nextSegment = segment.allNextSegments[index];
if (done[segment.id] && index === 0) {
continue;
}
done[segment.id] = segment;
if (!nextSegment) {
continue;
}
const nextSegment = segment.allNextSegments[index];
if (lastId === segment.id) {
text += `->${nextSegment.id}`;
} else {
text += `;\n${segment.id}->${nextSegment.id}`;
}
lastId = nextSegment.id;
if (!nextSegment) {
continue;
}
stack.unshift([segment, 1 + index]);
stack.push([nextSegment, 0]);
}
if (lastId === segment.id) {
text += `->${nextSegment.id}`;
} else {
text += `;\n${segment.id}->${nextSegment.id}`;
}
lastId = nextSegment.id;
codePath.returnedSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->final";
} else {
text += `;\n${finalSegment.id}->final`;
}
lastId = null;
});
stack.unshift([segment, 1 + index]);
stack.push([nextSegment, 0]);
}
codePath.thrownSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->thrown";
} else {
text += `;\n${finalSegment.id}->thrown`;
}
lastId = null;
});
codePath.returnedSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->final";
} else {
text += `;\n${finalSegment.id}->final`;
}
lastId = null;
});
return `${text};`;
},
codePath.thrownSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->thrown";
} else {
text += `;\n${finalSegment.id}->thrown`;
}
lastId = null;
});
return `${text};`;
}
};
+243 -268
View File
@@ -13,8 +13,8 @@
// Requirements
//------------------------------------------------------------------------------
const assert = require("../../shared/assert"),
CodePathSegment = require("./code-path-segment");
const assert = require("assert"),
CodePathSegment = require("./code-path-segment");
//------------------------------------------------------------------------------
// Helpers
@@ -26,7 +26,7 @@ const assert = require("../../shared/assert"),
* @returns {boolean} `true` if the segment is reachable.
*/
function isReachable(segment) {
return segment.reachable;
return segment.reachable;
}
/**
@@ -54,43 +54,44 @@ function isReachable(segment) {
* @returns {Array<CodePathSegment>} An array of the newly-created segments.
*/
function createSegments(context, startIndex, endIndex, create) {
/** @type {Array<Array<CodePathSegment>>} */
const list = context.segmentsList;
/*
* Both `startIndex` and `endIndex` work the same way: if the number is zero
* or more, then the number is used as-is. If the number is negative,
* then that number is added to the length of the segments list to
* determine the index to use. That means -1 for either argument
* is the last element, -2 is the second to last, and so on.
*
* So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the
* effective `startIndex` is 0 and the effective `endIndex` is 2, so this function
* will include items at indices 0, 1, and 2.
*
* Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only
* be using the last segment in `list`.
*/
const normalizedBegin =
startIndex >= 0 ? startIndex : list.length + startIndex;
const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex;
/** @type {Array<Array<CodePathSegment>>} */
const list = context.segmentsList;
/** @type {Array<CodePathSegment>} */
const segments = [];
/*
* Both `startIndex` and `endIndex` work the same way: if the number is zero
* or more, then the number is used as-is. If the number is negative,
* then that number is added to the length of the segments list to
* determine the index to use. That means -1 for either argument
* is the last element, -2 is the second to last, and so on.
*
* So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the
* effective `startIndex` is 0 and the effective `endIndex` is 2, so this function
* will include items at indices 0, 1, and 2.
*
* Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only
* be using the last segment in `list`.
*/
const normalizedBegin = startIndex >= 0 ? startIndex : list.length + startIndex;
const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex;
for (let i = 0; i < context.count; ++i) {
// this is passed into `new CodePathSegment` to add to code path.
const allPrevSegments = [];
/** @type {Array<CodePathSegment>} */
const segments = [];
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
allPrevSegments.push(list[j][i]);
}
for (let i = 0; i < context.count; ++i) {
// note: `create` is just a wrapper that augments `new CodePathSegment`.
segments.push(create(context.idGenerator.next(), allPrevSegments));
}
// this is passed into `new CodePathSegment` to add to code path.
const allPrevSegments = [];
return segments;
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
allPrevSegments.push(list[j][i]);
}
// note: `create` is just a wrapper that augments `new CodePathSegment`.
segments.push(create(context.idGenerator.next(), allPrevSegments));
}
return segments;
}
/**
@@ -102,56 +103,50 @@ function createSegments(context, startIndex, endIndex, create) {
* @returns {Array<CodePathSegment>} The merged segments.
*/
function mergeExtraSegments(context, segments) {
let currentSegments = segments;
let currentSegments = segments;
/*
* We need to ensure that the array returned from this function contains no more
* than the number of segments that the context allows. `context.count` indicates
* how many items should be in the returned array to ensure that the new segment
* entries will line up with the already existing segment entries.
*/
while (currentSegments.length > context.count) {
const merged = [];
/*
* We need to ensure that the array returned from this function contains no more
* than the number of segments that the context allows. `context.count` indicates
* how many items should be in the returned array to ensure that the new segment
* entries will line up with the already existing segment entries.
*/
while (currentSegments.length > context.count) {
const merged = [];
/*
* Because `context.count` is a factor of 2 inside of a `finally` block,
* we can divide the segment count by 2 to merge the paths together.
* This loops through each segment in the list and creates a new `CodePathSegment`
* that has the segment and the segment two slots away as previous segments.
*
* If `currentSegments` is [a,b,c,d], this will create new segments e and f, such
* that:
*
* When `i` is 0:
* a->e
* c->e
*
* When `i` is 1:
* b->f
* d->f
*/
for (
let i = 0, length = Math.floor(currentSegments.length / 2);
i < length;
++i
) {
merged.push(
CodePathSegment.newNext(context.idGenerator.next(), [
currentSegments[i],
currentSegments[i + length],
]),
);
}
/*
* Because `context.count` is a factor of 2 inside of a `finally` block,
* we can divide the segment count by 2 to merge the paths together.
* This loops through each segment in the list and creates a new `CodePathSegment`
* that has the segment and the segment two slots away as previous segments.
*
* If `currentSegments` is [a,b,c,d], this will create new segments e and f, such
* that:
*
* When `i` is 0:
* a->e
* c->e
*
* When `i` is 1:
* b->f
* d->f
*/
for (let i = 0, length = Math.floor(currentSegments.length / 2); i < length; ++i) {
merged.push(CodePathSegment.newNext(
context.idGenerator.next(),
[currentSegments[i], currentSegments[i + length]]
));
}
/*
* Go through the loop condition one more time to see if we have the
* number of segments for the context. If not, we'll keep merging paths
* of the merged segments until we get there.
*/
currentSegments = merged;
}
/*
* Go through the loop condition one more time to see if we have the
* number of segments for the context. If not, we'll keep merging paths
* of the merged segments until we get there.
*/
currentSegments = merged;
}
return currentSegments;
return currentSegments;
}
//------------------------------------------------------------------------------
@@ -162,213 +157,193 @@ function mergeExtraSegments(context, segments) {
* Manages the forking of code paths.
*/
class ForkContext {
/**
* Creates a new instance.
* @param {IdGenerator} idGenerator An identifier generator for segments.
* @param {ForkContext|null} upper The preceding fork context.
* @param {number} count The number of parallel segments in each element
* of `segmentsList`.
*/
constructor(idGenerator, upper, count) {
/**
* The ID generator that will generate segment IDs for any new
* segments that are created.
* @type {IdGenerator}
*/
this.idGenerator = idGenerator;
/**
* The preceding fork context.
* @type {ForkContext|null}
*/
this.upper = upper;
/**
* Creates a new instance.
* @param {IdGenerator} idGenerator An identifier generator for segments.
* @param {ForkContext|null} upper The preceding fork context.
* @param {number} count The number of parallel segments in each element
* of `segmentsList`.
*/
constructor(idGenerator, upper, count) {
/**
* The number of elements in each element of `segmentsList`. In most
* cases, this is 1 but can be 2 when there is a `finally` present,
* which forks the code path outside of normal flow. In the case of nested
* `finally` blocks, this can be a multiple of 2.
* @type {number}
*/
this.count = count;
/**
* The ID generator that will generate segment IDs for any new
* segments that are created.
* @type {IdGenerator}
*/
this.idGenerator = idGenerator;
/**
* The segments within this context. Each element in this array has
* `count` elements that represent one step in each fork. For example,
* when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path
* a->c->e and one path b->d->f, and `count` is 2 because each element
* is an array with two elements.
* @type {Array<Array<CodePathSegment>>}
*/
this.segmentsList = [];
}
/**
* The preceding fork context.
* @type {ForkContext|null}
*/
this.upper = upper;
/**
* The segments that begin this fork context.
* @type {Array<CodePathSegment>}
*/
get head() {
const list = this.segmentsList;
/**
* The number of elements in each element of `segmentsList`. In most
* cases, this is 1 but can be 2 when there is a `finally` present,
* which forks the code path outside of normal flow. In the case of nested
* `finally` blocks, this can be a multiple of 2.
* @type {number}
*/
this.count = count;
return list.length === 0 ? [] : list.at(-1);
}
/**
* The segments within this context. Each element in this array has
* `count` elements that represent one step in each fork. For example,
* when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path
* a->c->e and one path b->d->f, and `count` is 2 because each element
* is an array with two elements.
* @type {Array<Array<CodePathSegment>>}
*/
this.segmentsList = [];
}
/**
* Indicates if the context contains no segments.
* @type {boolean}
*/
get empty() {
return this.segmentsList.length === 0;
}
/**
* The segments that begin this fork context.
* @type {Array<CodePathSegment>}
*/
get head() {
const list = this.segmentsList;
/**
* Indicates if there are any segments that are reachable.
* @type {boolean}
*/
get reachable() {
const segments = this.head;
return list.length === 0 ? [] : list[list.length - 1];
}
return segments.length > 0 && segments.some(isReachable);
}
/**
* Indicates if the context contains no segments.
* @type {boolean}
*/
get empty() {
return this.segmentsList.length === 0;
}
/**
* Creates new segments in this context and appends them to the end of the
* already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`.
* @param {number} startIndex The index of the first segment in the context
* that should be specified as previous segments for the newly created segments.
* @param {number} endIndex The index of the last segment in the context
* that should be specified as previous segments for the newly created segments.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeNext(startIndex, endIndex) {
return createSegments(
this,
startIndex,
endIndex,
CodePathSegment.newNext,
);
}
/**
* Indicates if there are any segments that are reachable.
* @type {boolean}
*/
get reachable() {
const segments = this.head;
/**
* Creates new unreachable segments in this context and appends them to the end of the
* already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`.
* @param {number} startIndex The index of the first segment in the context
* that should be specified as previous segments for the newly created segments.
* @param {number} endIndex The index of the last segment in the context
* that should be specified as previous segments for the newly created segments.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeUnreachable(startIndex, endIndex) {
return createSegments(
this,
startIndex,
endIndex,
CodePathSegment.newUnreachable,
);
}
return segments.length > 0 && segments.some(isReachable);
}
/**
* Creates new segments in this context and does not append them to the end
* of the already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`. The `startIndex` and `endIndex` are only used to determine if
* the new segments should be reachable. If any of the segments in this range
* are reachable then the new segments are also reachable; otherwise, the new
* segments are unreachable.
* @param {number} startIndex The index of the first segment in the context
* that should be considered for reachability.
* @param {number} endIndex The index of the last segment in the context
* that should be considered for reachability.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeDisconnected(startIndex, endIndex) {
return createSegments(
this,
startIndex,
endIndex,
CodePathSegment.newDisconnected,
);
}
/**
* Creates new segments in this context and appends them to the end of the
* already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`.
* @param {number} startIndex The index of the first segment in the context
* that should be specified as previous segments for the newly created segments.
* @param {number} endIndex The index of the last segment in the context
* that should be specified as previous segments for the newly created segments.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeNext(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newNext);
}
/**
* Adds segments to the head of this context.
* @param {Array<CodePathSegment>} segments The segments to add.
* @returns {void}
*/
add(segments) {
assert(
segments.length >= this.count,
`${segments.length} >= ${this.count}`,
);
this.segmentsList.push(mergeExtraSegments(this, segments));
}
/**
* Creates new unreachable segments in this context and appends them to the end of the
* already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`.
* @param {number} startIndex The index of the first segment in the context
* that should be specified as previous segments for the newly created segments.
* @param {number} endIndex The index of the last segment in the context
* that should be specified as previous segments for the newly created segments.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeUnreachable(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable);
}
/**
* Replaces the head segments with the given segments.
* The current head segments are removed.
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments.
* @returns {void}
*/
replaceHead(replacementHeadSegments) {
assert(
replacementHeadSegments.length >= this.count,
`${replacementHeadSegments.length} >= ${this.count}`,
);
this.segmentsList.splice(
-1,
1,
mergeExtraSegments(this, replacementHeadSegments),
);
}
/**
* Creates new segments in this context and does not append them to the end
* of the already existing `CodePathSegment`s specified by `startIndex` and
* `endIndex`. The `startIndex` and `endIndex` are only used to determine if
* the new segments should be reachable. If any of the segments in this range
* are reachable then the new segments are also reachable; otherwise, the new
* segments are unreachable.
* @param {number} startIndex The index of the first segment in the context
* that should be considered for reachability.
* @param {number} endIndex The index of the last segment in the context
* that should be considered for reachability.
* @returns {Array<CodePathSegment>} An array of the newly created segments.
*/
makeDisconnected(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected);
}
/**
* Adds all segments of a given fork context into this context.
* @param {ForkContext} otherForkContext The fork context to add from.
* @returns {void}
*/
addAll(otherForkContext) {
assert(otherForkContext.count === this.count);
this.segmentsList.push(...otherForkContext.segmentsList);
}
/**
* Adds segments to the head of this context.
* @param {Array<CodePathSegment>} segments The segments to add.
* @returns {void}
*/
add(segments) {
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
this.segmentsList.push(mergeExtraSegments(this, segments));
}
/**
* Clears all segments in this context.
* @returns {void}
*/
clear() {
this.segmentsList = [];
}
/**
* Replaces the head segments with the given segments.
* The current head segments are removed.
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments.
* @returns {void}
*/
replaceHead(replacementHeadSegments) {
assert(
replacementHeadSegments.length >= this.count,
`${replacementHeadSegments.length} >= ${this.count}`
);
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, replacementHeadSegments));
}
/**
* Creates a new root context, meaning that there are no parent
* fork contexts.
* @param {IdGenerator} idGenerator An identifier generator for segments.
* @returns {ForkContext} New fork context.
*/
static newRoot(idGenerator) {
const context = new ForkContext(idGenerator, null, 1);
/**
* Adds all segments of a given fork context into this context.
* @param {ForkContext} otherForkContext The fork context to add from.
* @returns {void}
*/
addAll(otherForkContext) {
assert(otherForkContext.count === this.count);
this.segmentsList.push(...otherForkContext.segmentsList);
}
context.add([CodePathSegment.newRoot(idGenerator.next())]);
/**
* Clears all segments in this context.
* @returns {void}
*/
clear() {
this.segmentsList = [];
}
return context;
}
/**
* Creates a new root context, meaning that there are no parent
* fork contexts.
* @param {IdGenerator} idGenerator An identifier generator for segments.
* @returns {ForkContext} New fork context.
*/
static newRoot(idGenerator) {
const context = new ForkContext(idGenerator, null, 1);
/**
* Creates an empty fork context preceded by a given context.
* @param {ForkContext} parentContext The parent fork context.
* @param {boolean} shouldForkLeavingPath Indicates that we are inside of
* a `finally` block and should therefore fork the path that leaves
* `finally`.
* @returns {ForkContext} New fork context.
*/
static newEmpty(parentContext, shouldForkLeavingPath) {
return new ForkContext(
parentContext.idGenerator,
parentContext,
(shouldForkLeavingPath ? 2 : 1) * parentContext.count,
);
}
context.add([CodePathSegment.newRoot(idGenerator.next())]);
return context;
}
/**
* Creates an empty fork context preceded by a given context.
* @param {ForkContext} parentContext The parent fork context.
* @param {boolean} shouldForkLeavingPath Indicates that we are inside of
* a `finally` block and should therefore fork the path that leaves
* `finally`.
* @returns {ForkContext} New fork context.
*/
static newEmpty(parentContext, shouldForkLeavingPath) {
return new ForkContext(
parentContext.idGenerator,
parentContext,
(shouldForkLeavingPath ? 2 : 1) * parentContext.count
);
}
}
module.exports = ForkContext;
+20 -19
View File
@@ -17,28 +17,29 @@
* A generator for unique ids.
*/
class IdGenerator {
/**
* @param {string} prefix Optional. A prefix of generated ids.
*/
constructor(prefix) {
this.prefix = String(prefix);
this.n = 0;
}
/**
* Generates id.
* @returns {string} A generated id.
*/
next() {
this.n = (1 + this.n) | 0;
/**
* @param {string} prefix Optional. A prefix of generated ids.
*/
constructor(prefix) {
this.prefix = String(prefix);
this.n = 0;
}
/* c8 ignore start */
if (this.n < 0) {
this.n = 1;
} /* c8 ignore stop */
/**
* Generates id.
* @returns {string} A generated id.
*/
next() {
this.n = 1 + this.n | 0;
return this.prefix + this.n;
}
/* c8 ignore start */
if (this.n < 0) {
this.n = 1;
}/* c8 ignore stop */
return this.prefix + this.n;
}
}
module.exports = IdGenerator;
+185
View File
@@ -0,0 +1,185 @@
/**
* @fileoverview Config Comment Parser
* @author Nicholas C. Zakas
*/
/* eslint class-methods-use-this: off -- Methods desired on instance */
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const levn = require("levn"),
{
Legacy: {
ConfigOps
}
} = require("@eslint/eslintrc/universal"),
{
directivesPattern
} = require("../shared/directives");
const debug = require("debug")("eslint:config-comment-parser");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../shared/types").LintMessage} LintMessage */
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Object to parse ESLint configuration comments inside JavaScript files.
* @name ConfigCommentParser
*/
module.exports = class ConfigCommentParser {
/**
* Parses a list of "name:string_value" or/and "name" options divided by comma or
* whitespace. Used for "global" and "exported" comments.
* @param {string} string The string to parse.
* @param {Comment} comment The comment node which has the string.
* @returns {Object} Result map object of names and string values, or null values if no value was provided
*/
parseStringConfig(string, comment) {
debug("Parsing String config");
const items = {};
// Collapse whitespace around `:` and `,` to make parsing easier
const trimmedString = string.replace(/\s*([:,])\s*/gu, "$1");
trimmedString.split(/\s|,+/u).forEach(name => {
if (!name) {
return;
}
// value defaults to null (if not provided), e.g: "foo" => ["foo", null]
const [key, value = null] = name.split(":");
items[key] = { value, comment };
});
return items;
}
/**
* Parses a JSON-like config.
* @param {string} string The string to parse.
* @param {Object} location Start line and column of comments for potential error message.
* @returns {({success: true, config: Object}|{success: false, error: LintMessage})} Result map object
*/
parseJsonConfig(string, location) {
debug("Parsing JSON config");
let items = {};
// Parses a JSON-like comment by the same way as parsing CLI option.
try {
items = levn.parse("Object", string) || {};
// Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
// Also, commaless notations have invalid severity:
// "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
// Should ignore that case as well.
if (ConfigOps.isEverySeverityValid(items)) {
return {
success: true,
config: items
};
}
} catch {
debug("Levn parsing failed; falling back to manual parsing.");
// ignore to parse the string by a fallback.
}
/*
* Optionator cannot parse commaless notations.
* But we are supporting that. So this is a fallback for that.
*/
items = {};
const normalizedString = string.replace(/([-a-zA-Z0-9/]+):/gu, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/u, "$1,");
try {
items = JSON.parse(`{${normalizedString}}`);
} catch (ex) {
debug("Manual parsing failed.");
return {
success: false,
error: {
ruleId: null,
fatal: true,
severity: 2,
message: `Failed to parse JSON from '${normalizedString}': ${ex.message}`,
line: location.start.line,
column: location.start.column + 1,
nodeType: null
}
};
}
return {
success: true,
config: items
};
}
/**
* Parses a config of values separated by comma.
* @param {string} string The string to parse.
* @returns {Object} Result map of values and true values
*/
parseListConfig(string) {
debug("Parsing list config");
const items = {};
string.split(",").forEach(name => {
const trimmedName = name.trim().replace(/^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/us, "$<ruleId>");
if (trimmedName) {
items[trimmedName] = true;
}
});
return items;
}
/**
* Extract the directive and the justification from a given directive comment and trim them.
* @param {string} value The comment text to extract.
* @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
*/
extractDirectiveComment(value) {
const match = /\s-{2,}\s/u.exec(value);
if (!match) {
return { directivePart: value.trim(), justificationPart: "" };
}
const directive = value.slice(0, match.index).trim();
const justification = value.slice(match.index + match[0].length).trim();
return { directivePart: directive, justificationPart: justification };
}
/**
* Parses a directive comment into directive text and value.
* @param {Comment} comment The comment node with the directive to be parsed.
* @returns {{directiveText: string, directiveValue: string}} The directive text and value.
*/
parseDirective(comment) {
const { directivePart } = this.extractDirectiveComment(comment.value);
const match = directivesPattern.exec(directivePart);
const directiveText = match[1];
const directiveValue = directivePart.slice(match.index + directiveText.length);
return { directiveText, directiveValue };
}
};
-332
View File
@@ -1,332 +0,0 @@
/**
* @fileoverview ESQuery wrapper for ESLint.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const esquery = require("esquery");
//-----------------------------------------------------------------------------
// Typedefs
//-----------------------------------------------------------------------------
/**
* @typedef {import("esquery").Selector} ESQuerySelector
* @typedef {import("esquery").ESQueryOptions} ESQueryOptions
*/
//------------------------------------------------------------------------------
// Classes
//------------------------------------------------------------------------------
/**
* The result of parsing and analyzing an ESQuery selector.
*/
class ESQueryParsedSelector {
/**
* The raw selector string that was parsed
* @type {string}
*/
source;
/**
* Whether this selector is an exit selector
* @type {boolean}
*/
isExit;
/**
* An object (from esquery) describing the matching behavior of the selector
* @type {ESQuerySelector}
*/
root;
/**
* The node types that could possibly trigger this selector, or `null` if all node types could trigger it
* @type {string[]|null}
*/
nodeTypes;
/**
* The number of class, pseudo-class, and attribute queries in this selector
* @type {number}
*/
attributeCount;
/**
* The number of identifier queries in this selector
* @type {number}
*/
identifierCount;
/**
* Creates a new parsed selector.
* @param {string} source The raw selector string that was parsed
* @param {boolean} isExit Whether this selector is an exit selector
* @param {ESQuerySelector} root An object (from esquery) describing the matching behavior of the selector
* @param {string[]|null} nodeTypes The node types that could possibly trigger this selector, or `null` if all node types could trigger it
* @param {number} attributeCount The number of class, pseudo-class, and attribute queries in this selector
* @param {number} identifierCount The number of identifier queries in this selector
*/
constructor(
source,
isExit,
root,
nodeTypes,
attributeCount,
identifierCount,
) {
this.source = source;
this.isExit = isExit;
this.root = root;
this.nodeTypes = nodeTypes;
this.attributeCount = attributeCount;
this.identifierCount = identifierCount;
}
/**
* Compares this selector's specificity to another selector for sorting purposes.
* @param {ESQueryParsedSelector} otherSelector The selector to compare against
* @returns {number}
* a value less than 0 if this selector is less specific than otherSelector
* a value greater than 0 if this selector is more specific than otherSelector
* a value less than 0 if this selector and otherSelector have the same specificity, and this selector <= otherSelector alphabetically
* a value greater than 0 if this selector and otherSelector have the same specificity, and this selector > otherSelector alphabetically
*/
compare(otherSelector) {
return (
this.attributeCount - otherSelector.attributeCount ||
this.identifierCount - otherSelector.identifierCount ||
(this.source <= otherSelector.source ? -1 : 1)
);
}
}
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const selectorCache = new Map();
/**
* Computes the union of one or more arrays
* @param {...any[]} arrays One or more arrays to union
* @returns {any[]} The union of the input arrays
*/
function union(...arrays) {
return [...new Set(arrays.flat())];
}
/**
* Computes the intersection of one or more arrays
* @param {...any[]} arrays One or more arrays to intersect
* @returns {any[]} The intersection of the input arrays
*/
function intersection(...arrays) {
if (arrays.length === 0) {
return [];
}
let result = [...new Set(arrays[0])];
for (const array of arrays.slice(1)) {
result = result.filter(x => array.includes(x));
}
return result;
}
/**
* Analyzes a parsed selector and returns combined data about it
* @param {ESQuerySelector} parsedSelector An object (from esquery) describing the matching behavior of the selector
* @returns {{nodeTypes:string[]|null, attributeCount:number, identifierCount:number}} Object containing selector data.
*/
function analyzeParsedSelector(parsedSelector) {
let attributeCount = 0;
let identifierCount = 0;
/**
* Analyzes a selector and returns the node types that could possibly trigger it.
* @param {ESQuerySelector} selector The selector to analyze.
* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
*/
function analyzeSelector(selector) {
switch (selector.type) {
case "identifier":
identifierCount++;
return [selector.value];
case "not":
selector.selectors.map(analyzeSelector);
return null;
case "matches": {
const typesForComponents =
selector.selectors.map(analyzeSelector);
if (typesForComponents.every(Boolean)) {
return union(...typesForComponents);
}
return null;
}
case "compound": {
const typesForComponents = selector.selectors
.map(analyzeSelector)
.filter(typesForComponent => typesForComponent);
// If all of the components could match any type, then the compound could also match any type.
if (!typesForComponents.length) {
return null;
}
/*
* If at least one of the components could only match a particular type, the compound could only match
* the intersection of those types.
*/
return intersection(...typesForComponents);
}
case "attribute":
case "field":
case "nth-child":
case "nth-last-child":
attributeCount++;
return null;
case "child":
case "descendant":
case "sibling":
case "adjacent":
analyzeSelector(selector.left);
return analyzeSelector(selector.right);
case "class":
// TODO: abstract into JSLanguage somehow
if (selector.name === "function") {
return [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
];
}
return null;
default:
return null;
}
}
const nodeTypes = analyzeSelector(parsedSelector);
return {
nodeTypes,
attributeCount,
identifierCount,
};
}
/**
* Tries to parse a simple selector string, such as a single identifier or wildcard.
* This saves time by avoiding the overhead of esquery parsing for simple cases.
* @param {string} selector The selector string to parse.
* @returns {Object|null} An object describing the selector if it is simple, or `null` if it is not.
*/
function trySimpleParseSelector(selector) {
if (selector === "*") {
return {
type: "wildcard",
value: "*",
};
}
if (/^[a-z]+$/iu.test(selector)) {
return {
type: "identifier",
value: selector,
};
}
return null;
}
/**
* Parses a raw selector string, and throws a useful error if parsing fails.
* @param {string} selector The selector string to parse.
* @returns {Object} An object (from esquery) describing the matching behavior of this selector
* @throws {Error} An error if the selector is invalid
*/
function tryParseSelector(selector) {
try {
return esquery.parse(selector);
} catch (err) {
if (
err.location &&
err.location.start &&
typeof err.location.start.offset === "number"
) {
throw new SyntaxError(
`Syntax error in selector "${selector}" at position ${err.location.start.offset}: ${err.message}`,
{
cause: err,
},
);
}
throw err;
}
}
/**
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
* @param {string} source A raw AST selector
* @returns {ESQueryParsedSelector} A selector descriptor
*/
function parse(source) {
if (selectorCache.has(source)) {
return selectorCache.get(source);
}
const cleanSource = source.replace(/:exit$/u, "");
const parsedSelector =
trySimpleParseSelector(cleanSource) ?? tryParseSelector(cleanSource);
const { nodeTypes, attributeCount, identifierCount } =
analyzeParsedSelector(parsedSelector);
const result = new ESQueryParsedSelector(
source,
source.endsWith(":exit"),
parsedSelector,
nodeTypes,
attributeCount,
identifierCount,
);
selectorCache.set(source, result);
return result;
}
/**
* Checks if a node matches a given selector.
* @param {Object} node The node to check against the selector.
* @param {ESQuerySelector} root The root of the selector to match against.
* @param {Object[]} ancestry The ancestry of the node being checked, which is an array of nodes from the current node to the root.
* @param {ESQueryOptions} options The options to use for matching.
* @returns {boolean} `true` if the node matches the selector, `false` otherwise.
*/
function matches(node, root, ancestry, options) {
return esquery.matches(node, root, ancestry, options);
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
module.exports = {
parse,
matches,
ESQueryParsedSelector,
};
-144
View File
@@ -1,144 +0,0 @@
/**
* @fileoverview The FileContext class.
* @author Nicholas C. Zakas
*/
"use strict";
/**
* Represents a file context that the linter can use to lint a file.
*/
class FileContext {
/**
* The current working directory.
* @type {string}
*/
cwd;
/**
* The filename of the file being linted.
* @type {string}
*/
filename;
/**
* The physical filename of the file being linted.
* @type {string}
*/
physicalFilename;
/**
* The source code of the file being linted.
* @type {SourceCode}
*/
sourceCode;
/**
* The parser options for the file being linted.
* @type {Record<string, unknown>}
* @deprecated Use `languageOptions` instead.
*/
parserOptions;
/**
* The path to the parser used to parse this file.
* @type {string}
* @deprecated No longer supported.
*/
parserPath;
/**
* The language options used when parsing this file.
* @type {Record<string, unknown>}
*/
languageOptions;
/**
* The settings for the file being linted.
* @type {Record<string, unknown>}
*/
settings;
/**
* Creates a new instance.
* @param {Object} config The configuration object for the file context.
* @param {string} config.cwd The current working directory.
* @param {string} config.filename The filename of the file being linted.
* @param {string} config.physicalFilename The physical filename of the file being linted.
* @param {SourceCode} config.sourceCode The source code of the file being linted.
* @param {Record<string, unknown>} config.parserOptions The parser options for the file being linted.
* @param {string} config.parserPath The path to the parser used to parse this file.
* @param {Record<string, unknown>} config.languageOptions The language options used when parsing this file.
* @param {Record<string, unknown>} config.settings The settings for the file being linted.
*/
constructor({
cwd,
filename,
physicalFilename,
sourceCode,
parserOptions,
parserPath,
languageOptions,
settings,
}) {
this.cwd = cwd;
this.filename = filename;
this.physicalFilename = physicalFilename;
this.sourceCode = sourceCode;
this.parserOptions = parserOptions;
this.parserPath = parserPath;
this.languageOptions = languageOptions;
this.settings = settings;
Object.freeze(this);
}
/**
* Gets the current working directory.
* @returns {string} The current working directory.
* @deprecated Use `cwd` instead.
*/
getCwd() {
return this.cwd;
}
/**
* Gets the filename of the file being linted.
* @returns {string} The filename of the file being linted.
* @deprecated Use `filename` instead.
*/
getFilename() {
return this.filename;
}
/**
* Gets the physical filename of the file being linted.
* @returns {string} The physical filename of the file being linted.
* @deprecated Use `physicalFilename` instead.
*/
getPhysicalFilename() {
return this.physicalFilename;
}
/**
* Gets the source code of the file being linted.
* @returns {SourceCode} The source code of the file being linted.
* @deprecated Use `sourceCode` instead.
*/
getSourceCode() {
return this.sourceCode;
}
/**
* Creates a new object with the current object as the prototype and
* the specified properties as its own properties.
* @param {Object} extension The properties to add to the new object.
* @returns {FileContext} A new object with the current object as the prototype
* and the specified properties as its own properties.
*/
extend(extension) {
return Object.freeze(Object.assign(Object.create(this), extension));
}
}
exports.FileContext = FileContext;
-608
View File
@@ -1,608 +0,0 @@
/**
* @fileoverview A class to track messages reported by the linter for a file.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("../shared/assert");
const { RuleFixer } = require("./rule-fixer");
const { interpolate } = require("./interpolate");
const ruleReplacements = require("../../conf/replacements.json");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
/** @typedef {import("../types").Linter.LintSuggestion} SuggestionResult */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
/**
* An error message description
* @typedef {Object} MessageDescriptor
* @property {ASTNode} [node] The reported node
* @property {Location} loc The location of the problem.
* @property {string} message The problem message.
* @property {Object} [data] Optional data to use to fill in placeholders in the
* message.
* @property {Function} [fix] The function to call that creates a fix command.
* @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes.
*/
/**
* @typedef {Object} LintProblem
* @property {string} ruleId The rule ID that reported the problem.
* @property {string} message The problem message.
* @property {SourceLocation} loc The location of the problem.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_ERROR_LOC = {
start: { line: 1, column: 0 },
end: { line: 1, column: 1 },
};
/**
* Updates a given location based on the language offsets. This allows us to
* change 0-based locations to 1-based locations. We always want ESLint
* reporting lines and columns starting from 1.
* @todo Potentially this should be moved into a shared utility file.
* @param {Object} location The location to update.
* @param {number} location.line The starting line number.
* @param {number} location.column The starting column number.
* @param {number} [location.endLine] The ending line number.
* @param {number} [location.endColumn] The ending column number.
* @param {Language} language The language to use to adjust the location information.
* @returns {Object} The updated location.
*/
function updateLocationInformation(
{ line, column, endLine, endColumn },
language,
) {
const columnOffset = language.columnStart === 1 ? 0 : 1;
const lineOffset = language.lineStart === 1 ? 0 : 1;
// calculate separately to account for undefined
const finalEndLine = endLine === void 0 ? endLine : endLine + lineOffset;
const finalEndColumn =
endColumn === void 0 ? endColumn : endColumn + columnOffset;
return {
line: line + lineOffset,
column: column + columnOffset,
endLine: finalEndLine,
endColumn: finalEndColumn,
};
}
/**
* creates a missing-rule message.
* @param {string} ruleId the ruleId to create
* @returns {string} created error message
* @private
*/
function createMissingRuleMessage(ruleId) {
return Object.hasOwn(ruleReplacements.rules, ruleId)
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}`
: `Definition for rule '${ruleId}' was not found.`;
}
/**
* creates a linting problem
* @param {LintProblem} options to create linting error
* @param {RuleSeverity} severity the error message to report
* @param {Language} language the language to use to adjust the location information.
* @returns {LintMessage} created problem, returns a missing-rule problem if only provided ruleId.
* @private
*/
function createLintingProblem(options, severity, language) {
const {
ruleId = null,
loc = DEFAULT_ERROR_LOC,
message = createMissingRuleMessage(options.ruleId),
} = options;
return {
ruleId,
message,
...updateLocationInformation(
{
line: loc.start.line,
column: loc.start.column,
endLine: loc.end.line,
endColumn: loc.end.column,
},
language,
),
severity,
nodeType: null,
};
}
/**
* Translates a multi-argument context.report() call into a single object argument call
* @param {...*} args A list of arguments passed to `context.report`
* @returns {MessageDescriptor} A normalized object containing report information
*/
function normalizeMultiArgReportCall(...args) {
// If there is one argument, it is considered to be a new-style call already.
if (args.length === 1) {
// Shallow clone the object to avoid surprises if reusing the descriptor
return Object.assign({}, args[0]);
}
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
if (typeof args[1] === "string") {
return {
node: args[0],
message: args[1],
data: args[2],
fix: args[3],
};
}
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
return {
node: args[0],
loc: args[1],
message: args[2],
data: args[3],
fix: args[4],
};
}
/**
* Asserts that either a loc or a node was provided, and the node is valid if it was provided.
* @param {MessageDescriptor} descriptor A descriptor to validate
* @returns {void}
* @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
*/
function assertValidNodeInfo(descriptor) {
if (descriptor.node) {
assert(typeof descriptor.node === "object", "Node must be an object");
} else {
assert(
descriptor.loc,
"Node must be provided when reporting error if location is not provided",
);
}
}
/**
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
*/
function normalizeReportLoc(descriptor) {
if (descriptor.loc.start) {
return descriptor.loc;
}
return { start: descriptor.loc, end: null };
}
/**
* Clones the given fix object.
* @param {Fix|null} fix The fix to clone.
* @returns {Fix|null} Deep cloned fix object or `null` if `null` or `undefined` was passed in.
*/
function cloneFix(fix) {
if (!fix) {
return null;
}
return {
range: [fix.range[0], fix.range[1]],
text: fix.text,
};
}
/**
* Check that a fix has a valid range.
* @param {Fix|null} fix The fix to validate.
* @returns {void}
*/
function assertValidFix(fix) {
if (fix) {
assert(
fix.range &&
typeof fix.range[0] === "number" &&
typeof fix.range[1] === "number",
`Fix has invalid range: ${JSON.stringify(fix, null, 2)}`,
);
}
}
/**
* Compares items in a fixes array by range.
* @param {Fix} a The first message.
* @param {Fix} b The second message.
* @returns {number} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @private
*/
function compareFixesByRange(a, b) {
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
}
/**
* Merges the given fixes array into one.
* @param {Fix[]} fixes The fixes to merge.
* @param {SourceCode} sourceCode The source code object to get the text between fixes.
* @returns {{text: string, range: number[]}} The merged fixes
*/
function mergeFixes(fixes, sourceCode) {
for (const fix of fixes) {
assertValidFix(fix);
}
if (fixes.length === 0) {
return null;
}
if (fixes.length === 1) {
return cloneFix(fixes[0]);
}
fixes.sort(compareFixesByRange);
const originalText = sourceCode.text;
const start = fixes[0].range[0];
const end = fixes.at(-1).range[1];
let text = "";
let lastPos = Number.MIN_SAFE_INTEGER;
for (const fix of fixes) {
assert(
fix.range[0] >= lastPos,
"Fix objects must not be overlapped in a report.",
);
if (fix.range[0] >= 0) {
text += originalText.slice(
Math.max(0, start, lastPos),
fix.range[0],
);
}
text += fix.text;
lastPos = fix.range[1];
}
text += originalText.slice(Math.max(0, start, lastPos), end);
return { range: [start, end], text };
}
/**
* Gets one fix object from the given descriptor.
* If the descriptor retrieves multiple fixes, this merges those to one.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {SourceCode} sourceCode The source code object to get text between fixes.
* @returns {({text: string, range: number[]}|null)} The fix for the descriptor
*/
function normalizeFixes(descriptor, sourceCode) {
if (typeof descriptor.fix !== "function") {
return null;
}
const ruleFixer = new RuleFixer({ sourceCode });
// @type {null | Fix | Fix[] | IterableIterator<Fix>}
const fix = descriptor.fix(ruleFixer);
// Merge to one.
if (fix && Symbol.iterator in fix) {
return mergeFixes(Array.from(fix), sourceCode);
}
assertValidFix(fix);
return cloneFix(fix);
}
/**
* Gets an array of suggestion objects from the given descriptor.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {SourceCode} sourceCode The source code object to get text between fixes.
* @param {Object} messages Object of meta messages for the rule.
* @returns {Array<SuggestionResult>} The suggestions for the descriptor
*/
function mapSuggestions(descriptor, sourceCode, messages) {
if (!descriptor.suggest || !Array.isArray(descriptor.suggest)) {
return [];
}
return (
descriptor.suggest
.map(suggestInfo => {
const computedDesc =
suggestInfo.desc || messages[suggestInfo.messageId];
return {
...suggestInfo,
desc: interpolate(computedDesc, suggestInfo.data),
fix: normalizeFixes(suggestInfo, sourceCode),
};
})
// Remove suggestions that didn't provide a fix
.filter(({ fix }) => fix)
);
}
/**
* Creates information about the report from a descriptor
* @param {Object} options Information about the problem
* @param {string} options.ruleId Rule ID
* @param {(0|1|2)} options.severity Rule severity
* @param {(ASTNode|null)} options.node Node
* @param {string} options.message Error message
* @param {string} [options.messageId] The error message ID.
* @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
* @param {{text: string, range: (number[]|null)}} options.fix The fix object
* @param {Array<{text: string, range: (number[]|null)}>} options.suggestions The array of suggestions objects
* @param {Language} [options.language] The language to use to adjust line and column offsets.
* @returns {LintMessage} Information about the report
*/
function createProblem(options) {
const { language } = options;
// calculate offsets based on the language in use
const columnOffset = language.columnStart === 1 ? 0 : 1;
const lineOffset = language.lineStart === 1 ? 0 : 1;
const problem = {
ruleId: options.ruleId,
severity: options.severity,
message: options.message,
line: options.loc.start.line + lineOffset,
column: options.loc.start.column + columnOffset,
nodeType: (options.node && options.node.type) || null,
};
/*
* If this isn’t in the conditional, some of the tests fail
* because `messageId` is present in the problem object
*/
if (options.messageId) {
problem.messageId = options.messageId;
}
if (options.loc.end) {
problem.endLine = options.loc.end.line + lineOffset;
problem.endColumn = options.loc.end.column + columnOffset;
}
if (options.fix) {
problem.fix = options.fix;
}
if (options.suggestions && options.suggestions.length > 0) {
problem.suggestions = options.suggestions;
}
return problem;
}
/**
* Validates that suggestions are properly defined. Throws if an error is detected.
* @param {Array<{ desc?: string, messageId?: string }>} suggest The incoming suggest data.
* @param {Object} messages Object of meta messages for the rule.
* @returns {void}
*/
function validateSuggestions(suggest, messages) {
if (suggest && Array.isArray(suggest)) {
suggest.forEach(suggestion => {
if (suggestion.messageId) {
const { messageId } = suggestion;
if (!messages) {
throw new TypeError(
`context.report() called with a suggest option with a messageId '${messageId}', but no messages were present in the rule metadata.`,
);
}
if (!messages[messageId]) {
throw new TypeError(
`context.report() called with a suggest option with a messageId '${messageId}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`,
);
}
if (suggestion.desc) {
throw new TypeError(
"context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one.",
);
}
} else if (!suggestion.desc) {
throw new TypeError(
"context.report() called with a suggest option that doesn't have either a `desc` or `messageId`",
);
}
if (typeof suggestion.fix !== "function") {
throw new TypeError(
`context.report() called with a suggest option without a fix function. See: ${JSON.stringify(suggestion, null, 2)}`,
);
}
});
}
}
/**
* Computes the message from a report descriptor.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {Object} messages Object of meta messages for the rule.
* @returns {string} The computed message.
* @throws {TypeError} If messageId is not found or both message and messageId are provided.
*/
function computeMessageFromDescriptor(descriptor, messages) {
if (descriptor.messageId) {
if (!messages) {
throw new TypeError(
"context.report() called with a messageId, but no messages were present in the rule metadata.",
);
}
const id = descriptor.messageId;
if (descriptor.message) {
throw new TypeError(
"context.report() called with a message and a messageId. Please only pass one.",
);
}
if (!messages || !Object.hasOwn(messages, id)) {
throw new TypeError(
`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`,
);
}
return messages[id];
}
if (descriptor.message) {
return descriptor.message;
}
throw new TypeError(
"Missing `message` property in report() call; add a message that describes the linting problem.",
);
}
/**
* A report object that contains the messages reported the linter
* for a file.
*/
class FileReport {
/**
* The messages reported by the linter for this file.
* @type {LintMessage[]}
*/
messages = [];
/**
* A rule mapper that maps rule IDs to their metadata.
* @type {(string) => RuleDefinition}
*/
#ruleMapper;
/**
* The source code object for the file.
* @type {SourceCode}
*/
#sourceCode;
/**
* The language to use to adjust line and column offsets.
* @type {Language}
*/
#language;
/**
* Whether to disable fixes for this report.
* @type {boolean}
*/
#disableFixes;
/**
* Creates a new FileReport instance.
* @param {Object} options The options for the file report
* @param {(string) => RuleDefinition} options.ruleMapper A rule mapper that maps rule IDs to their metadata.
* @param {SourceCode} options.sourceCode The source code object for the file.
* @param {Language} options.language The language to use to adjust line and column offsets.
* @param {boolean} [options.disableFixes=false] Whether to disable fixes for this report.
*/
constructor({ ruleMapper, sourceCode, language, disableFixes = false }) {
this.#ruleMapper = ruleMapper;
this.#sourceCode = sourceCode;
this.#language = language;
this.#disableFixes = disableFixes;
}
/**
* Adds a rule-generated message to the report.
* @param {string} ruleId The rule ID that reported the problem.
* @param {0|1|2} severity The severity of the problem (0 = off, 1 = warning, 2 = error).
* @param {...*} args The arguments passed to `context.report()`.
* @returns {LintMessage} The created message object.
* @throws {TypeError} If the messageId is not found or both message and messageId are provided.
* @throws {AssertionError} If the node is not an object or neither a node nor a loc is provided.
*/
addRuleMessage(ruleId, severity, ...args) {
const descriptor = normalizeMultiArgReportCall(...args);
const ruleDefinition = this.#ruleMapper(ruleId);
const messages = ruleDefinition?.meta?.messages;
assertValidNodeInfo(descriptor);
const computedMessage = computeMessageFromDescriptor(
descriptor,
messages,
);
validateSuggestions(descriptor.suggest, messages);
this.messages.push(
createProblem({
ruleId,
severity,
node: descriptor.node,
message: interpolate(computedMessage, descriptor.data),
messageId: descriptor.messageId,
loc: descriptor.loc
? normalizeReportLoc(descriptor)
: this.#sourceCode.getLoc(descriptor.node),
fix: this.#disableFixes
? null
: normalizeFixes(descriptor, this.#sourceCode),
suggestions: this.#disableFixes
? []
: mapSuggestions(descriptor, this.#sourceCode, messages),
language: this.#language,
}),
);
return this.messages.at(-1);
}
/**
* Adds an error message to the report. Meant to be called outside of rules.
* @param {LintProblem} descriptor The descriptor for the error message.
* @returns {LintMessage} The created message object.
*/
addError(descriptor) {
const message = createLintingProblem(descriptor, 2, this.#language);
this.messages.push(message);
return message;
}
/**
* Adds a fatal error message to the report. Meant to be called outside of rules.
* @param {LintProblem} descriptor The descriptor for the fatal error message.
* @returns {LintMessage} The created message object.
*/
addFatal(descriptor) {
const message = createLintingProblem(descriptor, 2, this.#language);
message.fatal = true;
this.messages.push(message);
return message;
}
/**
* Adds a warning message to the report. Meant to be called outside of rules.
* @param {LintProblem} descriptor The descriptor for the warning message.
* @returns {LintMessage} The created message object.
*/
addWarning(descriptor) {
const message = createLintingProblem(descriptor, 1, this.#language);
this.messages.push(message);
return message;
}
}
module.exports = {
FileReport,
updateLocationInformation,
};
+5 -3
View File
@@ -1,11 +1,13 @@
"use strict";
const { Linter } = require("./linter");
const interpolate = require("./interpolate");
const SourceCodeFixer = require("./source-code-fixer");
module.exports = {
Linter,
Linter,
// For testers.
SourceCodeFixer,
// For testers.
SourceCodeFixer,
interpolate
};
+13 -35
View File
@@ -9,42 +9,20 @@
// Public Interface
//------------------------------------------------------------------------------
/**
* Returns a global expression matching placeholders in messages.
* @returns {RegExp} Global regular expression matching placeholders
*/
function getPlaceholderMatcher() {
return /\{\{([^{}]+)\}\}/gu;
}
module.exports = (text, data) => {
if (!data) {
return text;
}
/**
* Replaces {{ placeholders }} in the message with the provided data.
* Does not replace placeholders not available in the data.
* @param {string} text Original message with potential placeholders
* @param {Record<string, string>} data Map of placeholder name to its value
* @returns {string} Message with replaced placeholders
*/
function interpolate(text, data) {
if (!data) {
return text;
}
// Substitution content for any {{ }} markers.
return text.replace(/\{\{([^{}]+?)\}\}/gu, (fullMatch, termWithWhitespace) => {
const term = termWithWhitespace.trim();
const matcher = getPlaceholderMatcher();
if (term in data) {
return data[term];
}
// Substitution content for any {{ }} markers.
return text.replace(matcher, (fullMatch, termWithWhitespace) => {
const term = termWithWhitespace.trim();
if (term in data) {
return data[term];
}
// Preserve old behavior: If parameter name not provided, don't replace it.
return fullMatch;
});
}
module.exports = {
getPlaceholderMatcher,
interpolate,
// Preserve old behavior: If parameter name not provided, don't replace it.
return fullMatch;
});
};
+1701 -2244
View File
File diff suppressed because it is too large. Load diff
+354
View File
@@ -0,0 +1,354 @@
/**
* @fileoverview The event generator for AST nodes.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const esquery = require("esquery");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* An object describing an AST selector
* @typedef {Object} ASTSelector
* @property {string} rawSelector The string that was parsed into this selector
* @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
* @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
* @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
* or `null` if all node types could cause a match
* @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
* @property {number} identifierCount The total number of identifier queries in this selector
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Computes the union of one or more arrays
* @param {...any[]} arrays One or more arrays to union
* @returns {any[]} The union of the input arrays
*/
function union(...arrays) {
return [...new Set(arrays.flat())];
}
/**
* Computes the intersection of one or more arrays
* @param {...any[]} arrays One or more arrays to intersect
* @returns {any[]} The intersection of the input arrays
*/
function intersection(...arrays) {
if (arrays.length === 0) {
return [];
}
let result = [...new Set(arrays[0])];
for (const array of arrays.slice(1)) {
result = result.filter(x => array.includes(x));
}
return result;
}
/**
* Gets the possible types of a selector
* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
*/
function getPossibleTypes(parsedSelector) {
switch (parsedSelector.type) {
case "identifier":
return [parsedSelector.value];
case "matches": {
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
if (typesForComponents.every(Boolean)) {
return union(...typesForComponents);
}
return null;
}
case "compound": {
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes).filter(typesForComponent => typesForComponent);
// If all of the components could match any type, then the compound could also match any type.
if (!typesForComponents.length) {
return null;
}
/*
* If at least one of the components could only match a particular type, the compound could only match
* the intersection of those types.
*/
return intersection(...typesForComponents);
}
case "child":
case "descendant":
case "sibling":
case "adjacent":
return getPossibleTypes(parsedSelector.right);
case "class":
if (parsedSelector.name === "function") {
return ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"];
}
return null;
default:
return null;
}
}
/**
* Counts the number of class, pseudo-class, and attribute queries in this selector
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
* @returns {number} The number of class, pseudo-class, and attribute queries in this selector
*/
function countClassAttributes(parsedSelector) {
switch (parsedSelector.type) {
case "child":
case "descendant":
case "sibling":
case "adjacent":
return countClassAttributes(parsedSelector.left) + countClassAttributes(parsedSelector.right);
case "compound":
case "not":
case "matches":
return parsedSelector.selectors.reduce((sum, childSelector) => sum + countClassAttributes(childSelector), 0);
case "attribute":
case "field":
case "nth-child":
case "nth-last-child":
return 1;
default:
return 0;
}
}
/**
* Counts the number of identifier queries in this selector
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
* @returns {number} The number of identifier queries
*/
function countIdentifiers(parsedSelector) {
switch (parsedSelector.type) {
case "child":
case "descendant":
case "sibling":
case "adjacent":
return countIdentifiers(parsedSelector.left) + countIdentifiers(parsedSelector.right);
case "compound":
case "not":
case "matches":
return parsedSelector.selectors.reduce((sum, childSelector) => sum + countIdentifiers(childSelector), 0);
case "identifier":
return 1;
default:
return 0;
}
}
/**
* Compares the specificity of two selector objects, with CSS-like rules.
* @param {ASTSelector} selectorA An AST selector descriptor
* @param {ASTSelector} selectorB Another AST selector descriptor
* @returns {number}
* a value less than 0 if selectorA is less specific than selectorB
* a value greater than 0 if selectorA is more specific than selectorB
* a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
* a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
*/
function compareSpecificity(selectorA, selectorB) {
return selectorA.attributeCount - selectorB.attributeCount ||
selectorA.identifierCount - selectorB.identifierCount ||
(selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1);
}
/**
* Parses a raw selector string, and throws a useful error if parsing fails.
* @param {string} rawSelector A raw AST selector
* @returns {Object} An object (from esquery) describing the matching behavior of this selector
* @throws {Error} An error if the selector is invalid
*/
function tryParseSelector(rawSelector) {
try {
return esquery.parse(rawSelector.replace(/:exit$/u, ""));
} catch (err) {
if (err.location && err.location.start && typeof err.location.start.offset === "number") {
throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`);
}
throw err;
}
}
const selectorCache = new Map();
/**
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
* @param {string} rawSelector A raw AST selector
* @returns {ASTSelector} A selector descriptor
*/
function parseSelector(rawSelector) {
if (selectorCache.has(rawSelector)) {
return selectorCache.get(rawSelector);
}
const parsedSelector = tryParseSelector(rawSelector);
const result = {
rawSelector,
isExit: rawSelector.endsWith(":exit"),
parsedSelector,
listenerTypes: getPossibleTypes(parsedSelector),
attributeCount: countClassAttributes(parsedSelector),
identifierCount: countIdentifiers(parsedSelector)
};
selectorCache.set(rawSelector, result);
return result;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The event generator for AST nodes.
* This implements below interface.
*
* ```ts
* interface EventGenerator {
* emitter: SafeEmitter;
* enterNode(node: ASTNode): void;
* leaveNode(node: ASTNode): void;
* }
* ```
*/
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/linter/safe-emitter.js for more details on `SafeEmitter`.)
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
* @returns {NodeEventGenerator} new instance
*/
constructor(emitter, esqueryOptions) {
this.emitter = emitter;
this.esqueryOptions = esqueryOptions;
this.currentAncestry = [];
this.enterSelectorsByNodeType = new Map();
this.exitSelectorsByNodeType = new Map();
this.anyTypeEnterSelectors = [];
this.anyTypeExitSelectors = [];
emitter.eventNames().forEach(rawSelector => {
const selector = parseSelector(rawSelector);
if (selector.listenerTypes) {
const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
selector.listenerTypes.forEach(nodeType => {
if (!typeMap.has(nodeType)) {
typeMap.set(nodeType, []);
}
typeMap.get(nodeType).push(selector);
});
return;
}
const selectors = selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
selectors.push(selector);
});
this.anyTypeEnterSelectors.sort(compareSpecificity);
this.anyTypeExitSelectors.sort(compareSpecificity);
this.enterSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
this.exitSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
}
/**
* Checks a selector against a node, and emits it if it matches
* @param {ASTNode} node The node to check
* @param {ASTSelector} selector An AST selector descriptor
* @returns {void}
*/
applySelector(node, selector) {
if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
this.emitter.emit(selector.rawSelector, node);
}
}
/**
* Applies all appropriate selectors to a node, in specificity order
* @param {ASTNode} node The node to check
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
* @returns {void}
*/
applySelectors(node, isExit) {
const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
const anyTypeSelectors = isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
/*
* selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
* Iterate through each of them, applying selectors in the right order.
*/
let selectorsByTypeIndex = 0;
let anyTypeSelectorsIndex = 0;
while (selectorsByTypeIndex < selectorsByNodeType.length || anyTypeSelectorsIndex < anyTypeSelectors.length) {
if (
selectorsByTypeIndex >= selectorsByNodeType.length ||
anyTypeSelectorsIndex < anyTypeSelectors.length &&
compareSpecificity(anyTypeSelectors[anyTypeSelectorsIndex], selectorsByNodeType[selectorsByTypeIndex]) < 0
) {
this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
} else {
this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
}
}
}
/**
* Emits an event of entering AST node.
* @param {ASTNode} node A node which was entered.
* @returns {void}
*/
enterNode(node) {
if (node.parent) {
this.currentAncestry.unshift(node.parent);
}
this.applySelectors(node, false);
}
/**
* Emits an event of leaving AST node.
* @param {ASTNode} node A node which was left.
* @returns {void}
*/
leaveNode(node) {
this.applySelectors(node, true);
this.currentAncestry.shift();
}
}
module.exports = NodeEventGenerator;
+369
View File
@@ -0,0 +1,369 @@
/**
* @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert");
const ruleFixer = require("./rule-fixer");
const interpolate = require("./interpolate");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/**
* An error message description
* @typedef {Object} MessageDescriptor
* @property {ASTNode} [node] The reported node
* @property {Location} loc The location of the problem.
* @property {string} message The problem message.
* @property {Object} [data] Optional data to use to fill in placeholders in the
* message.
* @property {Function} [fix] The function to call that creates a fix command.
* @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes.
*/
//------------------------------------------------------------------------------
// Module Definition
//------------------------------------------------------------------------------
/**
* Translates a multi-argument context.report() call into a single object argument call
* @param {...*} args A list of arguments passed to `context.report`
* @returns {MessageDescriptor} A normalized object containing report information
*/
function normalizeMultiArgReportCall(...args) {
// If there is one argument, it is considered to be a new-style call already.
if (args.length === 1) {
// Shallow clone the object to avoid surprises if reusing the descriptor
return Object.assign({}, args[0]);
}
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
if (typeof args[1] === "string") {
return {
node: args[0],
message: args[1],
data: args[2],
fix: args[3]
};
}
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
return {
node: args[0],
loc: args[1],
message: args[2],
data: args[3],
fix: args[4]
};
}
/**
* Asserts that either a loc or a node was provided, and the node is valid if it was provided.
* @param {MessageDescriptor} descriptor A descriptor to validate
* @returns {void}
* @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
*/
function assertValidNodeInfo(descriptor) {
if (descriptor.node) {
assert(typeof descriptor.node === "object", "Node must be an object");
} else {
assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
}
}
/**
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
*/
function normalizeReportLoc(descriptor) {
if (descriptor.loc) {
if (descriptor.loc.start) {
return descriptor.loc;
}
return { start: descriptor.loc, end: null };
}
return descriptor.node.loc;
}
/**
* Clones the given fix object.
* @param {Fix|null} fix The fix to clone.
* @returns {Fix|null} Deep cloned fix object or `null` if `null` or `undefined` was passed in.
*/
function cloneFix(fix) {
if (!fix) {
return null;
}
return {
range: [fix.range[0], fix.range[1]],
text: fix.text
};
}
/**
* Check that a fix has a valid range.
* @param {Fix|null} fix The fix to validate.
* @returns {void}
*/
function assertValidFix(fix) {
if (fix) {
assert(fix.range && typeof fix.range[0] === "number" && typeof fix.range[1] === "number", `Fix has invalid range: ${JSON.stringify(fix, null, 2)}`);
}
}
/**
* Compares items in a fixes array by range.
* @param {Fix} a The first message.
* @param {Fix} b The second message.
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @private
*/
function compareFixesByRange(a, b) {
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
}
/**
* Merges the given fixes array into one.
* @param {Fix[]} fixes The fixes to merge.
* @param {SourceCode} sourceCode The source code object to get the text between fixes.
* @returns {{text: string, range: number[]}} The merged fixes
*/
function mergeFixes(fixes, sourceCode) {
for (const fix of fixes) {
assertValidFix(fix);
}
if (fixes.length === 0) {
return null;
}
if (fixes.length === 1) {
return cloneFix(fixes[0]);
}
fixes.sort(compareFixesByRange);
const originalText = sourceCode.text;
const start = fixes[0].range[0];
const end = fixes[fixes.length - 1].range[1];
let text = "";
let lastPos = Number.MIN_SAFE_INTEGER;
for (const fix of fixes) {
assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
if (fix.range[0] >= 0) {
text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
}
text += fix.text;
lastPos = fix.range[1];
}
text += originalText.slice(Math.max(0, start, lastPos), end);
return { range: [start, end], text };
}
/**
* Gets one fix object from the given descriptor.
* If the descriptor retrieves multiple fixes, this merges those to one.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {SourceCode} sourceCode The source code object to get text between fixes.
* @returns {({text: string, range: number[]}|null)} The fix for the descriptor
*/
function normalizeFixes(descriptor, sourceCode) {
if (typeof descriptor.fix !== "function") {
return null;
}
// @type {null | Fix | Fix[] | IterableIterator<Fix>}
const fix = descriptor.fix(ruleFixer);
// Merge to one.
if (fix && Symbol.iterator in fix) {
return mergeFixes(Array.from(fix), sourceCode);
}
assertValidFix(fix);
return cloneFix(fix);
}
/**
* Gets an array of suggestion objects from the given descriptor.
* @param {MessageDescriptor} descriptor The report descriptor.
* @param {SourceCode} sourceCode The source code object to get text between fixes.
* @param {Object} messages Object of meta messages for the rule.
* @returns {Array<SuggestionResult>} The suggestions for the descriptor
*/
function mapSuggestions(descriptor, sourceCode, messages) {
if (!descriptor.suggest || !Array.isArray(descriptor.suggest)) {
return [];
}
return descriptor.suggest
.map(suggestInfo => {
const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId];
return {
...suggestInfo,
desc: interpolate(computedDesc, suggestInfo.data),
fix: normalizeFixes(suggestInfo, sourceCode)
};
})
// Remove suggestions that didn't provide a fix
.filter(({ fix }) => fix);
}
/**
* Creates information about the report from a descriptor
* @param {Object} options Information about the problem
* @param {string} options.ruleId Rule ID
* @param {(0|1|2)} options.severity Rule severity
* @param {(ASTNode|null)} options.node Node
* @param {string} options.message Error message
* @param {string} [options.messageId] The error message ID.
* @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
* @param {{text: string, range: (number[]|null)}} options.fix The fix object
* @param {Array<{text: string, range: (number[]|null)}>} options.suggestions The array of suggestions objects
* @returns {LintMessage} Information about the report
*/
function createProblem(options) {
const problem = {
ruleId: options.ruleId,
severity: options.severity,
message: options.message,
line: options.loc.start.line,
column: options.loc.start.column + 1,
nodeType: options.node && options.node.type || null
};
/*
* If this isn’t in the conditional, some of the tests fail
* because `messageId` is present in the problem object
*/
if (options.messageId) {
problem.messageId = options.messageId;
}
if (options.loc.end) {
problem.endLine = options.loc.end.line;
problem.endColumn = options.loc.end.column + 1;
}
if (options.fix) {
problem.fix = options.fix;
}
if (options.suggestions && options.suggestions.length > 0) {
problem.suggestions = options.suggestions;
}
return problem;
}
/**
* Validates that suggestions are properly defined. Throws if an error is detected.
* @param {Array<{ desc?: string, messageId?: string }>} suggest The incoming suggest data.
* @param {Object} messages Object of meta messages for the rule.
* @returns {void}
*/
function validateSuggestions(suggest, messages) {
if (suggest && Array.isArray(suggest)) {
suggest.forEach(suggestion => {
if (suggestion.messageId) {
const { messageId } = suggestion;
if (!messages) {
throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}', but no messages were present in the rule metadata.`);
}
if (!messages[messageId]) {
throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
}
if (suggestion.desc) {
throw new TypeError("context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one.");
}
} else if (!suggestion.desc) {
throw new TypeError("context.report() called with a suggest option that doesn't have either a `desc` or `messageId`");
}
if (typeof suggestion.fix !== "function") {
throw new TypeError(`context.report() called with a suggest option without a fix function. See: ${suggestion}`);
}
});
}
}
/**
* 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, disableFixes: boolean}} metadata Metadata for the reported problem
* @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
* @returns {function(...args): LintMessage} Function that returns information about the report
*/
module.exports = function createReportTranslator(metadata) {
/*
* `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
* The report translator itself (i.e. the function that `createReportTranslator` returns) gets
* called every time a rule reports a problem, which happens much less frequently (usually, the vast
* majority of rules don't report any problems for a given file).
*/
return (...args) => {
const descriptor = normalizeMultiArgReportCall(...args);
const messages = metadata.messageIds;
assertValidNodeInfo(descriptor);
let computedMessage;
if (descriptor.messageId) {
if (!messages) {
throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
}
const id = descriptor.messageId;
if (descriptor.message) {
throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
}
if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
}
computedMessage = messages[id];
} else if (descriptor.message) {
computedMessage = descriptor.message;
} else {
throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
}
validateSuggestions(descriptor.suggest, messages);
return createProblem({
ruleId: metadata.ruleId,
severity: metadata.severity,
node: descriptor.node,
message: interpolate(computedMessage, descriptor.data),
messageId: descriptor.messageId,
loc: normalizeReportLoc(descriptor),
fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode),
suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages)
});
};
};
+96 -125
View File
@@ -4,16 +4,6 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* @import { SourceRange } from "@eslint/core";
*/
/* eslint class-methods-use-this: off -- Methods desired on instance */
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
@@ -26,16 +16,16 @@
/**
* Creates a fix command that inserts text at the specified index in the source text.
* @param {number} index The 0-based index at which to insert the new text.
* @param {int} index The 0-based index at which to insert the new text.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
* @private
*/
function insertTextAt(index, text) {
return {
range: [index, index],
text,
};
return {
range: [index, index],
text
};
}
//------------------------------------------------------------------------------
@@ -45,125 +35,106 @@ function insertTextAt(index, text) {
/**
* Creates code fixing commands for rules.
*/
class RuleFixer {
/**
* The source code object representing the text to be fixed.
* @type {SourceCode}
*/
#sourceCode;
/**
* Creates a new instance.
* @param {Object} options The options for the fixer.
* @param {SourceCode} options.sourceCode The source code object representing the text to be fixed.
*/
constructor({ sourceCode }) {
this.#sourceCode = sourceCode;
}
const ruleFixer = Object.freeze({
/**
* Creates a fix command that inserts text after the given node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to insert after.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextAfter(nodeOrToken, text) {
const range = this.#sourceCode.getRange(nodeOrToken);
/**
* Creates a fix command that inserts text after the given node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to insert after.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextAfter(nodeOrToken, text) {
return this.insertTextAfterRange(nodeOrToken.range, text);
},
return this.insertTextAfterRange(range, text);
}
/**
* Creates a fix command that inserts text after the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {int[]} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextAfterRange(range, text) {
return insertTextAt(range[1], text);
},
/**
* Creates a fix command that inserts text after the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {SourceRange} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextAfterRange(range, text) {
return insertTextAt(range[1], text);
}
/**
* Creates a fix command that inserts text before the given node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to insert before.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextBefore(nodeOrToken, text) {
return this.insertTextBeforeRange(nodeOrToken.range, text);
},
/**
* Creates a fix command that inserts text before the given node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to insert before.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextBefore(nodeOrToken, text) {
const range = this.#sourceCode.getRange(nodeOrToken);
/**
* Creates a fix command that inserts text before the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {int[]} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextBeforeRange(range, text) {
return insertTextAt(range[0], text);
},
return this.insertTextBeforeRange(range, text);
}
/**
* Creates a fix command that replaces text at the node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
replaceText(nodeOrToken, text) {
return this.replaceTextRange(nodeOrToken.range, text);
},
/**
* Creates a fix command that inserts text before the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {SourceRange} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
insertTextBeforeRange(range, text) {
return insertTextAt(range[0], text);
}
/**
* Creates a fix command that replaces text at the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {int[]} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
replaceTextRange(range, text) {
return {
range,
text
};
},
/**
* Creates a fix command that replaces text at the node or token.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
replaceText(nodeOrToken, text) {
const range = this.#sourceCode.getRange(nodeOrToken);
/**
* Creates a fix command that removes the node or token from the source.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
* @returns {Object} The fix command.
*/
remove(nodeOrToken) {
return this.removeRange(nodeOrToken.range);
},
return this.replaceTextRange(range, text);
}
/**
* Creates a fix command that removes the specified range of text from the source.
* The fix is not applied until applyFixes() is called.
* @param {int[]} range The range to remove, first item is start of range, second
* is end of range.
* @returns {Object} The fix command.
*/
removeRange(range) {
return {
range,
text: ""
};
}
/**
* Creates a fix command that replaces text at the specified range in the source text.
* The fix is not applied until applyFixes() is called.
* @param {SourceRange} range The range to replace, first item is start of range, second
* is end of range.
* @param {string} text The text to insert.
* @returns {Object} The fix command.
*/
replaceTextRange(range, text) {
return {
range,
text,
};
}
});
/**
* Creates a fix command that removes the node or token from the source.
* The fix is not applied until applyFixes() is called.
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
* @returns {Object} The fix command.
*/
remove(nodeOrToken) {
const range = this.#sourceCode.getRange(nodeOrToken);
return this.removeRange(range);
}
/**
* Creates a fix command that removes the specified range of text from the source.
* The fix is not applied until applyFixes() is called.
* @param {SourceRange} range The range to remove, first item is start of range, second
* is end of range.
* @returns {Object} The fix command.
*/
removeRange(range) {
return {
range,
text: "",
};
}
}
module.exports = { RuleFixer };
module.exports = ruleFixer;
+46 -37
View File
@@ -13,10 +13,18 @@
const builtInRules = require("../rules");
//------------------------------------------------------------------------------
// Typedefs
// Helpers
//------------------------------------------------------------------------------
/** @typedef {import("../types").Rule.RuleModule} Rule */
/**
* 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
@@ -26,46 +34,47 @@ const builtInRules = require("../rules");
* A storage for rules.
*/
class Rules {
constructor() {
this._rules = Object.create(null);
}
constructor() {
this._rules = Object.create(null);
}
/**
* Registers a rule module for rule id in storage.
* @param {string} ruleId Rule id (file name).
* @param {Rule} rule Rule object.
* @returns {void}
*/
define(ruleId, rule) {
this._rules[ruleId] = rule;
}
/**
* 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 {Rule} Rule object.
*/
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);
}
/**
* 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;
}
return null;
}
*[Symbol.iterator]() {
yield* builtInRules;
*[Symbol.iterator]() {
yield* builtInRules;
for (const ruleId of Object.keys(this._rules)) {
yield [ruleId, this.get(ruleId)];
}
}
for (const ruleId of Object.keys(this._rules)) {
yield [ruleId, this.get(ruleId)];
}
}
}
module.exports = Rules;
+52
View File
@@ -0,0 +1,52 @@
/**
* @fileoverview A variant of EventEmitter which does not give listeners information about each other
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* An event emitter
* @typedef {Object} SafeEmitter
* @property {(eventName: string, listenerFunc: Function) => void} on Adds a listener for a given event name
* @property {(eventName: string, arg1?: any, arg2?: any, arg3?: any) => void} emit Emits an event with a given name.
* This calls all the listeners that were listening for that name, with `arg1`, `arg2`, and `arg3` as arguments.
* @property {function(): string[]} eventNames Gets the list of event names that have registered listeners.
*/
/**
* Creates an object which can listen for and emit events.
* This is similar to the EventEmitter API in Node's standard library, but it has a few differences.
* The goal is to allow multiple modules to attach arbitrary listeners to the same emitter, without
* letting the modules know about each other at all.
* 1. It has no special keys like `error` and `newListener`, which would allow modules to detect when
* another module throws an error or registers a listener.
* 2. It calls listener functions without any `this` value. (`EventEmitter` calls listeners with a
* `this` value of the emitter instance, which would give listeners access to other listeners.)
* @returns {SafeEmitter} An emitter
*/
module.exports = () => {
const listeners = Object.create(null);
return Object.freeze({
on(eventName, listener) {
if (eventName in listeners) {
listeners[eventName].push(listener);
} else {
listeners[eventName] = [listener];
}
},
emit(eventName, ...args) {
if (eventName in listeners) {
listeners[eventName].forEach(listener => listener(...args));
}
},
eventNames() {
return Object.keys(listeners);
}
});
};
+82 -84
View File
@@ -20,22 +20,22 @@ const BOM = "\uFEFF";
* Compares items in a messages array by range.
* @param {Message} a The first message.
* @param {Message} b The second message.
* @returns {number} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @private
*/
function compareMessagesByFixRange(a, b) {
return a.fix.range[0] - b.fix.range[0] || a.fix.range[1] - b.fix.range[1];
return a.fix.range[0] - b.fix.range[0] || a.fix.range[1] - b.fix.range[1];
}
/**
* Compares items in a messages array by line and column.
* @param {Message} a The first message.
* @param {Message} b The second message.
* @returns {number} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
* @private
*/
function compareMessagesByLocation(a, b) {
return a.line - b.line || a.column - b.column;
return a.line - b.line || a.column - b.column;
}
//------------------------------------------------------------------------------
@@ -47,7 +47,7 @@ function compareMessagesByLocation(a, b) {
* @constructor
*/
function SourceCodeFixer() {
Object.freeze(this);
Object.freeze(this);
}
/**
@@ -58,97 +58,95 @@ function SourceCodeFixer() {
* @param {boolean|Function} [shouldFix=true] Determines whether each message should be fixed
* @returns {Object} An object containing the fixed text and any unfixed messages.
*/
SourceCodeFixer.applyFixes = function (sourceText, messages, shouldFix) {
debug("Applying fixes");
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
debug("Applying fixes");
if (shouldFix === false) {
debug("shouldFix parameter was false, not attempting fixes");
return {
fixed: false,
messages,
output: sourceText,
};
}
if (shouldFix === false) {
debug("shouldFix parameter was false, not attempting fixes");
return {
fixed: false,
messages,
output: sourceText
};
}
// clone the array
const remainingMessages = [],
fixes = [],
bom = sourceText.startsWith(BOM) ? BOM : "",
text = bom ? sourceText.slice(1) : sourceText;
let lastPos = Number.NEGATIVE_INFINITY,
output = bom;
// clone the array
const remainingMessages = [],
fixes = [],
bom = sourceText.startsWith(BOM) ? BOM : "",
text = bom ? sourceText.slice(1) : sourceText;
let lastPos = Number.NEGATIVE_INFINITY,
output = bom;
/**
* Try to use the 'fix' from a problem.
* @param {Message} problem The message object to apply fixes from
* @returns {boolean} Whether fix was successfully applied
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0];
const end = fix.range[1];
/**
* Try to use the 'fix' from a problem.
* @param {Message} problem The message object to apply fixes from
* @returns {boolean} Whether fix was successfully applied
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0];
const end = fix.range[1];
// Remain it as a problem if it's overlapped or it's a negative range
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// Remain it as a problem if it's overlapped or it's a negative range
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// Remove BOM.
if (
(start < 0 && end >= 0) ||
(start === 0 && fix.text.startsWith(BOM))
) {
output = "";
}
// Remove BOM.
if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
output = "";
}
// Make output to this fix.
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
output += fix.text;
lastPos = end;
return true;
}
// Make output to this fix.
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
output += fix.text;
lastPos = end;
return true;
}
messages.forEach(problem => {
if (Object.hasOwn(problem, "fix") && problem.fix) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
messages.forEach(problem => {
if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
if (fixes.length) {
debug("Found fixes to apply");
let fixesWereApplied = false;
if (fixes.length) {
debug("Found fixes to apply");
let fixesWereApplied = false;
for (const problem of fixes.sort(compareMessagesByFixRange)) {
if (typeof shouldFix !== "function" || shouldFix(problem)) {
attemptFix(problem);
for (const problem of fixes.sort(compareMessagesByFixRange)) {
if (typeof shouldFix !== "function" || shouldFix(problem)) {
attemptFix(problem);
/*
* The only time attemptFix will fail is if a previous fix was
* applied which conflicts with it. So we can mark this as true.
*/
fixesWereApplied = true;
} else {
remainingMessages.push(problem);
}
}
output += text.slice(Math.max(0, lastPos));
/*
* The only time attemptFix will fail is if a previous fix was
* applied which conflicts with it. So we can mark this as true.
*/
fixesWereApplied = true;
} else {
remainingMessages.push(problem);
}
}
output += text.slice(Math.max(0, lastPos));
return {
fixed: fixesWereApplied,
messages: remainingMessages.sort(compareMessagesByLocation),
output,
};
}
return {
fixed: fixesWereApplied,
messages: remainingMessages.sort(compareMessagesByLocation),
output
};
}
debug("No fixes to apply");
return {
fixed: false,
messages,
output: bom + text
};
debug("No fixes to apply");
return {
fixed: false,
messages,
output: bom + text,
};
};
module.exports = SourceCodeFixer;
-333
View File
@@ -1,333 +0,0 @@
/**
* @fileoverview Traverser for SourceCode objects.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { parse, matches } = require("./esquery");
const vk = require("eslint-visitor-keys");
//-----------------------------------------------------------------------------
// Typedefs
//-----------------------------------------------------------------------------
/**
* @import { Language, SourceCode } from "@eslint/core";
* @import { ESQueryOptions } from "esquery";
* @import { ESQueryParsedSelector } from "./esquery.js";
* @import { SourceCodeVisitor } from "./source-code-visitor.js";
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const STEP_KIND_VISIT = 1;
const STEP_KIND_CALL = 2;
/**
* Compares two ESQuery selectors by specificity.
* @param {ESQueryParsedSelector} a The first selector to compare.
* @param {ESQueryParsedSelector} b The second selector to compare.
* @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
*/
function compareSpecificity(a, b) {
return a.compare(b);
}
/**
* Helper to wrap ESQuery operations.
*/
class ESQueryHelper {
/**
* Creates a new instance.
* @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
*/
constructor(visitor, esqueryOptions) {
/**
* The visitor to use during traversal.
* @type {SourceCodeVisitor}
*/
this.visitor = visitor;
/**
* The options for `esquery` to use during matching.
* @type {ESQueryOptions}
*/
this.esqueryOptions = esqueryOptions;
/**
* A map of node type to selectors targeting that node type on the
* enter phase of traversal.
* @type {Map<string, ESQueryParsedSelector[]>}
*/
this.enterSelectorsByNodeType = new Map();
/**
* A map of node type to selectors targeting that node type on the
* exit phase of traversal.
* @type {Map<string, ESQueryParsedSelector[]>}
*/
this.exitSelectorsByNodeType = new Map();
/**
* An array of selectors that match any node type on the
* enter phase of traversal.
* @type {ESQueryParsedSelector[]}
*/
this.anyTypeEnterSelectors = [];
/**
* An array of selectors that match any node type on the
* exit phase of traversal.
* @type {ESQueryParsedSelector[]}
*/
this.anyTypeExitSelectors = [];
visitor.forEachName(rawSelector => {
const selector = parse(rawSelector);
/*
* If this selector has identified specific node types,
* add it to the map for these node types for faster lookup.
*/
if (selector.nodeTypes) {
const typeMap = selector.isExit
? this.exitSelectorsByNodeType
: this.enterSelectorsByNodeType;
selector.nodeTypes.forEach(nodeType => {
if (!typeMap.has(nodeType)) {
typeMap.set(nodeType, []);
}
typeMap.get(nodeType).push(selector);
});
return;
}
/*
* Remaining selectors are added to the "any type" selectors
* list for the appropriate phase of traversal. This ensures
* that all selectors will still be applied even if no
* specific node type is matched.
*/
const selectors = selector.isExit
? this.anyTypeExitSelectors
: this.anyTypeEnterSelectors;
selectors.push(selector);
});
// sort all selectors by specificity for prioritizing call order
this.anyTypeEnterSelectors.sort(compareSpecificity);
this.anyTypeExitSelectors.sort(compareSpecificity);
this.enterSelectorsByNodeType.forEach(selectorList =>
selectorList.sort(compareSpecificity),
);
this.exitSelectorsByNodeType.forEach(selectorList =>
selectorList.sort(compareSpecificity),
);
}
/**
* Checks if a node matches a given selector.
* @param {ASTNode} node The node to check
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
* @param {ESQueryParsedSelector} selector An AST selector descriptor
* @returns {boolean} `true` if the selector matches the node, `false` otherwise
*/
matches(node, ancestry, selector) {
return matches(node, selector.root, ancestry, this.esqueryOptions);
}
/**
* Calculates all appropriate selectors to a node, in specificity order
* @param {ASTNode} node The node to check
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
* @returns {string[]} An array of selectors that match the node.
*/
calculateSelectors(node, ancestry, isExit) {
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
const selectors = [];
/*
* Get the selectors that may match this node. First, check
* to see if the node type has specific selectors,
* then gather the "any type" selectors.
*/
const selectorsByNodeType =
(isExit
? this.exitSelectorsByNodeType
: this.enterSelectorsByNodeType
).get(node[nodeTypeKey]) || [];
const anyTypeSelectors = isExit
? this.anyTypeExitSelectors
: this.anyTypeEnterSelectors;
/*
* selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
* Iterate through each of them, applying selectors in the right order.
*/
let selectorsByNodeTypeIndex = 0;
let anyTypeSelectorsIndex = 0;
while (
selectorsByNodeTypeIndex < selectorsByNodeType.length ||
anyTypeSelectorsIndex < anyTypeSelectors.length
) {
/*
* If we've already exhausted the selectors for this node type,
* or if the next any type selector is more specific than the
* next selector for this node type, apply the any type selector.
*/
const hasMoreNodeTypeSelectors =
selectorsByNodeTypeIndex < selectorsByNodeType.length;
const hasMoreAnyTypeSelectors =
anyTypeSelectorsIndex < anyTypeSelectors.length;
const anyTypeSelector = anyTypeSelectors[anyTypeSelectorsIndex];
const nodeTypeSelector =
selectorsByNodeType[selectorsByNodeTypeIndex];
// Only compare specificity if both selectors exist
const isAnyTypeSelectorLessSpecific =
hasMoreAnyTypeSelectors &&
hasMoreNodeTypeSelectors &&
anyTypeSelector.compare(nodeTypeSelector) < 0;
if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorLessSpecific) {
anyTypeSelectorsIndex++;
if (this.matches(node, ancestry, anyTypeSelector)) {
selectors.push(anyTypeSelector.source);
}
} else {
selectorsByNodeTypeIndex++;
if (this.matches(node, ancestry, nodeTypeSelector)) {
selectors.push(nodeTypeSelector.source);
}
}
}
return selectors;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Traverses source code and ensures that visitor methods are called when
* entering and leaving each node.
*/
class SourceCodeTraverser {
/**
* The language of the source code being traversed.
* @type {Language}
*/
#language;
/**
* Map of languages to instances of this class.
* @type {WeakMap<Language, SourceCodeTraverser>}
*/
static instances = new WeakMap();
/**
* Creates a new instance.
* @param {Language} language The language of the source code being traversed.
*/
constructor(language) {
this.#language = language;
}
static getInstance(language) {
if (!this.instances.has(language)) {
this.instances.set(language, new this(language));
}
return this.instances.get(language);
}
/**
* Traverses the given source code synchronously.
* @param {SourceCode} sourceCode The source code to traverse.
* @param {SourceCodeVisitor} visitor The emitter to use for events.
* @param {Object} options Options for traversal.
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
* @returns {void}
* @throws {Error} If an error occurs during traversal.
*/
traverseSync(sourceCode, visitor, { steps } = {}) {
const esquery = new ESQueryHelper(visitor, {
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
fallback: vk.getKeys,
matchClass: this.#language.matchesSelectorClass ?? (() => false),
nodeTypeKey: this.#language.nodeTypeKey,
});
const currentAncestry = [];
for (const step of steps ?? sourceCode.traverse()) {
switch (step.kind) {
case STEP_KIND_VISIT: {
try {
if (step.phase === 1) {
esquery
.calculateSelectors(
step.target,
currentAncestry,
false,
)
.forEach(selector => {
visitor.callSync(
selector,
...(step.args ?? [step.target]),
);
});
currentAncestry.unshift(step.target);
} else {
currentAncestry.shift();
esquery
.calculateSelectors(
step.target,
currentAncestry,
true,
)
.forEach(selector => {
visitor.callSync(
selector,
...(step.args ?? [step.target]),
);
});
}
} catch (err) {
err.currentNode = step.target;
throw err;
}
break;
}
case STEP_KIND_CALL: {
visitor.callSync(step.target, ...step.args);
break;
}
default:
throw new Error(
`Invalid traversal step found: "${step.kind}".`,
);
}
}
}
}
module.exports = { SourceCodeTraverser };
-81
View File
@@ -1,81 +0,0 @@
/**
* @fileoverview SourceCodeVisitor class
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const emptyArray = Object.freeze([]);
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* A structure to hold a list of functions to call for a given name.
* This is used to allow multiple rules to register functions for a given name
* without having to know about each other.
*/
class SourceCodeVisitor {
/**
* The functions to call for a given name.
* @type {Map<string, Function[]>}
*/
#functions = new Map();
/**
* Adds a function to the list of functions to call for a given name.
* @param {string} name The name of the function to call.
* @param {Function} func The function to call.
* @returns {void}
*/
add(name, func) {
if (this.#functions.has(name)) {
this.#functions.get(name).push(func);
} else {
this.#functions.set(name, [func]);
}
}
/**
* Gets the list of functions to call for a given name.
* @param {string} name The name of the function to call.
* @returns {Function[]} The list of functions to call.
*/
get(name) {
if (this.#functions.has(name)) {
return this.#functions.get(name);
}
return emptyArray;
}
/**
* Iterates over all names and calls the callback with the name.
* @param {(name:string) => void} callback The callback to call for each name.
* @returns {void}
*/
forEachName(callback) {
this.#functions.forEach((funcs, name) => {
callback(name);
});
}
/**
* Calls the functions for a given name with the given arguments.
* @param {string} name The name of the function to call.
* @param {any[]} args The arguments to pass to the function.
* @returns {void}
*/
callSync(name, ...args) {
if (this.#functions.has(name)) {
this.#functions.get(name).forEach(func => func(...args));
}
}
}
module.exports = { SourceCodeVisitor };
+79 -127
View File
@@ -5,8 +5,6 @@
"use strict";
const { startTime, endTime } = require("../shared/stats");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
@@ -15,26 +13,26 @@ const { startTime, endTime } = require("../shared/stats");
/**
* Align the string to left
* @param {string} str string to evaluate
* @param {number} len length of the string
* @param {int} len length of the string
* @param {string} ch delimiter character
* @returns {string} modified string
* @private
*/
function alignLeft(str, len, ch) {
return str + new Array(len - str.length + 1).join(ch || " ");
return str + new Array(len - str.length + 1).join(ch || " ");
}
/* c8 ignore next */
/**
* Align the string to right
* @param {string} str string to evaluate
* @param {number} len length of the string
* @param {int} len length of the string
* @param {string} ch delimiter character
* @returns {string} modified string
* @private
*/
function alignRight(str, len, ch) {
return new Array(len - str.length + 1).join(ch || " ") + str;
return new Array(len - str.length + 1).join(ch || " ") + str;
}
//------------------------------------------------------------------------------
@@ -51,21 +49,19 @@ const ALIGN = [alignLeft, alignRight, alignRight];
* @returns {number} the number of rules to show
*/
function getListSize() {
const MINIMUM_SIZE = 10;
const MINIMUM_SIZE = 10;
if (typeof process.env.TIMING !== "string") {
return MINIMUM_SIZE;
}
if (typeof process.env.TIMING !== "string") {
return MINIMUM_SIZE;
}
if (process.env.TIMING.toLowerCase() === "all") {
return Number.POSITIVE_INFINITY;
}
if (process.env.TIMING.toLowerCase() === "all") {
return Number.POSITIVE_INFINITY;
}
const TIMING_ENV_VAR_AS_INTEGER = Number.parseInt(process.env.TIMING, 10);
const TIMING_ENV_VAR_AS_INTEGER = Number.parseInt(process.env.TIMING, 10);
return TIMING_ENV_VAR_AS_INTEGER > 10
? TIMING_ENV_VAR_AS_INTEGER
: MINIMUM_SIZE;
return TIMING_ENV_VAR_AS_INTEGER > 10 ? TIMING_ENV_VAR_AS_INTEGER : MINIMUM_SIZE;
}
/* c8 ignore next */
@@ -76,134 +72,90 @@ function getListSize() {
* @private
*/
function display(data) {
let total = 0;
const rows = Object.keys(data)
.map(key => {
const time = data[key];
let total = 0;
const rows = Object.keys(data)
.map(key => {
const time = data[key];
total += time;
return [key, time];
})
.sort((a, b) => b[1] - a[1])
.slice(0, getListSize());
total += time;
return [key, time];
})
.sort((a, b) => b[1] - a[1])
.slice(0, getListSize());
rows.forEach(row => {
row.push(`${((row[1] * 100) / total).toFixed(1)}%`);
row[1] = row[1].toFixed(3);
});
rows.forEach(row => {
row.push(`${(row[1] * 100 / total).toFixed(1)}%`);
row[1] = row[1].toFixed(3);
});
rows.unshift(HEADERS);
rows.unshift(HEADERS);
const widths = [];
const widths = [];
rows.forEach(row => {
const len = row.length;
rows.forEach(row => {
const len = row.length;
for (let i = 0; i < len; i++) {
const n = row[i].length;
for (let i = 0; i < len; i++) {
const n = row[i].length;
if (!widths[i] || n > widths[i]) {
widths[i] = n;
}
}
});
if (!widths[i] || n > widths[i]) {
widths[i] = n;
}
}
});
const table = rows.map(row =>
row.map((cell, index) => ALIGN[index](cell, widths[index])).join(" | "),
);
const table = rows.map(row => (
row
.map((cell, index) => ALIGN[index](cell, widths[index]))
.join(" | ")
));
table.splice(
1,
0,
widths
.map((width, index) => {
const extraAlignment =
index !== 0 && index !== widths.length - 1 ? 2 : 1;
table.splice(1, 0, widths.map((width, index) => {
const extraAlignment = index !== 0 && index !== widths.length - 1 ? 2 : 1;
return ALIGN[index](":", width + extraAlignment, "-");
})
.join("|"),
);
return ALIGN[index](":", width + extraAlignment, "-");
}).join("|"));
console.log(table.join("\n")); // eslint-disable-line no-console -- Debugging function
console.log(table.join("\n")); // eslint-disable-line no-console -- Debugging function
}
/* c8 ignore next */
module.exports = (function () {
const data = Object.create(null);
let displayEnabled = true;
module.exports = (function() {
/**
* Time the run
* @param {any} key key from the data object
* @param {Function} fn function to be called
* @param {boolean} stats if 'stats' is true, return the result and the time difference
* @returns {Function} function to be executed
* @private
*/
function time(key, fn, stats) {
return function (...args) {
const t = startTime();
const result = fn(...args);
const tdiff = endTime(t);
const data = Object.create(null);
if (enabled) {
if (typeof data[key] === "undefined") {
data[key] = 0;
}
/**
* Time the run
* @param {any} key key from the data object
* @param {Function} fn function to be called
* @returns {Function} function to be executed
* @private
*/
function time(key, fn) {
if (typeof data[key] === "undefined") {
data[key] = 0;
}
data[key] += tdiff;
}
return function(...args) {
let t = process.hrtime();
const result = fn(...args);
return stats ? { result, tdiff } : result;
};
}
t = process.hrtime(t);
data[key] += t[0] * 1e3 + t[1] / 1e6;
return result;
};
}
/**
* Returns a shallow copy of the collected timings data.
* @returns {Record<string, number>} mapping of ruleId to total time in ms
*/
function getData() {
return { ...data };
}
if (enabled) {
process.on("exit", () => {
display(data);
});
}
/**
* Merges rule timing totals collected elsewhere into this process' totals.
* @param {Record<string, number>} dataToMerge mapping of ruleId to total time in ms
* @returns {void}
*/
function mergeData(dataToMerge) {
for (const [key, value] of Object.entries(dataToMerge)) {
if (typeof data[key] === "undefined") {
data[key] = 0;
}
data[key] += value;
}
}
return {
time,
enabled,
getListSize
};
/**
* Disables printing of timing data on process exit.
* Intended for worker threads or non-main contexts.
* @returns {void}
*/
function disableDisplay() {
displayEnabled = false;
}
if (enabled) {
process.on("exit", () => {
if (displayEnabled && Object.keys(data).length > 0) {
display(data);
}
});
}
return {
time,
enabled,
getListSize,
getData,
mergeData,
disableDisplay,
};
})();
}());
-115
View File
@@ -1,115 +0,0 @@
/**
* @fileoverview Virtual file
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/** @typedef {import("@eslint/core").File} File */
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines if a given value has a byte order mark (BOM).
* @param {string|Uint8Array} value The value to check.
* @returns {boolean} `true` if the value has a BOM, `false` otherwise.
*/
function hasUnicodeBOM(value) {
return typeof value === "string"
? value.charCodeAt(0) === 0xfeff
: value[0] === 0xef && value[1] === 0xbb && value[2] === 0xbf;
}
/**
* Strips Unicode BOM from the given value.
* @param {string|Uint8Array} value The value to remove the BOM from.
* @returns {string|Uint8Array} The stripped value.
*/
function stripUnicodeBOM(value) {
if (!hasUnicodeBOM(value)) {
return value;
}
if (typeof value === "string") {
/*
* Check Unicode BOM.
* In JavaScript, string data is stored as UTF-16, so BOM is 0xFEFF.
* http://www.ecma-international.org/ecma-262/6.0/#sec-unicode-format-control-characters
*/
return value.slice(1);
}
/*
* In a Uint8Array, the BOM is represented by three bytes: 0xEF, 0xBB, and 0xBF,
* so we can just remove the first three bytes.
*/
return value.slice(3);
}
//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------
/**
* Represents a virtual file inside of ESLint.
* @implements {File}
*/
class VFile {
/**
* The file path including any processor-created virtual path.
* @type {string}
* @readonly
*/
path;
/**
* The file path on disk.
* @type {string}
* @readonly
*/
physicalPath;
/**
* The file contents.
* @type {string|Uint8Array}
* @readonly
*/
body;
/**
* The raw body of the file, including a BOM if present.
* @type {string|Uint8Array}
* @readonly
*/
rawBody;
/**
* Indicates whether the file has a byte order mark (BOM).
* @type {boolean}
* @readonly
*/
bom;
/**
* Creates a new instance.
* @param {string} path The file path.
* @param {string|Uint8Array} body The file contents.
* @param {Object} [options] Additional options.
* @param {string} [options.physicalPath] The file path on disk.
*/
constructor(path, body, { physicalPath } = {}) {
this.path = path;
this.physicalPath = physicalPath ?? path;
this.bom = hasUnicodeBOM(body);
this.body = stripUnicodeBOM(body);
this.rawBody = body;
}
}
module.exports = { VFile };
+311 -449
View File
@@ -23,7 +23,6 @@ const optionator = require("optionator");
* @property {string} [cacheLocation] Path to the cache file or directory
* @property {"metadata" | "content"} cacheStrategy Strategy to use for detecting changed files in the cache
* @property {boolean} [color] Force enabling/disabling of color
* @property {number | "auto" | "off"} [concurrency] Number of linting threads, "auto" to choose automatically, "off" for no multithreading
* @property {string} [config] Use this configuration, overriding .eslintrc.* config options if present
* @property {boolean} debug Output debugging information
* @property {string[]} [env] Specify environments
@@ -31,7 +30,6 @@ const optionator = require("optionator");
* @property {boolean} errorOnUnmatchedPattern Prevent errors when pattern is unmatched
* @property {boolean} eslintrc Disable use of configuration from .eslintrc.*
* @property {string[]} [ext] Specify JavaScript file extensions
* @property {string[]} [flag] Feature flags
* @property {boolean} fix Automatically fix problems
* @property {boolean} fixDryRun Automatically fix problems without saving the changes to the file system
* @property {("directive" | "problem" | "suggestion" | "layout")[]} [fixType] Specify the types of fixes to apply (directive, problem, suggestion, layout)
@@ -40,31 +38,23 @@ const optionator = require("optionator");
* @property {boolean} [help] Show help
* @property {boolean} ignore Disable use of ignore files and patterns
* @property {string} [ignorePath] Specify path of ignore file
* @property {string[]} [ignorePattern] Patterns of files to ignore. In eslintrc mode, these are in addition to `.eslintignore`
* @property {string[]} [ignorePattern] Pattern of files to ignore (in addition to those in .eslintignore)
* @property {boolean} init Run config initialization wizard
* @property {boolean} inlineConfig Prevent comments from changing config or rules
* @property {number} maxWarnings Number of warnings to trigger nonzero exit code
* @property {string} [outputFile] Specify file to write report to
* @property {string} [parser] Specify the parser to be used
* @property {Object} [parserOptions] Specify parser options
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
* @property {boolean} [passOnUnprunedSuppressions] Ignore unused suppressions
* @property {string[]} [plugin] Specify plugins
* @property {string} [printConfig] Print the configuration for the given file
* @property {boolean} [pruneSuppressions] Prune unused suppressions
* @property {boolean} quiet Report errors only
* @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable and eslint-enable directives
* @property {string | undefined} reportUnusedDisableDirectivesSeverity A severity string indicating if and how unused disable and enable directives should be tracked and reported.
* @property {string} [resolvePluginsRelativeTo] A folder where plugins should be resolved from, CWD by default
* @property {Object} [rule] Specify rules
* @property {string[]} [rulesdir] Load additional rules from this directory. Deprecated: Use rules from plugins
* @property {boolean} [stats] Report additional statistics
* @property {boolean} stdin Lint code provided on <STDIN>
* @property {string} [stdinFilename] Specify filename to process STDIN as
* @property {boolean} [suppressAll] Suppress all error violations
* @property {string} [suppressionsLocation] Path to the suppressions file or directory
* @property {string[]} [suppressRule] Suppress specific rules
* @property {boolean} quiet Report errors only
* @property {boolean} [version] Output the version number
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files
* @property {string[]} _ Positional filenames or patterns
@@ -81,456 +71,328 @@ const optionator = require("optionator");
* @param {boolean} usingFlatConfig Indicates if flat config is being used.
* @returns {Object} The optionator instance.
*/
module.exports = function (usingFlatConfig) {
let lookupFlag;
module.exports = function(usingFlatConfig) {
if (usingFlatConfig) {
lookupFlag = {
option: "config-lookup",
type: "Boolean",
default: "true",
description: "Disable look up for eslint.config.js",
};
} else {
lookupFlag = {
option: "eslintrc",
type: "Boolean",
default: "true",
description: "Disable use of configuration from .eslintrc.*",
};
}
let lookupFlag;
let envFlag;
if (usingFlatConfig) {
lookupFlag = {
option: "config-lookup",
type: "Boolean",
default: "true",
description: "Disable look up for eslint.config.js"
};
} else {
lookupFlag = {
option: "eslintrc",
type: "Boolean",
default: "true",
description: "Disable use of configuration from .eslintrc.*"
};
}
if (!usingFlatConfig) {
envFlag = {
option: "env",
type: "[String]",
description: "Specify environments",
};
}
let envFlag;
let inspectConfigFlag;
if (!usingFlatConfig) {
envFlag = {
option: "env",
type: "[String]",
description: "Specify environments"
};
}
if (usingFlatConfig) {
inspectConfigFlag = {
option: "inspect-config",
type: "Boolean",
description:
"Open the config inspector with the current configuration",
};
}
let extFlag;
let extFlag;
if (!usingFlatConfig) {
extFlag = {
option: "ext",
type: "[String]",
description: "Specify JavaScript file extensions"
};
}
if (!usingFlatConfig) {
extFlag = {
option: "ext",
type: "[String]",
description: "Specify JavaScript file extensions",
};
} else {
extFlag = {
option: "ext",
type: "[String]",
description: "Specify additional file extensions to lint",
};
}
let resolvePluginsFlag;
let resolvePluginsFlag;
if (!usingFlatConfig) {
resolvePluginsFlag = {
option: "resolve-plugins-relative-to",
type: "path::String",
description: "A folder where plugins should be resolved from, CWD by default"
};
}
if (!usingFlatConfig) {
resolvePluginsFlag = {
option: "resolve-plugins-relative-to",
type: "path::String",
description:
"A folder where plugins should be resolved from, CWD by default",
};
}
let rulesDirFlag;
let rulesDirFlag;
if (!usingFlatConfig) {
rulesDirFlag = {
option: "rulesdir",
type: "[path::String]",
description: "Load additional rules from this directory. Deprecated: Use rules from plugins"
};
}
if (!usingFlatConfig) {
rulesDirFlag = {
option: "rulesdir",
type: "[path::String]",
description:
"Load additional rules from this directory. Deprecated: Use rules from plugins",
};
}
let ignorePathFlag;
let ignorePathFlag;
if (!usingFlatConfig) {
ignorePathFlag = {
option: "ignore-path",
type: "path::String",
description: "Specify path of ignore file"
};
}
if (!usingFlatConfig) {
ignorePathFlag = {
option: "ignore-path",
type: "path::String",
description: "Specify path of ignore file",
};
}
let warnIgnoredFlag;
let statsFlag;
if (usingFlatConfig) {
warnIgnoredFlag = {
option: "warn-ignored",
type: "Boolean",
default: "true",
description: "Suppress warnings when the file list includes ignored files"
};
}
if (usingFlatConfig) {
statsFlag = {
option: "stats",
type: "Boolean",
default: "false",
description: "Add statistics to the lint report",
};
}
let warnIgnoredFlag;
if (usingFlatConfig) {
warnIgnoredFlag = {
option: "warn-ignored",
type: "Boolean",
default: "true",
description:
"Suppress warnings when the file list includes ignored files",
};
}
let flagFlag;
if (usingFlatConfig) {
flagFlag = {
option: "flag",
type: "[String]",
description: "Enable a feature flag",
};
}
let reportUnusedInlineConfigsFlag;
if (usingFlatConfig) {
reportUnusedInlineConfigsFlag = {
option: "report-unused-inline-configs",
type: "String",
default: void 0,
description:
"Adds reported errors for unused eslint inline config comments",
enum: ["off", "warn", "error", "0", "1", "2"],
};
}
let mcpFlag;
if (usingFlatConfig) {
mcpFlag = {
option: "mcp",
type: "Boolean",
description: "Start the ESLint MCP server",
};
}
let concurrencyFlag;
if (usingFlatConfig) {
concurrencyFlag = {
option: "concurrency",
type: "Int|String",
default: "off",
description:
"Number of linting threads, auto to choose automatically, off for no multithreading",
};
}
return optionator({
prepend: "eslint [options] file.js [file.js] [dir]",
defaults: {
concatRepeatedArrays: true,
mergeRepeatedObjects: true,
},
options: [
{
heading: "Basic configuration",
},
lookupFlag,
{
option: "config",
alias: "c",
type: "path::String",
description: usingFlatConfig
? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs"
: "Use this configuration, overriding .eslintrc.* config options if present",
},
inspectConfigFlag,
envFlag,
extFlag,
{
option: "global",
type: "[String]",
description: "Define global variables",
},
{
option: "parser",
type: "String",
description: "Specify the parser to be used",
},
{
option: "parser-options",
type: "Object",
description: "Specify parser options",
},
resolvePluginsFlag,
{
heading: "Specify Rules and Plugins",
},
{
option: "plugin",
type: "[String]",
description: "Specify plugins",
},
{
option: "rule",
type: "Object",
description: "Specify rules",
},
rulesDirFlag,
{
heading: "Fix Problems",
},
{
option: "fix",
type: "Boolean",
default: false,
description: "Automatically fix problems",
},
{
option: "fix-dry-run",
type: "Boolean",
default: false,
description:
"Automatically fix problems without saving the changes to the file system",
},
{
option: "fix-type",
type: "Array",
description:
"Specify the types of fixes to apply (directive, problem, suggestion, layout)",
},
{
heading: "Ignore Files",
},
ignorePathFlag,
{
option: "ignore",
type: "Boolean",
default: "true",
description: "Disable use of ignore files and patterns",
},
{
option: "ignore-pattern",
type: "[String]",
description: `Patterns of files to ignore${usingFlatConfig ? "" : " (in addition to those in .eslintignore)"}`,
concatRepeatedArrays: [
true,
{
oneValuePerFlag: true,
},
],
},
{
heading: "Use stdin",
},
{
option: "stdin",
type: "Boolean",
default: "false",
description: "Lint code provided on <STDIN>",
},
{
option: "stdin-filename",
type: "String",
description: "Specify filename to process STDIN as",
},
{
heading: "Handle Warnings",
},
{
option: "quiet",
type: "Boolean",
default: "false",
description: "Report errors only",
},
{
option: "max-warnings",
type: "Int",
default: "-1",
description: "Number of warnings to trigger nonzero exit code",
},
{
heading: "Output",
},
{
option: "output-file",
alias: "o",
type: "path::String",
description: "Specify file to write report to",
},
{
option: "format",
alias: "f",
type: "String",
default: "stylish",
description: "Use a specific output format",
},
{
option: "color",
type: "Boolean",
alias: "no-color",
description: "Force enabling/disabling of color",
},
{
heading: "Inline configuration comments",
},
{
option: "inline-config",
type: "Boolean",
default: "true",
description: "Prevent comments from changing config or rules",
},
{
option: "report-unused-disable-directives",
type: "Boolean",
default: void 0,
description:
"Adds reported errors for unused eslint-disable and eslint-enable directives",
},
{
option: "report-unused-disable-directives-severity",
type: "String",
default: void 0,
description:
"Chooses severity level for reporting unused eslint-disable and eslint-enable directives",
enum: ["off", "warn", "error", "0", "1", "2"],
},
reportUnusedInlineConfigsFlag,
{
heading: "Caching",
},
{
option: "cache",
type: "Boolean",
default: "false",
description: "Only check changed files",
},
{
option: "cache-file",
type: "path::String",
default: ".eslintcache",
description:
"Path to the cache file. Deprecated: use --cache-location",
},
{
option: "cache-location",
type: "path::String",
description: "Path to the cache file or directory",
},
{
option: "cache-strategy",
dependsOn: ["cache"],
type: "String",
default: "metadata",
enum: ["metadata", "content"],
description:
"Strategy to use for detecting changed files in the cache",
},
{
heading: "Suppressing Violations",
},
{
option: "suppress-all",
type: "Boolean",
default: "false",
description: "Suppress all violations",
},
{
option: "suppress-rule",
type: "[String]",
description: "Suppress specific rules",
},
{
option: "suppressions-location",
type: "path::String",
description: "Specify the location of the suppressions file",
},
{
option: "prune-suppressions",
type: "Boolean",
default: "false",
description: "Prune unused suppressions",
},
{
option: "pass-on-unpruned-suppressions",
type: "Boolean",
default: "false",
description: "Ignore unused suppressions",
},
{
heading: "Miscellaneous",
},
{
option: "init",
type: "Boolean",
default: "false",
description: "Run config initialization wizard",
},
{
option: "env-info",
type: "Boolean",
default: "false",
description: "Output execution environment information",
},
{
option: "error-on-unmatched-pattern",
type: "Boolean",
default: "true",
description: "Prevent errors when pattern is unmatched",
},
{
option: "exit-on-fatal-error",
type: "Boolean",
default: "false",
description: "Exit with exit code 2 in case of fatal error",
},
warnIgnoredFlag,
{
option: "pass-on-no-patterns",
type: "Boolean",
default: false,
description:
"Exit with exit code 0 in case no file patterns are passed",
},
{
option: "debug",
type: "Boolean",
default: false,
description: "Output debugging information",
},
{
option: "help",
alias: "h",
type: "Boolean",
description: "Show help",
},
{
option: "version",
alias: "v",
type: "Boolean",
description: "Output the version number",
},
{
option: "print-config",
type: "path::String",
description: "Print the configuration for the given file",
},
statsFlag,
flagFlag,
mcpFlag,
concurrencyFlag,
].filter(value => !!value),
});
return optionator({
prepend: "eslint [options] file.js [file.js] [dir]",
defaults: {
concatRepeatedArrays: true,
mergeRepeatedObjects: true
},
options: [
{
heading: "Basic configuration"
},
lookupFlag,
{
option: "config",
alias: "c",
type: "path::String",
description: usingFlatConfig
? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs"
: "Use this configuration, overriding .eslintrc.* config options if present"
},
envFlag,
extFlag,
{
option: "global",
type: "[String]",
description: "Define global variables"
},
{
option: "parser",
type: "String",
description: "Specify the parser to be used"
},
{
option: "parser-options",
type: "Object",
description: "Specify parser options"
},
resolvePluginsFlag,
{
heading: "Specify Rules and Plugins"
},
{
option: "plugin",
type: "[String]",
description: "Specify plugins"
},
{
option: "rule",
type: "Object",
description: "Specify rules"
},
rulesDirFlag,
{
heading: "Fix Problems"
},
{
option: "fix",
type: "Boolean",
default: false,
description: "Automatically fix problems"
},
{
option: "fix-dry-run",
type: "Boolean",
default: false,
description: "Automatically fix problems without saving the changes to the file system"
},
{
option: "fix-type",
type: "Array",
description: "Specify the types of fixes to apply (directive, problem, suggestion, layout)"
},
{
heading: "Ignore Files"
},
ignorePathFlag,
{
option: "ignore",
type: "Boolean",
default: "true",
description: "Disable use of ignore files and patterns"
},
{
option: "ignore-pattern",
type: "[String]",
description: "Pattern of files to ignore (in addition to those in .eslintignore)",
concatRepeatedArrays: [true, {
oneValuePerFlag: true
}]
},
{
heading: "Use stdin"
},
{
option: "stdin",
type: "Boolean",
default: "false",
description: "Lint code provided on <STDIN>"
},
{
option: "stdin-filename",
type: "String",
description: "Specify filename to process STDIN as"
},
{
heading: "Handle Warnings"
},
{
option: "quiet",
type: "Boolean",
default: "false",
description: "Report errors only"
},
{
option: "max-warnings",
type: "Int",
default: "-1",
description: "Number of warnings to trigger nonzero exit code"
},
{
heading: "Output"
},
{
option: "output-file",
alias: "o",
type: "path::String",
description: "Specify file to write report to"
},
{
option: "format",
alias: "f",
type: "String",
default: "stylish",
description: "Use a specific output format"
},
{
option: "color",
type: "Boolean",
alias: "no-color",
description: "Force enabling/disabling of color"
},
{
heading: "Inline configuration comments"
},
{
option: "inline-config",
type: "Boolean",
default: "true",
description: "Prevent comments from changing config or rules"
},
{
option: "report-unused-disable-directives",
type: "Boolean",
default: void 0,
description: "Adds reported errors for unused eslint-disable and eslint-enable directives"
},
{
option: "report-unused-disable-directives-severity",
type: "String",
default: void 0,
description: "Chooses severity level for reporting unused eslint-disable and eslint-enable directives",
enum: ["off", "warn", "error", "0", "1", "2"]
},
{
heading: "Caching"
},
{
option: "cache",
type: "Boolean",
default: "false",
description: "Only check changed files"
},
{
option: "cache-file",
type: "path::String",
default: ".eslintcache",
description: "Path to the cache file. Deprecated: use --cache-location"
},
{
option: "cache-location",
type: "path::String",
description: "Path to the cache file or directory"
},
{
option: "cache-strategy",
dependsOn: ["cache"],
type: "String",
default: "metadata",
enum: ["metadata", "content"],
description: "Strategy to use for detecting changed files in the cache"
},
{
heading: "Miscellaneous"
},
{
option: "init",
type: "Boolean",
default: "false",
description: "Run config initialization wizard"
},
{
option: "env-info",
type: "Boolean",
default: "false",
description: "Output execution environment information"
},
{
option: "error-on-unmatched-pattern",
type: "Boolean",
default: "true",
description: "Prevent errors when pattern is unmatched"
},
{
option: "exit-on-fatal-error",
type: "Boolean",
default: "false",
description: "Exit with exit code 2 in case of fatal error"
},
warnIgnoredFlag,
{
option: "debug",
type: "Boolean",
default: false,
description: "Output debugging information"
},
{
option: "help",
alias: "h",
type: "Boolean",
description: "Show help"
},
{
option: "version",
alias: "v",
type: "Boolean",
description: "Output the version number"
},
{
option: "print-config",
type: "path::String",
description: "Print the configuration for the given file"
}
].filter(value => !!value)
});
};
File diff suppressed because it is too large. Load diff
+1 -3
View File
@@ -1,7 +1,5 @@
"use strict";
const RuleTester = require("./rule-tester");
module.exports = {
RuleTester,
RuleTester: require("./rule-tester")
};
File diff suppressed because it is too large. Load diff
+231 -305
View File
@@ -40,23 +40,20 @@ const astUtils = require("./utils/ast-utils");
* @returns {boolean} `true` if the lists have same tokens.
*/
function areEqualTokenLists(left, right) {
if (left.length !== right.length) {
return false;
}
if (left.length !== right.length) {
return false;
}
for (let i = 0; i < left.length; i++) {
const leftToken = left[i],
rightToken = right[i];
for (let i = 0; i < left.length; i++) {
const leftToken = left[i],
rightToken = right[i];
if (
leftToken.type !== rightToken.type ||
leftToken.value !== rightToken.value
) {
return false;
}
}
if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
return false;
}
}
return true;
return true;
}
/**
@@ -66,16 +63,18 @@ function areEqualTokenLists(left, right) {
* @returns {boolean} `true` if the keys are equal.
*/
function areEqualKeys(left, right) {
if (typeof left === "string" && typeof right === "string") {
// Statically computed names.
return left === right;
}
if (Array.isArray(left) && Array.isArray(right)) {
// Token lists.
return areEqualTokenLists(left, right);
}
if (typeof left === "string" && typeof right === "string") {
return false;
// Statically computed names.
return left === right;
}
if (Array.isArray(left) && Array.isArray(right)) {
// Token lists.
return areEqualTokenLists(left, right);
}
return false;
}
/**
@@ -84,7 +83,7 @@ function areEqualKeys(left, right) {
* @returns {boolean} `true` if the node is of an accessor kind.
*/
function isAccessorKind(node) {
return node.kind === "get" || node.kind === "set";
return node.kind === "get" || node.kind === "set";
}
/**
@@ -96,13 +95,13 @@ function isAccessorKind(node) {
* @returns {boolean} `true` if the node is an argument of the specified method call.
*/
function isArgumentOfMethodCall(node, index, object, property) {
const parent = node.parent;
const parent = node.parent;
return (
parent.type === "CallExpression" &&
astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
parent.arguments[index] === node
);
return (
parent.type === "CallExpression" &&
astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
parent.arguments[index] === node
);
}
/**
@@ -111,310 +110,237 @@ function isArgumentOfMethodCall(node, index, object, property) {
* @returns {boolean} `true` if the node is a property descriptor.
*/
function isPropertyDescriptor(node) {
// Object.defineProperty(obj, "foo", {set: ...})
if (
isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
) {
return true;
}
/*
* Object.defineProperties(obj, {foo: {set: ...}})
* Object.create(proto, {foo: {set: ...}})
*/
const grandparent = node.parent.parent;
// Object.defineProperty(obj, "foo", {set: ...})
if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
) {
return true;
}
return (
grandparent.type === "ObjectExpression" &&
(isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
isArgumentOfMethodCall(
grandparent,
1,
"Object",
"defineProperties",
))
);
/*
* Object.defineProperties(obj, {foo: {set: ...}})
* Object.create(proto, {foo: {set: ...}})
*/
const grandparent = node.parent.parent;
return grandparent.type === "ObjectExpression" && (
isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
meta: {
type: "suggestion",
defaultOptions: [
{
enforceForTSTypes: false,
enforceForClassMembers: true,
getWithoutSet: false,
setWithoutGet: true,
},
],
docs: {
description: "Enforce getter and setter pairs in objects and classes",
recommended: false,
url: "https://eslint.org/docs/latest/rules/accessor-pairs"
},
docs: {
description:
"Enforce getter and setter pairs in objects and classes",
recommended: false,
url: "https://eslint.org/docs/latest/rules/accessor-pairs",
},
schema: [{
type: "object",
properties: {
getWithoutSet: {
type: "boolean",
default: false
},
setWithoutGet: {
type: "boolean",
default: true
},
enforceForClassMembers: {
type: "boolean",
default: true
}
},
additionalProperties: false
}],
schema: [
{
type: "object",
properties: {
getWithoutSet: {
type: "boolean",
},
setWithoutGet: {
type: "boolean",
},
enforceForClassMembers: {
type: "boolean",
},
enforceForTSTypes: {
type: "boolean",
},
},
additionalProperties: false,
},
],
messages: {
missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
missingGetterInClass: "Getter is not present for class {{ name }}.",
missingSetterInClass: "Setter is not present for class {{ name }}."
}
},
create(context) {
const config = context.options[0] || {};
const checkGetWithoutSet = config.getWithoutSet === true;
const checkSetWithoutGet = config.setWithoutGet !== false;
const enforceForClassMembers = config.enforceForClassMembers !== false;
const sourceCode = context.sourceCode;
messages: {
missingGetterInPropertyDescriptor:
"Getter is not present in property descriptor.",
missingSetterInPropertyDescriptor:
"Setter is not present in property descriptor.",
missingGetterInObjectLiteral:
"Getter is not present for {{ name }}.",
missingSetterInObjectLiteral:
"Setter is not present for {{ name }}.",
missingGetterInClass: "Getter is not present for class {{ name }}.",
missingSetterInClass: "Setter is not present for class {{ name }}.",
missingGetterInType: "Getter is not present for type {{ name }}.",
missingSetterInType: "Setter is not present for type {{ name }}.",
},
},
create(context) {
const [
{
getWithoutSet: checkGetWithoutSet,
setWithoutGet: checkSetWithoutGet,
enforceForClassMembers,
enforceForTSTypes,
},
] = context.options;
const sourceCode = context.sourceCode;
/**
* Reports the given node.
* @param {ASTNode} node The node to report.
* @param {string} messageKind "missingGetter" or "missingSetter".
* @returns {void}
* @private
*/
function report(node, messageKind) {
if (node.type === "Property") {
context.report({
node,
messageId: `${messageKind}InObjectLiteral`,
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
data: { name: astUtils.getFunctionNameWithKind(node.value) }
});
} else if (node.type === "MethodDefinition") {
context.report({
node,
messageId: `${messageKind}InClass`,
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
data: { name: astUtils.getFunctionNameWithKind(node.value) }
});
} else {
context.report({
node,
messageId: `${messageKind}InPropertyDescriptor`
});
}
}
/**
* Reports the given node.
* @param {ASTNode} node The node to report.
* @param {string} messageKind "missingGetter" or "missingSetter".
* @returns {void}
* @private
*/
function report(node, messageKind) {
if (node.type === "Property") {
context.report({
node,
messageId: `${messageKind}InObjectLiteral`,
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
data: {
name: astUtils.getFunctionNameWithKind(node.value),
},
});
} else if (node.type === "MethodDefinition") {
context.report({
node,
messageId: `${messageKind}InClass`,
loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
data: {
name: astUtils.getFunctionNameWithKind(node.value),
},
});
} else if (node.type === "TSMethodSignature") {
context.report({
node,
messageId: `${messageKind}InType`,
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
data: {
name: astUtils.getFunctionNameWithKind(node),
},
});
} else {
context.report({
node,
messageId: `${messageKind}InPropertyDescriptor`,
});
}
}
/**
* Reports each of the nodes in the given list using the same messageId.
* @param {ASTNode[]} nodes Nodes to report.
* @param {string} messageKind "missingGetter" or "missingSetter".
* @returns {void}
* @private
*/
function reportList(nodes, messageKind) {
for (const node of nodes) {
report(node, messageKind);
}
}
/**
* Reports each of the nodes in the given list using the same messageId.
* @param {ASTNode[]} nodes Nodes to report.
* @param {string} messageKind "missingGetter" or "missingSetter".
* @returns {void}
* @private
*/
function reportList(nodes, messageKind) {
for (const node of nodes) {
report(node, messageKind);
}
}
/**
* Checks accessor pairs in the given list of nodes.
* @param {ASTNode[]} nodes The list to check.
* @returns {void}
* @private
*/
function checkList(nodes) {
const accessors = [];
let found = false;
/**
* Checks accessor pairs in the given list of nodes.
* @param {ASTNode[]} nodes The list to check.
* @returns {void}
* @private
*/
function checkList(nodes) {
const accessors = [];
let found = false;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isAccessorKind(node)) {
if (isAccessorKind(node)) {
// Creates a new `AccessorData` object for the given getter or setter node.
const name = astUtils.getStaticPropertyName(node);
const key =
name !== null ? name : sourceCode.getTokens(node.key);
// Creates a new `AccessorData` object for the given getter or setter node.
const name = astUtils.getStaticPropertyName(node);
const key = (name !== null) ? name : sourceCode.getTokens(node.key);
// Merges the given `AccessorData` object into the given accessors list.
for (let j = 0; j < accessors.length; j++) {
const accessor = accessors[j];
// Merges the given `AccessorData` object into the given accessors list.
for (let j = 0; j < accessors.length; j++) {
const accessor = accessors[j];
if (areEqualKeys(accessor.key, key)) {
accessor.getters.push(
...(node.kind === "get" ? [node] : []),
);
accessor.setters.push(
...(node.kind === "set" ? [node] : []),
);
found = true;
break;
}
}
if (!found) {
accessors.push({
key,
getters: node.kind === "get" ? [node] : [],
setters: node.kind === "set" ? [node] : [],
});
}
found = false;
}
}
if (areEqualKeys(accessor.key, key)) {
accessor.getters.push(...node.kind === "get" ? [node] : []);
accessor.setters.push(...node.kind === "set" ? [node] : []);
found = true;
break;
}
}
if (!found) {
accessors.push({
key,
getters: node.kind === "get" ? [node] : [],
setters: node.kind === "set" ? [node] : []
});
}
found = false;
}
}
for (const { getters, setters } of accessors) {
if (checkSetWithoutGet && setters.length && !getters.length) {
reportList(setters, "missingGetter");
}
if (checkGetWithoutSet && getters.length && !setters.length) {
reportList(getters, "missingSetter");
}
}
}
for (const { getters, setters } of accessors) {
if (checkSetWithoutGet && setters.length && !getters.length) {
reportList(setters, "missingGetter");
}
if (checkGetWithoutSet && getters.length && !setters.length) {
reportList(getters, "missingSetter");
}
}
}
/**
* Checks accessor pairs in an object literal.
* @param {ASTNode} node `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkObjectLiteral(node) {
checkList(node.properties.filter(p => p.type === "Property"));
}
/**
* Checks accessor pairs in an object literal.
* @param {ASTNode} node `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkObjectLiteral(node) {
checkList(node.properties.filter(p => p.type === "Property"));
}
/**
* Checks accessor pairs in a property descriptor.
* @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkPropertyDescriptor(node) {
const namesToCheck = new Set(
node.properties
.filter(
p =>
p.type === "Property" &&
p.kind === "init" &&
!p.computed,
)
.map(({ key }) => key.name),
);
/**
* Checks accessor pairs in a property descriptor.
* @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkPropertyDescriptor(node) {
const namesToCheck = new Set(node.properties
.filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
.map(({ key }) => key.name));
const hasGetter = namesToCheck.has("get");
const hasSetter = namesToCheck.has("set");
const hasGetter = namesToCheck.has("get");
const hasSetter = namesToCheck.has("set");
if (checkSetWithoutGet && hasSetter && !hasGetter) {
report(node, "missingGetter");
}
if (checkGetWithoutSet && hasGetter && !hasSetter) {
report(node, "missingSetter");
}
}
if (checkSetWithoutGet && hasSetter && !hasGetter) {
report(node, "missingGetter");
}
if (checkGetWithoutSet && hasGetter && !hasSetter) {
report(node, "missingSetter");
}
}
/**
* Checks the given object expression as an object literal and as a possible property descriptor.
* @param {ASTNode} node `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkObjectExpression(node) {
checkObjectLiteral(node);
if (isPropertyDescriptor(node)) {
checkPropertyDescriptor(node);
}
}
/**
* Checks the given object expression as an object literal and as a possible property descriptor.
* @param {ASTNode} node `ObjectExpression` node to check.
* @returns {void}
* @private
*/
function checkObjectExpression(node) {
checkObjectLiteral(node);
if (isPropertyDescriptor(node)) {
checkPropertyDescriptor(node);
}
}
/**
* Checks the given class body.
* @param {ASTNode} node `ClassBody` node to check.
* @returns {void}
* @private
*/
function checkClassBody(node) {
const methodDefinitions = node.body.filter(
m => m.type === "MethodDefinition",
);
/**
* Checks the given class body.
* @param {ASTNode} node `ClassBody` node to check.
* @returns {void}
* @private
*/
function checkClassBody(node) {
const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
checkList(methodDefinitions.filter(m => m.static));
checkList(methodDefinitions.filter(m => !m.static));
}
checkList(methodDefinitions.filter(m => m.static));
checkList(methodDefinitions.filter(m => !m.static));
}
/**
* Checks the given type.
* @param {ASTNode} node `TSTypeLiteral` or `TSInterfaceBody` node to check.
* @returns {void}
* @private
*/
function checkType(node) {
const members =
node.type === "TSTypeLiteral" ? node.members : node.body;
const methodDefinitions = members.filter(
m => m.type === "TSMethodSignature",
);
const listeners = {};
checkList(methodDefinitions);
}
if (checkSetWithoutGet || checkGetWithoutSet) {
listeners.ObjectExpression = checkObjectExpression;
if (enforceForClassMembers) {
listeners.ClassBody = checkClassBody;
}
}
const listeners = {};
if (checkSetWithoutGet || checkGetWithoutSet) {
listeners.ObjectExpression = checkObjectExpression;
if (enforceForClassMembers) {
listeners.ClassBody = checkClassBody;
}
if (enforceForTSTypes) {
listeners["TSTypeLiteral, TSInterfaceBody"] = checkType;
}
}
return listeners;
},
return listeners;
}
};
+220 -250
View File
@@ -12,280 +12,250 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-bracket-newline",
url: "https://eslint.style/rules/array-bracket-newline",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description:
"Enforce linebreaks after opening and before closing array brackets",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-bracket-newline",
},
docs: {
description: "Enforce linebreaks after opening and before closing array brackets",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-bracket-newline"
},
fixable: "whitespace",
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["always", "never", "consistent"],
},
{
type: "object",
properties: {
multiline: {
type: "boolean",
},
minItems: {
type: ["integer", "null"],
minimum: 0,
},
},
additionalProperties: false,
},
],
},
],
schema: [
{
oneOf: [
{
enum: ["always", "never", "consistent"]
},
{
type: "object",
properties: {
multiline: {
type: "boolean"
},
minItems: {
type: ["integer", "null"],
minimum: 0
}
},
additionalProperties: false
}
]
}
],
messages: {
unexpectedOpeningLinebreak:
"There should be no linebreak after '['.",
unexpectedClosingLinebreak:
"There should be no linebreak before ']'.",
missingOpeningLinebreak: "A linebreak is required after '['.",
missingClosingLinebreak: "A linebreak is required before ']'.",
},
},
messages: {
unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
missingOpeningLinebreak: "A linebreak is required after '['.",
missingClosingLinebreak: "A linebreak is required before ']'."
}
},
create(context) {
const sourceCode = context.sourceCode;
create(context) {
const sourceCode = context.sourceCode;
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Normalizes a given option value.
* @param {string|Object|undefined} option An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(option) {
let consistent = false;
let multiline = false;
let minItems;
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
if (option) {
if (option === "consistent") {
consistent = true;
minItems = Number.POSITIVE_INFINITY;
} else if (option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
} else {
consistent = false;
multiline = true;
minItems = Number.POSITIVE_INFINITY;
}
/**
* Normalizes a given option value.
* @param {string|Object|undefined} option An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(option) {
let consistent = false;
let multiline = false;
let minItems = 0;
return { consistent, multiline, minItems };
}
if (option) {
if (option === "consistent") {
consistent = true;
minItems = Number.POSITIVE_INFINITY;
} else if (option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
} else {
consistent = false;
multiline = true;
minItems = Number.POSITIVE_INFINITY;
}
/**
* Normalizes a given option value.
* @param {string|Object|undefined} options An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
const value = normalizeOptionValue(options);
return { consistent, multiline, minItems };
}
return { ArrayExpression: value, ArrayPattern: value };
}
/**
* Normalizes a given option value.
* @param {string|Object|undefined} options An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
const value = normalizeOptionValue(options);
/**
* Reports that there shouldn't be a linebreak after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "unexpectedOpeningLinebreak",
fix(fixer) {
const nextToken = sourceCode.getTokenAfter(token, {
includeComments: true,
});
return { ArrayExpression: value, ArrayPattern: value };
}
if (astUtils.isCommentToken(nextToken)) {
return null;
}
/**
* Reports that there shouldn't be a linebreak after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "unexpectedOpeningLinebreak",
fix(fixer) {
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
return fixer.removeRange([
token.range[1],
nextToken.range[0],
]);
},
});
}
if (astUtils.isCommentToken(nextToken)) {
return null;
}
/**
* Reports that there shouldn't be a linebreak before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "unexpectedClosingLinebreak",
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(token, {
includeComments: true,
});
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
if (astUtils.isCommentToken(previousToken)) {
return null;
}
/**
* Reports that there shouldn't be a linebreak before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "unexpectedClosingLinebreak",
fix(fixer) {
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
return fixer.removeRange([
previousToken.range[1],
token.range[0],
]);
},
});
}
if (astUtils.isCommentToken(previousToken)) {
return null;
}
/**
* Reports that there should be a linebreak after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingOpeningLinebreak",
fix(fixer) {
return fixer.insertTextAfter(token, "\n");
},
});
}
return fixer.removeRange([previousToken.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a linebreak before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingClosingLinebreak",
fix(fixer) {
return fixer.insertTextBefore(token, "\n");
},
});
}
/**
* Reports that there should be a linebreak after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingOpeningLinebreak",
fix(fixer) {
return fixer.insertTextAfter(token, "\n");
}
});
}
/**
* Reports a given node if it violated this rule.
* @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
const openBracket = sourceCode.getFirstToken(node);
const closeBracket = sourceCode.getLastToken(node);
const firstIncComment = sourceCode.getTokenAfter(openBracket, {
includeComments: true,
});
const lastIncComment = sourceCode.getTokenBefore(closeBracket, {
includeComments: true,
});
const first = sourceCode.getTokenAfter(openBracket);
const last = sourceCode.getTokenBefore(closeBracket);
/**
* Reports that there should be a linebreak before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingLinebreak(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingClosingLinebreak",
fix(fixer) {
return fixer.insertTextBefore(token, "\n");
}
});
}
const needsLinebreaks =
elements.length >= options.minItems ||
(options.multiline &&
elements.length > 0 &&
firstIncComment.loc.start.line !==
lastIncComment.loc.end.line) ||
(elements.length === 0 &&
firstIncComment.type === "Block" &&
firstIncComment.loc.start.line !==
lastIncComment.loc.end.line &&
firstIncComment === lastIncComment) ||
(options.consistent &&
openBracket.loc.end.line !== first.loc.start.line);
/**
* Reports a given node if it violated this rule.
* @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
const openBracket = sourceCode.getFirstToken(node);
const closeBracket = sourceCode.getLastToken(node);
const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
const first = sourceCode.getTokenAfter(openBracket);
const last = sourceCode.getTokenBefore(closeBracket);
/*
* Use tokens or comments to check multiline or not.
* But use only tokens to check whether linebreaks are needed.
* This allows:
* var arr = [ // eslint-disable-line foo
* 'a'
* ]
*/
const needsLinebreaks = (
elements.length >= options.minItems ||
(
options.multiline &&
elements.length > 0 &&
firstIncComment.loc.start.line !== lastIncComment.loc.end.line
) ||
(
elements.length === 0 &&
firstIncComment.type === "Block" &&
firstIncComment.loc.start.line !== lastIncComment.loc.end.line &&
firstIncComment === lastIncComment
) ||
(
options.consistent &&
openBracket.loc.end.line !== first.loc.start.line
)
);
if (needsLinebreaks) {
if (astUtils.isTokenOnSameLine(openBracket, first)) {
reportRequiredBeginningLinebreak(node, openBracket);
}
if (astUtils.isTokenOnSameLine(last, closeBracket)) {
reportRequiredEndingLinebreak(node, closeBracket);
}
} else {
if (!astUtils.isTokenOnSameLine(openBracket, first)) {
reportNoBeginningLinebreak(node, openBracket);
}
if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
reportNoEndingLinebreak(node, closeBracket);
}
}
}
/*
* Use tokens or comments to check multiline or not.
* But use only tokens to check whether linebreaks are needed.
* This allows:
* var arr = [ // eslint-disable-line foo
* 'a'
* ]
*/
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
if (needsLinebreaks) {
if (astUtils.isTokenOnSameLine(openBracket, first)) {
reportRequiredBeginningLinebreak(node, openBracket);
}
if (astUtils.isTokenOnSameLine(last, closeBracket)) {
reportRequiredEndingLinebreak(node, closeBracket);
}
} else {
if (!astUtils.isTokenOnSameLine(openBracket, first)) {
reportNoBeginningLinebreak(node, openBracket);
}
if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
reportNoEndingLinebreak(node, closeBracket);
}
}
}
return {
ArrayPattern: check,
ArrayExpression: check,
};
},
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
ArrayPattern: check,
ArrayExpression: check
};
}
};
+206 -263
View File
@@ -11,291 +11,234 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-bracket-spacing",
url: "https://eslint.style/rules/array-bracket-spacing",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description: "Enforce consistent spacing inside array brackets",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-bracket-spacing",
},
docs: {
description: "Enforce consistent spacing inside array brackets",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-bracket-spacing"
},
fixable: "whitespace",
fixable: "whitespace",
schema: [
{
enum: ["always", "never"],
},
{
type: "object",
properties: {
singleValue: {
type: "boolean",
},
objectsInArrays: {
type: "boolean",
},
arraysInArrays: {
type: "boolean",
},
},
additionalProperties: false,
},
],
schema: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
singleValue: {
type: "boolean"
},
objectsInArrays: {
type: "boolean"
},
arraysInArrays: {
type: "boolean"
}
},
additionalProperties: false
}
],
messages: {
unexpectedSpaceAfter:
"There should be no space after '{{tokenValue}}'.",
unexpectedSpaceBefore:
"There should be no space before '{{tokenValue}}'.",
missingSpaceAfter: "A space is required after '{{tokenValue}}'.",
missingSpaceBefore: "A space is required before '{{tokenValue}}'.",
},
},
create(context) {
const spaced = context.options[0] === "always",
sourceCode = context.sourceCode;
messages: {
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
missingSpaceAfter: "A space is required after '{{tokenValue}}'.",
missingSpaceBefore: "A space is required before '{{tokenValue}}'."
}
},
create(context) {
const spaced = context.options[0] === "always",
sourceCode = context.sourceCode;
/**
* Determines whether an option is set, relative to the spacing option.
* If spaced is "always", then check whether option is set to false.
* If spaced is "never", then check whether option is set to true.
* @param {Object} option The option to exclude.
* @returns {boolean} Whether or not the property is excluded.
*/
function isOptionSet(option) {
return context.options[1]
? context.options[1][option] === !spaced
: false;
}
/**
* Determines whether an option is set, relative to the spacing option.
* If spaced is "always", then check whether option is set to false.
* If spaced is "never", then check whether option is set to true.
* @param {Object} option The option to exclude.
* @returns {boolean} Whether or not the property is excluded.
*/
function isOptionSet(option) {
return context.options[1] ? context.options[1][option] === !spaced : false;
}
const options = {
spaced,
singleElementException: isOptionSet("singleValue"),
objectsInArraysException: isOptionSet("objectsInArrays"),
arraysInArraysException: isOptionSet("arraysInArrays"),
};
const options = {
spaced,
singleElementException: isOptionSet("singleValue"),
objectsInArraysException: isOptionSet("objectsInArrays"),
arraysInArraysException: isOptionSet("arraysInArrays")
};
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoBeginningSpace(node, token) {
const nextToken = sourceCode.getTokenAfter(token);
/**
* Reports that there shouldn't be a space after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoBeginningSpace(node, token) {
const nextToken = sourceCode.getTokenAfter(token);
context.report({
node,
loc: { start: token.loc.end, end: nextToken.loc.start },
messageId: "unexpectedSpaceAfter",
data: {
tokenValue: token.value,
},
fix(fixer) {
return fixer.removeRange([
token.range[1],
nextToken.range[0],
]);
},
});
}
context.report({
node,
loc: { start: token.loc.end, end: nextToken.loc.start },
messageId: "unexpectedSpaceAfter",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/**
* Reports that there shouldn't be a space before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoEndingSpace(node, token) {
const previousToken = sourceCode.getTokenBefore(token);
/**
* Reports that there shouldn't be a space before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoEndingSpace(node, token) {
const previousToken = sourceCode.getTokenBefore(token);
context.report({
node,
loc: { start: previousToken.loc.end, end: token.loc.start },
messageId: "unexpectedSpaceBefore",
data: {
tokenValue: token.value,
},
fix(fixer) {
return fixer.removeRange([
previousToken.range[1],
token.range[0],
]);
},
});
}
context.report({
node,
loc: { start: previousToken.loc.end, end: token.loc.start },
messageId: "unexpectedSpaceBefore",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.removeRange([previousToken.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a space after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingSpaceAfter",
data: {
tokenValue: token.value,
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
},
});
}
/**
* Reports that there should be a space after the first token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredBeginningSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingSpaceAfter",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* Reports that there should be a space before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingSpaceBefore",
data: {
tokenValue: token.value,
},
fix(fixer) {
return fixer.insertTextBefore(token, " ");
},
});
}
/**
* Reports that there should be a space before the last token
* @param {ASTNode} node The node to report in the event of an error.
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredEndingSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "missingSpaceBefore",
data: {
tokenValue: token.value
},
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/**
* Determines if a node is an object type
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node is an object type.
*/
function isObjectType(node) {
return (
node &&
(node.type === "ObjectExpression" ||
node.type === "ObjectPattern")
);
}
/**
* Determines if a node is an object type
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node is an object type.
*/
function isObjectType(node) {
return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern");
}
/**
* Determines if a node is an array type
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node is an array type.
*/
function isArrayType(node) {
return (
node &&
(node.type === "ArrayExpression" ||
node.type === "ArrayPattern")
);
}
/**
* Determines if a node is an array type
* @param {ASTNode} node The node to check.
* @returns {boolean} Whether or not the node is an array type.
*/
function isArrayType(node) {
return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern");
}
/**
* Validates the spacing around array brackets
* @param {ASTNode} node The node we're checking for spacing
* @returns {void}
*/
function validateArraySpacing(node) {
if (options.spaced && node.elements.length === 0) {
return;
}
/**
* Validates the spacing around array brackets
* @param {ASTNode} node The node we're checking for spacing
* @returns {void}
*/
function validateArraySpacing(node) {
if (options.spaced && node.elements.length === 0) {
return;
}
const first = sourceCode.getFirstToken(node),
second = sourceCode.getFirstToken(node, 1),
last = node.typeAnnotation
? sourceCode.getTokenBefore(node.typeAnnotation)
: sourceCode.getLastToken(node),
penultimate = sourceCode.getTokenBefore(last),
firstElement = node.elements[0],
lastElement = node.elements.at(-1);
const first = sourceCode.getFirstToken(node),
second = sourceCode.getFirstToken(node, 1),
last = node.typeAnnotation
? sourceCode.getTokenBefore(node.typeAnnotation)
: sourceCode.getLastToken(node),
penultimate = sourceCode.getTokenBefore(last),
firstElement = node.elements[0],
lastElement = node.elements[node.elements.length - 1];
const openingBracketMustBeSpaced =
(options.objectsInArraysException &&
isObjectType(firstElement)) ||
(options.arraysInArraysException &&
isArrayType(firstElement)) ||
(options.singleElementException && node.elements.length === 1)
? !options.spaced
: options.spaced;
const openingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(firstElement) ||
options.arraysInArraysException && isArrayType(firstElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
const closingBracketMustBeSpaced =
(options.objectsInArraysException &&
isObjectType(lastElement)) ||
(options.arraysInArraysException && isArrayType(lastElement)) ||
(options.singleElementException && node.elements.length === 1)
? !options.spaced
: options.spaced;
const closingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(lastElement) ||
options.arraysInArraysException && isArrayType(lastElement) ||
options.singleElementException && node.elements.length === 1
? !options.spaced : options.spaced;
if (astUtils.isTokenOnSameLine(first, second)) {
if (
openingBracketMustBeSpaced &&
!sourceCode.isSpaceBetweenTokens(first, second)
) {
reportRequiredBeginningSpace(node, first);
}
if (
!openingBracketMustBeSpaced &&
sourceCode.isSpaceBetweenTokens(first, second)
) {
reportNoBeginningSpace(node, first);
}
}
if (astUtils.isTokenOnSameLine(first, second)) {
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
reportRequiredBeginningSpace(node, first);
}
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
reportNoBeginningSpace(node, first);
}
}
if (
first !== penultimate &&
astUtils.isTokenOnSameLine(penultimate, last)
) {
if (
closingBracketMustBeSpaced &&
!sourceCode.isSpaceBetweenTokens(penultimate, last)
) {
reportRequiredEndingSpace(node, last);
}
if (
!closingBracketMustBeSpaced &&
sourceCode.isSpaceBetweenTokens(penultimate, last)
) {
reportNoEndingSpace(node, last);
}
}
}
if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) {
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportRequiredEndingSpace(node, last);
}
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
reportNoEndingSpace(node, last);
}
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
ArrayPattern: validateArraySpacing,
ArrayExpression: validateArraySpacing,
};
},
return {
ArrayPattern: validateArraySpacing,
ArrayExpression: validateArraySpacing
};
}
};
+314 -361
View File
@@ -16,8 +16,7 @@ const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
const TARGET_METHODS =
/^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;
const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;
/**
* Checks a given node is a member access which has the specified name's
@@ -27,7 +26,7 @@ const TARGET_METHODS =
* the specified name's property. The node may be a `(Chain|Member)Expression` node.
*/
function isTargetMethod(node) {
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
}
/**
@@ -36,13 +35,14 @@ function isTargetMethod(node) {
* @returns {boolean} True if any segment is reachable; false otherwise.
*/
function isAnySegmentReachable(segments) {
for (const segment of segments) {
if (segment.reachable) {
return true;
}
}
return false;
for (const segment of segments) {
if (segment.reachable) {
return true;
}
}
return false;
}
/**
@@ -52,10 +52,10 @@ function isAnySegmentReachable(segments) {
* or else `Array.prototype.` if it is an instance method.
*/
function fullMethodName(arrayMethodName) {
if (["from", "of", "isArray"].includes(arrayMethodName)) {
return "Array.".concat(arrayMethodName);
}
return "Array.prototype.".concat(arrayMethodName);
if (["from", "of", "isArray"].includes(arrayMethodName)) {
return "Array.".concat(arrayMethodName);
}
return "Array.prototype.".concat(arrayMethodName);
}
/**
@@ -67,72 +67,73 @@ function fullMethodName(arrayMethodName) {
* null otherwise.
*/
function getArrayMethodName(node) {
let currentNode = node;
let currentNode = node;
while (currentNode) {
const parent = currentNode.parent;
while (currentNode) {
const parent = currentNode.parent;
switch (parent.type) {
/*
* Looks up the destination. e.g.,
* foo.every(nativeFoo || function foo() { ... });
*/
case "LogicalExpression":
case "ConditionalExpression":
case "ChainExpression":
currentNode = parent;
break;
switch (parent.type) {
/*
* If the upper function is IIFE, checks the destination of the return value.
* e.g.
* foo.every((function() {
* // setup...
* return function callback() { ... };
* })());
*/
case "ReturnStatement": {
const func = astUtils.getUpperFunction(parent);
/*
* Looks up the destination. e.g.,
* foo.every(nativeFoo || function foo() { ... });
*/
case "LogicalExpression":
case "ConditionalExpression":
case "ChainExpression":
currentNode = parent;
break;
if (func === null || !astUtils.isCallee(func)) {
return null;
}
currentNode = func.parent;
break;
}
/*
* If the upper function is IIFE, checks the destination of the return value.
* e.g.
* foo.every((function() {
* // setup...
* return function callback() { ... };
* })());
*/
case "ReturnStatement": {
const func = astUtils.getUpperFunction(parent);
/*
* e.g.
* Array.from([], function() {});
* list.every(function() {});
*/
case "CallExpression":
if (astUtils.isArrayFromMethod(parent.callee)) {
if (
parent.arguments.length >= 2 &&
parent.arguments[1] === currentNode
) {
return "from";
}
}
if (isTargetMethod(parent.callee)) {
if (
parent.arguments.length >= 1 &&
parent.arguments[0] === currentNode
) {
return astUtils.getStaticPropertyName(parent.callee);
}
}
return null;
if (func === null || !astUtils.isCallee(func)) {
return null;
}
currentNode = func.parent;
break;
}
// Otherwise this node is not target.
default:
return null;
}
}
/*
* e.g.
* Array.from([], function() {});
* list.every(function() {});
*/
case "CallExpression":
if (astUtils.isArrayFromMethod(parent.callee)) {
if (
parent.arguments.length >= 2 &&
parent.arguments[1] === currentNode
) {
return "from";
}
}
if (isTargetMethod(parent.callee)) {
if (
parent.arguments.length >= 1 &&
parent.arguments[0] === currentNode
) {
return astUtils.getStaticPropertyName(parent.callee);
}
}
return null;
/* c8 ignore next */
return null;
// Otherwise this node is not target.
default:
return null;
}
}
/* c8 ignore next */
return null;
}
/**
@@ -141,7 +142,7 @@ function getArrayMethodName(node) {
* @returns {boolean} - `true` if the node is a void expression
*/
function isExpressionVoid(node) {
return node.type === "UnaryExpression" && node.operator === "void";
return node.type === "UnaryExpression" && node.operator === "void";
}
/**
@@ -152,40 +153,39 @@ function isExpressionVoid(node) {
* @returns {Array<Object>} - An array of fix objects to apply to the node.
*/
function voidPrependFixer(sourceCode, node, fixer) {
const requiresParens =
// prepending `void ` will fail if the node has a lower precedence than void
astUtils.getPrecedence(node) <
astUtils.getPrecedence({
type: "UnaryExpression",
operator: "void",
}) &&
// check if there are parentheses around the node to avoid redundant parentheses
!astUtils.isParenthesised(sourceCode, node);
// avoid parentheses issues
const returnOrArrowToken = sourceCode.getTokenBefore(
node,
node.parent.type === "ArrowFunctionExpression"
? astUtils.isArrowToken
: // isReturnToken
token => token.type === "Keyword" && token.value === "return",
);
const requiresParens =
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
// prepending `void ` will fail if the node has a lower precedence than void
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
const prependSpace =
// is return token, as => allows void to be adjacent
returnOrArrowToken.value === "return" &&
// If two tokens (return and "(") are adjacent
returnOrArrowToken.range[1] === firstToken.range[0];
// check if there are parentheses around the node to avoid redundant parentheses
!astUtils.isParenthesised(sourceCode, node);
return [
fixer.insertTextBefore(
firstToken,
`${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`,
),
fixer.insertTextAfter(node, requiresParens ? ")" : ""),
];
// avoid parentheses issues
const returnOrArrowToken = sourceCode.getTokenBefore(
node,
node.parent.type === "ArrowFunctionExpression"
? astUtils.isArrowToken
// isReturnToken
: token => token.type === "Keyword" && token.value === "return"
);
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
const prependSpace =
// is return token, as => allows void to be adjacent
returnOrArrowToken.value === "return" &&
// If two tokens (return and "(") are adjacent
returnOrArrowToken.range[1] === firstToken.range[0];
return [
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
fixer.insertTextAfter(node, requiresParens ? ")" : "")
];
}
/**
@@ -196,298 +196,251 @@ function voidPrependFixer(sourceCode, node, fixer) {
* @returns {Array<Object>} - An array of fix objects to apply to the node.
*/
function curlyWrapFixer(sourceCode, node, fixer) {
const arrowToken = sourceCode.getTokenBefore(
node.body,
astUtils.isArrowToken,
);
const firstToken = sourceCode.getTokenAfter(arrowToken);
const lastToken = sourceCode.getLastToken(node);
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
const firstToken = sourceCode.getTokenAfter(arrowToken);
const lastToken = sourceCode.getLastToken(node);
return [
fixer.insertTextBefore(firstToken, "{"),
fixer.insertTextAfter(lastToken, "}"),
];
return [
fixer.insertTextBefore(firstToken, "{"),
fixer.insertTextAfter(lastToken, "}")
];
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
meta: {
type: "problem",
defaultOptions: [
{
allowImplicit: false,
checkForEach: false,
allowVoid: false,
},
],
docs: {
description: "Enforce `return` statements in callbacks of array methods",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-callback-return"
},
docs: {
description:
"Enforce `return` statements in callbacks of array methods",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-callback-return",
},
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive
hasSuggestions: true,
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
allowImplicit: {
type: "boolean",
default: false
},
checkForEach: {
type: "boolean",
default: false
},
allowVoid: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
schema: [
{
type: "object",
properties: {
allowImplicit: {
type: "boolean",
},
checkForEach: {
type: "boolean",
},
allowVoid: {
type: "boolean",
},
},
additionalProperties: false,
},
],
messages: {
expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.",
wrapBraces: "Wrap the expression in `{}`.",
prependVoid: "Prepend `void` to the expression."
}
},
messages: {
expectedAtEnd:
"{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
expectedInside:
"{{arrayMethodName}}() expects a return value from {{name}}.",
expectedReturnValue:
"{{arrayMethodName}}() expects a return value from {{name}}.",
expectedNoReturnValue:
"{{arrayMethodName}}() expects no useless return value from {{name}}.",
wrapBraces: "Wrap the expression in `{}`.",
prependVoid: "Prepend `void` to the expression.",
},
},
create(context) {
create(context) {
const [options] = context.options;
const sourceCode = context.sourceCode;
const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false };
const sourceCode = context.sourceCode;
let funcInfo = {
arrayMethodName: null,
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false,
node: null,
};
let funcInfo = {
arrayMethodName: null,
upper: null,
codePath: null,
hasReturn: false,
shouldCheck: false,
node: null
};
/**
* Checks whether or not the last code path segment is reachable.
* Then reports this function if the segment is reachable.
*
* If the last code path segment is reachable, there are paths which are not
* returned or thrown.
* @param {ASTNode} node A node to check.
* @returns {void}
*/
function checkLastSegment(node) {
if (!funcInfo.shouldCheck) {
return;
}
/**
* Checks whether or not the last code path segment is reachable.
* Then reports this function if the segment is reachable.
*
* If the last code path segment is reachable, there are paths which are not
* returned or thrown.
* @param {ASTNode} node A node to check.
* @returns {void}
*/
function checkLastSegment(node) {
const messageAndSuggestions = { messageId: "", suggest: [] };
if (!funcInfo.shouldCheck) {
return;
}
if (funcInfo.arrayMethodName === "forEach") {
if (
options.checkForEach &&
node.type === "ArrowFunctionExpression" &&
node.expression
) {
if (options.allowVoid) {
if (isExpressionVoid(node.body)) {
return;
}
const messageAndSuggestions = { messageId: "", suggest: [] };
messageAndSuggestions.messageId =
"expectedNoReturnValue";
messageAndSuggestions.suggest = [
{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(
sourceCode,
node,
fixer,
);
},
},
{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(
sourceCode,
node.body,
fixer,
);
},
},
];
} else {
messageAndSuggestions.messageId =
"expectedNoReturnValue";
messageAndSuggestions.suggest = [
{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(
sourceCode,
node,
fixer,
);
},
},
];
}
}
} else {
if (
node.body.type === "BlockStatement" &&
isAnySegmentReachable(funcInfo.currentSegments)
) {
messageAndSuggestions.messageId = funcInfo.hasReturn
? "expectedAtEnd"
: "expectedInside";
}
}
if (funcInfo.arrayMethodName === "forEach") {
if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
if (messageAndSuggestions.messageId) {
const name = astUtils.getFunctionNameWithKind(node);
if (options.allowVoid) {
if (isExpressionVoid(node.body)) {
return;
}
context.report({
node,
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
messageId: messageAndSuggestions.messageId,
data: {
name,
arrayMethodName: fullMethodName(
funcInfo.arrayMethodName,
),
},
suggest:
messageAndSuggestions.suggest.length !== 0
? messageAndSuggestions.suggest
: null,
});
}
}
messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [
{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(sourceCode, node, fixer);
}
},
{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(sourceCode, node.body, fixer);
}
}
];
} else {
messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(sourceCode, node, fixer);
}
}];
}
}
} else {
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
}
}
return {
// Stacks this function's information.
onCodePathStart(codePath, node) {
let methodName = null;
if (messageAndSuggestions.messageId) {
const name = astUtils.getFunctionNameWithKind(node);
if (TARGET_NODE_TYPE.test(node.type)) {
methodName = getArrayMethodName(node);
}
context.report({
node,
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
messageId: messageAndSuggestions.messageId,
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) },
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
});
}
}
funcInfo = {
arrayMethodName: methodName,
upper: funcInfo,
codePath,
hasReturn: false,
shouldCheck: methodName && !node.async && !node.generator,
node,
currentSegments: new Set(),
};
},
return {
// Pops this function's information.
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Stacks this function's information.
onCodePathStart(codePath, node) {
onUnreachableCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},
let methodName = null;
onUnreachableCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},
if (TARGET_NODE_TYPE.test(node.type)) {
methodName = getArrayMethodName(node);
}
onCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},
funcInfo = {
arrayMethodName: methodName,
upper: funcInfo,
codePath,
hasReturn: false,
shouldCheck:
methodName &&
!node.async &&
!node.generator,
node,
currentSegments: new Set()
};
},
onCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},
// Pops this function's information.
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Checks the return statement is valid.
ReturnStatement(node) {
if (!funcInfo.shouldCheck) {
return;
}
onUnreachableCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},
funcInfo.hasReturn = true;
onUnreachableCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},
const messageAndSuggestions = { messageId: "", suggest: [] };
onCodePathSegmentStart(segment) {
funcInfo.currentSegments.add(segment);
},
if (funcInfo.arrayMethodName === "forEach") {
// if checkForEach: true, returning a value at any path inside a forEach is not allowed
if (options.checkForEach && node.argument) {
if (options.allowVoid) {
if (isExpressionVoid(node.argument)) {
return;
}
onCodePathSegmentEnd(segment) {
funcInfo.currentSegments.delete(segment);
},
messageAndSuggestions.messageId =
"expectedNoReturnValue";
messageAndSuggestions.suggest = [
{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(
sourceCode,
node.argument,
fixer,
);
},
},
];
} else {
messageAndSuggestions.messageId =
"expectedNoReturnValue";
}
}
} else {
// if allowImplicit: false, should also check node.argument
if (!options.allowImplicit && !node.argument) {
messageAndSuggestions.messageId = "expectedReturnValue";
}
}
if (messageAndSuggestions.messageId) {
context.report({
node,
messageId: messageAndSuggestions.messageId,
data: {
name: astUtils.getFunctionNameWithKind(
funcInfo.node,
),
arrayMethodName: fullMethodName(
funcInfo.arrayMethodName,
),
},
suggest:
messageAndSuggestions.suggest.length !== 0
? messageAndSuggestions.suggest
: null,
});
}
},
// Checks the return statement is valid.
ReturnStatement(node) {
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment,
};
},
if (!funcInfo.shouldCheck) {
return;
}
funcInfo.hasReturn = true;
const messageAndSuggestions = { messageId: "", suggest: [] };
if (funcInfo.arrayMethodName === "forEach") {
// if checkForEach: true, returning a value at any path inside a forEach is not allowed
if (options.checkForEach && node.argument) {
if (options.allowVoid) {
if (isExpressionVoid(node.argument)) {
return;
}
messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(sourceCode, node.argument, fixer);
}
}];
} else {
messageAndSuggestions.messageId = "expectedNoReturnValue";
}
}
} else {
// if allowImplicit: false, should also check node.argument
if (!options.allowImplicit && !node.argument) {
messageAndSuggestions.messageId = "expectedReturnValue";
}
}
if (messageAndSuggestions.messageId) {
context.report({
node,
messageId: messageAndSuggestions.messageId,
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node),
arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
},
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
});
}
},
// Reports a given function if the last path is reachable.
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};
+254 -317
View File
@@ -12,363 +12,300 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "array-element-newline",
url: "https://eslint.style/rules/array-element-newline",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description: "Enforce line breaks after each array element",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-element-newline",
},
docs: {
description: "Enforce line breaks after each array element",
recommended: false,
url: "https://eslint.org/docs/latest/rules/array-element-newline"
},
fixable: "whitespace",
fixable: "whitespace",
schema: {
definitions: {
basicConfig: {
oneOf: [
{
enum: ["always", "never", "consistent"],
},
{
type: "object",
properties: {
multiline: {
type: "boolean",
},
minItems: {
type: ["integer", "null"],
minimum: 0,
},
},
additionalProperties: false,
},
],
},
},
type: "array",
items: [
{
oneOf: [
{
$ref: "#/definitions/basicConfig",
},
{
type: "object",
properties: {
ArrayExpression: {
$ref: "#/definitions/basicConfig",
},
ArrayPattern: {
$ref: "#/definitions/basicConfig",
},
},
additionalProperties: false,
minProperties: 1,
},
],
},
],
},
schema: {
definitions: {
basicConfig: {
oneOf: [
{
enum: ["always", "never", "consistent"]
},
{
type: "object",
properties: {
multiline: {
type: "boolean"
},
minItems: {
type: ["integer", "null"],
minimum: 0
}
},
additionalProperties: false
}
]
}
},
type: "array",
items: [
{
oneOf: [
{
$ref: "#/definitions/basicConfig"
},
{
type: "object",
properties: {
ArrayExpression: {
$ref: "#/definitions/basicConfig"
},
ArrayPattern: {
$ref: "#/definitions/basicConfig"
}
},
additionalProperties: false,
minProperties: 1
}
]
}
]
},
messages: {
unexpectedLineBreak: "There should be no linebreak here.",
missingLineBreak: "There should be a linebreak after this element.",
},
},
messages: {
unexpectedLineBreak: "There should be no linebreak here.",
missingLineBreak: "There should be a linebreak after this element."
}
},
create(context) {
const sourceCode = context.sourceCode;
create(context) {
const sourceCode = context.sourceCode;
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
/**
* Normalizes a given option value.
* @param {string|Object|undefined} providedOption An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(providedOption) {
let consistent = false;
let multiline = false;
let minItems;
/**
* Normalizes a given option value.
* @param {string|Object|undefined} providedOption An option value to parse.
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
*/
function normalizeOptionValue(providedOption) {
let consistent = false;
let multiline = false;
let minItems;
const option = providedOption || "always";
const option = providedOption || "always";
if (!option || option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else if (option === "consistent") {
consistent = true;
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
if (!option || option === "always" || option.minItems === 0) {
minItems = 0;
} else if (option === "never") {
minItems = Number.POSITIVE_INFINITY;
} else if (option === "consistent") {
consistent = true;
minItems = Number.POSITIVE_INFINITY;
} else {
multiline = Boolean(option.multiline);
minItems = option.minItems || Number.POSITIVE_INFINITY;
}
return { consistent, multiline, minItems };
}
return { consistent, multiline, minItems };
}
/**
* Normalizes a given option value.
* @param {string|Object|undefined} options An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
if (options && (options.ArrayExpression || options.ArrayPattern)) {
let expressionOptions, patternOptions;
/**
* Normalizes a given option value.
* @param {string|Object|undefined} options An option value to parse.
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
*/
function normalizeOptions(options) {
if (options && (options.ArrayExpression || options.ArrayPattern)) {
let expressionOptions, patternOptions;
if (options.ArrayExpression) {
expressionOptions = normalizeOptionValue(
options.ArrayExpression,
);
}
if (options.ArrayExpression) {
expressionOptions = normalizeOptionValue(options.ArrayExpression);
}
if (options.ArrayPattern) {
patternOptions = normalizeOptionValue(options.ArrayPattern);
}
if (options.ArrayPattern) {
patternOptions = normalizeOptionValue(options.ArrayPattern);
}
return {
ArrayExpression: expressionOptions,
ArrayPattern: patternOptions,
};
}
return { ArrayExpression: expressionOptions, ArrayPattern: patternOptions };
}
const value = normalizeOptionValue(options);
const value = normalizeOptionValue(options);
return { ArrayExpression: value, ArrayPattern: value };
}
return { ArrayExpression: value, ArrayPattern: value };
}
/**
* Reports that there shouldn't be a line break after the first token
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, {
includeComments: true,
});
/**
* Reports that there shouldn't be a line break after the first token
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportNoLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start,
},
messageId: "unexpectedLineBreak",
fix(fixer) {
if (astUtils.isCommentToken(tokenBefore)) {
return null;
}
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start
},
messageId: "unexpectedLineBreak",
fix(fixer) {
if (astUtils.isCommentToken(tokenBefore)) {
return null;
}
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
return fixer.replaceTextRange(
[tokenBefore.range[1], token.range[0]],
" ",
);
}
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ");
}
/*
* This will check if the comma is on the same line as the next element
* Following array:
* [
* 1
* , 2
* , 3
* ]
*
* will be fixed to:
* [
* 1, 2, 3
* ]
*/
const twoTokensBefore = sourceCode.getTokenBefore(
tokenBefore,
{ includeComments: true },
);
/*
* This will check if the comma is on the same line as the next element
* Following array:
* [
* 1
* , 2
* , 3
* ]
*
* will be fixed to:
* [
* 1, 2, 3
* ]
*/
const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true });
if (astUtils.isCommentToken(twoTokensBefore)) {
return null;
}
if (astUtils.isCommentToken(twoTokensBefore)) {
return null;
}
return fixer.replaceTextRange(
[twoTokensBefore.range[1], tokenBefore.range[0]],
"",
);
},
});
}
return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], "");
/**
* Reports that there should be a line break after the first token
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, {
includeComments: true,
});
}
});
}
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start,
},
messageId: "missingLineBreak",
fix(fixer) {
return fixer.replaceTextRange(
[tokenBefore.range[1], token.range[0]],
"\n",
);
},
});
}
/**
* Reports that there should be a line break after the first token
* @param {Token} token The token to use for the report.
* @returns {void}
*/
function reportRequiredLineBreak(token) {
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
/**
* Reports a given node if it violated this rule.
* @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
context.report({
loc: {
start: tokenBefore.loc.end,
end: token.loc.start
},
messageId: "missingLineBreak",
fix(fixer) {
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n");
}
});
}
if (!options) {
return;
}
/**
* Reports a given node if it violated this rule.
* @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node.
* @returns {void}
*/
function check(node) {
const elements = node.elements;
const normalizedOptions = normalizeOptions(context.options[0]);
const options = normalizedOptions[node.type];
let elementBreak = false;
if (!options) {
return;
}
/*
* MULTILINE: true
* loop through every element and check
* if at least one element has linebreaks inside
* this ensures that following is not valid (due to elements are on the same line):
*
* [
* 1,
* 2,
* 3
* ]
*/
if (options.multiline) {
elementBreak = elements
.filter(element => element !== null)
.some(
element =>
element.loc.start.line !== element.loc.end.line,
);
}
let elementBreak = false;
let linebreaksCount = 0;
/*
* MULTILINE: true
* loop through every element and check
* if at least one element has linebreaks inside
* this ensures that following is not valid (due to elements are on the same line):
*
* [
* 1,
* 2,
* 3
* ]
*/
if (options.multiline) {
elementBreak = elements
.filter(element => element !== null)
.some(element => element.loc.start.line !== element.loc.end.line);
}
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
let linebreaksCount = 0;
const previousElement = elements[i - 1];
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (i === 0 || element === null || previousElement === null) {
continue;
}
const previousElement = elements[i - 1];
const commaToken = sourceCode.getFirstTokenBetween(
previousElement,
element,
astUtils.isCommaToken,
);
const lastTokenOfPreviousElement =
sourceCode.getTokenBefore(commaToken);
const firstTokenOfCurrentElement =
sourceCode.getTokenAfter(commaToken);
if (i === 0 || element === null || previousElement === null) {
continue;
}
if (
!astUtils.isTokenOnSameLine(
lastTokenOfPreviousElement,
firstTokenOfCurrentElement,
)
) {
linebreaksCount++;
}
}
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
const needsLinebreaks =
elements.length >= options.minItems ||
(options.multiline && elementBreak) ||
(options.consistent &&
linebreaksCount > 0 &&
linebreaksCount < node.elements.length);
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
linebreaksCount++;
}
}
elements.forEach((element, i) => {
const previousElement = elements[i - 1];
const needsLinebreaks = (
elements.length >= options.minItems ||
(
options.multiline &&
elementBreak
) ||
(
options.consistent &&
linebreaksCount > 0 &&
linebreaksCount < node.elements.length
)
);
if (i === 0 || element === null || previousElement === null) {
return;
}
elements.forEach((element, i) => {
const previousElement = elements[i - 1];
const commaToken = sourceCode.getFirstTokenBetween(
previousElement,
element,
astUtils.isCommaToken,
);
const lastTokenOfPreviousElement =
sourceCode.getTokenBefore(commaToken);
const firstTokenOfCurrentElement =
sourceCode.getTokenAfter(commaToken);
if (i === 0 || element === null || previousElement === null) {
return;
}
if (needsLinebreaks) {
if (
astUtils.isTokenOnSameLine(
lastTokenOfPreviousElement,
firstTokenOfCurrentElement,
)
) {
reportRequiredLineBreak(firstTokenOfCurrentElement);
}
} else {
if (
!astUtils.isTokenOnSameLine(
lastTokenOfPreviousElement,
firstTokenOfCurrentElement,
)
) {
reportNoLineBreak(firstTokenOfCurrentElement);
}
}
});
}
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
if (needsLinebreaks) {
if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
reportRequiredLineBreak(firstTokenOfCurrentElement);
}
} else {
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
reportNoLineBreak(firstTokenOfCurrentElement);
}
}
});
}
return {
ArrayPattern: check,
ArrayExpression: check,
};
},
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
ArrayPattern: check,
ArrayExpression: check
};
}
};
+246 -368
View File
@@ -14,405 +14,283 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
meta: {
type: "suggestion",
defaultOptions: ["as-needed"],
docs: {
description: "Require braces around arrow function bodies",
recommended: false,
url: "https://eslint.org/docs/latest/rules/arrow-body-style"
},
docs: {
description: "Require braces around arrow function bodies",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/arrow-body-style",
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always", "never"]
}
],
minItems: 0,
maxItems: 1
},
{
type: "array",
items: [
{
enum: ["as-needed"]
},
{
type: "object",
properties: {
requireReturnForObjectLiteral: { type: "boolean" }
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 2
}
]
},
schema: {
anyOf: [
{
type: "array",
items: [
{
enum: ["always", "never"],
},
],
minItems: 0,
maxItems: 1,
},
{
type: "array",
items: [
{
enum: ["as-needed"],
},
{
type: "object",
properties: {
requireReturnForObjectLiteral: {
type: "boolean",
},
},
additionalProperties: false,
},
],
minItems: 0,
maxItems: 2,
},
],
},
fixable: "code",
fixable: "code",
messages: {
unexpectedOtherBlock: "Unexpected block statement surrounding arrow body.",
unexpectedEmptyBlock: "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
unexpectedObjectBlock: "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
unexpectedSingleBlock: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
expectedBlock: "Expected block statement surrounding arrow body."
}
},
messages: {
unexpectedOtherBlock:
"Unexpected block statement surrounding arrow body.",
unexpectedEmptyBlock:
"Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
unexpectedObjectBlock:
"Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
unexpectedSingleBlock:
"Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
expectedBlock: "Expected block statement surrounding arrow body.",
},
},
create(context) {
const options = context.options;
const always = options[0] === "always";
const asNeeded = !options[0] || options[0] === "as-needed";
const never = options[0] === "never";
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
const sourceCode = context.sourceCode;
let funcInfo = null;
create(context) {
const options = context.options;
const always = options[0] === "always";
const asNeeded = options[0] === "as-needed";
const never = options[0] === "never";
const requireReturnForObjectLiteral =
options[1] && options[1].requireReturnForObjectLiteral;
const sourceCode = context.sourceCode;
let funcInfo = null;
/**
* Checks whether the given node has ASI problem or not.
* @param {Token} token The token to check.
* @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
*/
function hasASIProblem(token) {
return token && token.type === "Punctuator" && /^[([/`+-]/u.test(token.value);
}
/**
* Checks whether the given node has ASI problem or not.
* @param {Token} token The token to check.
* @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
*/
function hasASIProblem(token) {
return (
token &&
token.type === "Punctuator" &&
/^[([/`+-]/u.test(token.value)
);
}
/**
* Gets the closing parenthesis by the given node.
* @param {ASTNode} node first node after an opening parenthesis.
* @returns {Token} The found closing parenthesis token.
*/
function findClosingParen(node) {
let nodeToCheck = node;
/**
* Gets the closing parenthesis by the given node.
* @param {ASTNode} node first node after an opening parenthesis.
* @returns {Token} The found closing parenthesis token.
*/
function findClosingParen(node) {
let nodeToCheck = node;
while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
nodeToCheck = nodeToCheck.parent;
}
return sourceCode.getTokenAfter(nodeToCheck);
}
while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
nodeToCheck = nodeToCheck.parent;
}
return sourceCode.getTokenAfter(nodeToCheck);
}
/**
* Check whether the node is inside of a for loop's init
* @param {ASTNode} node node is inside for loop
* @returns {boolean} `true` if the node is inside of a for loop, else `false`
*/
function isInsideForLoopInitializer(node) {
if (node && node.parent) {
if (node.parent.type === "ForStatement" && node.parent.init === node) {
return true;
}
return isInsideForLoopInitializer(node.parent);
}
return false;
}
/**
* Check whether the node is inside of a for loop's init
* @param {ASTNode} node node is inside for loop
* @returns {boolean} `true` if the node is inside of a for loop, else `false`
*/
function isInsideForLoopInitializer(node) {
if (node && node.parent) {
if (
node.parent.type === "ForStatement" &&
node.parent.init === node
) {
return true;
}
return isInsideForLoopInitializer(node.parent);
}
return false;
}
/**
* Determines whether a arrow function body needs braces
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function validate(node) {
const arrowBody = node.body;
/**
* Determines whether a arrow function body needs braces
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function validate(node) {
const arrowBody = node.body;
if (arrowBody.type === "BlockStatement") {
const blockBody = arrowBody.body;
if (arrowBody.type === "BlockStatement") {
const blockBody = arrowBody.body;
if (blockBody.length !== 1 && !never) {
return;
}
if (blockBody.length !== 1 && !never) {
return;
}
if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" &&
blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") {
return;
}
if (
asNeeded &&
requireReturnForObjectLiteral &&
blockBody[0].type === "ReturnStatement" &&
blockBody[0].argument &&
blockBody[0].argument.type === "ObjectExpression"
) {
return;
}
if (never || asNeeded && blockBody[0].type === "ReturnStatement") {
let messageId;
if (
never ||
(asNeeded && blockBody[0].type === "ReturnStatement")
) {
let messageId;
if (blockBody.length === 0) {
messageId = "unexpectedEmptyBlock";
} else if (blockBody.length > 1) {
messageId = "unexpectedOtherBlock";
} else if (blockBody[0].argument === null) {
messageId = "unexpectedSingleBlock";
} else if (astUtils.isOpeningBraceToken(sourceCode.getFirstToken(blockBody[0], { skip: 1 }))) {
messageId = "unexpectedObjectBlock";
} else {
messageId = "unexpectedSingleBlock";
}
if (blockBody.length === 0) {
messageId = "unexpectedEmptyBlock";
} else if (
blockBody.length > 1 ||
blockBody[0].type !== "ReturnStatement"
) {
messageId = "unexpectedOtherBlock";
} else if (blockBody[0].argument === null) {
messageId = "unexpectedSingleBlock";
} else if (
astUtils.isOpeningBraceToken(
sourceCode.getFirstToken(blockBody[0], { skip: 1 }),
)
) {
messageId = "unexpectedObjectBlock";
} else {
messageId = "unexpectedSingleBlock";
}
context.report({
node,
loc: arrowBody.loc,
messageId,
fix(fixer) {
const fixes = [];
context.report({
node,
loc: arrowBody.loc,
messageId,
fix(fixer) {
const fixes = [];
if (blockBody.length !== 1 ||
blockBody[0].type !== "ReturnStatement" ||
!blockBody[0].argument ||
hasASIProblem(sourceCode.getTokenAfter(arrowBody))
) {
return fixes;
}
if (
blockBody.length !== 1 ||
blockBody[0].type !== "ReturnStatement" ||
!blockBody[0].argument ||
hasASIProblem(
sourceCode.getTokenAfter(arrowBody),
)
) {
return fixes;
}
const openingBrace = sourceCode.getFirstToken(arrowBody);
const closingBrace = sourceCode.getLastToken(arrowBody);
const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1);
const lastValueToken = sourceCode.getLastToken(blockBody[0]);
const commentsExist =
sourceCode.commentsExistBetween(openingBrace, firstValueToken) ||
sourceCode.commentsExistBetween(lastValueToken, closingBrace);
const openingBrace =
sourceCode.getFirstToken(arrowBody);
const closingBrace =
sourceCode.getLastToken(arrowBody);
const firstValueToken = sourceCode.getFirstToken(
blockBody[0],
1,
);
const lastValueToken = sourceCode.getLastToken(
blockBody[0],
);
const commentsExist =
sourceCode.commentsExistBetween(
openingBrace,
firstValueToken,
) ||
sourceCode.commentsExistBetween(
lastValueToken,
closingBrace,
);
/*
* Remove tokens around the return value.
* If comments don't exist, remove extra spaces as well.
*/
if (commentsExist) {
fixes.push(
fixer.remove(openingBrace),
fixer.remove(closingBrace),
fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword
);
} else {
fixes.push(
fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
);
}
/*
* Remove tokens around the return value.
* If comments don't exist, remove extra spaces as well.
*/
if (commentsExist) {
fixes.push(
fixer.remove(openingBrace),
fixer.remove(closingBrace),
fixer.remove(
sourceCode.getTokenAfter(openingBrace),
), // return keyword
);
} else {
fixes.push(
fixer.removeRange([
openingBrace.range[0],
firstValueToken.range[0],
]),
fixer.removeRange([
lastValueToken.range[1],
closingBrace.range[1],
]),
);
}
/*
* If the first token of the return value is `{` or the return value is a sequence expression,
* enclose the return value by parentheses to avoid syntax error.
*/
if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) {
if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) {
fixes.push(
fixer.insertTextBefore(firstValueToken, "("),
fixer.insertTextAfter(lastValueToken, ")")
);
}
}
/*
* If the first token of the return value is `{` or the return value is a sequence expression,
* enclose the return value by parentheses to avoid syntax error.
*/
if (
astUtils.isOpeningBraceToken(firstValueToken) ||
blockBody[0].argument.type ===
"SequenceExpression" ||
(funcInfo.hasInOperator &&
isInsideForLoopInitializer(node))
) {
if (
!astUtils.isParenthesised(
sourceCode,
blockBody[0].argument,
)
) {
fixes.push(
fixer.insertTextBefore(
firstValueToken,
"(",
),
fixer.insertTextAfter(
lastValueToken,
")",
),
);
}
}
/*
* If the last token of the return statement is semicolon, remove it.
* Non-block arrow body is an expression, not a statement.
*/
if (astUtils.isSemicolonToken(lastValueToken)) {
fixes.push(fixer.remove(lastValueToken));
}
/*
* If the last token of the return statement is semicolon, remove it.
* Non-block arrow body is an expression, not a statement.
*/
if (astUtils.isSemicolonToken(lastValueToken)) {
fixes.push(fixer.remove(lastValueToken));
}
return fixes;
}
});
}
} else {
if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) {
context.report({
node,
loc: arrowBody.loc,
messageId: "expectedBlock",
fix(fixer) {
const fixes = [];
const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 });
const lastToken = sourceCode.getLastToken(node);
return fixes;
},
});
}
} else {
if (
always ||
(asNeeded &&
requireReturnForObjectLiteral &&
arrowBody.type === "ObjectExpression")
) {
context.report({
node,
loc: arrowBody.loc,
messageId: "expectedBlock",
fix(fixer) {
const fixes = [];
const arrowToken = sourceCode.getTokenBefore(
arrowBody,
astUtils.isArrowToken,
);
const [
firstTokenAfterArrow,
secondTokenAfterArrow,
] = sourceCode.getTokensAfter(arrowToken, {
count: 2,
});
const lastToken = sourceCode.getLastToken(node);
let parenthesisedObjectLiteral = null;
let parenthesisedObjectLiteral = null;
if (
astUtils.isOpeningParenToken(firstTokenAfterArrow) &&
astUtils.isOpeningBraceToken(secondTokenAfterArrow)
) {
const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]);
if (
astUtils.isOpeningParenToken(
firstTokenAfterArrow,
) &&
astUtils.isOpeningBraceToken(
secondTokenAfterArrow,
)
) {
const braceNode =
sourceCode.getNodeByRangeIndex(
secondTokenAfterArrow.range[0],
);
if (braceNode.type === "ObjectExpression") {
parenthesisedObjectLiteral = braceNode;
}
}
if (braceNode.type === "ObjectExpression") {
parenthesisedObjectLiteral = braceNode;
}
}
// If the value is object literal, remove parentheses which were forced by syntax.
if (parenthesisedObjectLiteral) {
const openingParenToken = firstTokenAfterArrow;
const openingBraceToken = secondTokenAfterArrow;
// If the value is object literal, remove parentheses which were forced by syntax.
if (parenthesisedObjectLiteral) {
const openingParenToken = firstTokenAfterArrow;
const openingBraceToken = secondTokenAfterArrow;
if (astUtils.isTokenOnSameLine(openingParenToken, openingBraceToken)) {
fixes.push(fixer.replaceText(openingParenToken, "{return "));
} else {
if (
astUtils.isTokenOnSameLine(
openingParenToken,
openingBraceToken,
)
) {
fixes.push(
fixer.replaceText(
openingParenToken,
"{return ",
),
);
} else {
// Avoid ASI
fixes.push(
fixer.replaceText(
openingParenToken,
"{",
),
fixer.insertTextBefore(
openingBraceToken,
"return ",
),
);
}
// Avoid ASI
fixes.push(
fixer.replaceText(openingParenToken, "{"),
fixer.insertTextBefore(openingBraceToken, "return ")
);
}
// Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
fixes.push(
fixer.remove(
findClosingParen(
parenthesisedObjectLiteral,
),
),
);
fixes.push(
fixer.insertTextAfter(lastToken, "}"),
);
} else {
fixes.push(
fixer.insertTextBefore(
firstTokenAfterArrow,
"{return ",
),
);
fixes.push(
fixer.insertTextAfter(lastToken, "}"),
);
}
// Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral)));
fixes.push(fixer.insertTextAfter(lastToken, "}"));
return fixes;
},
});
}
}
}
} else {
fixes.push(fixer.insertTextBefore(firstTokenAfterArrow, "{return "));
fixes.push(fixer.insertTextAfter(lastToken, "}"));
}
return {
"BinaryExpression[operator='in']"() {
let info = funcInfo;
return fixes;
}
});
}
}
}
while (info) {
info.hasInOperator = true;
info = info.upper;
}
},
ArrowFunctionExpression() {
funcInfo = {
upper: funcInfo,
hasInOperator: false,
};
},
"ArrowFunctionExpression:exit"(node) {
validate(node);
funcInfo = funcInfo.upper;
},
};
},
return {
"BinaryExpression[operator='in']"() {
let info = funcInfo;
while (info) {
info.hasInOperator = true;
info = info.upper;
}
},
ArrowFunctionExpression() {
funcInfo = {
upper: funcInfo,
hasInOperator: false
};
},
"ArrowFunctionExpression:exit"(node) {
validate(node);
funcInfo = funcInfo.upper;
}
};
}
};
+136 -187
View File
@@ -21,217 +21,166 @@ const astUtils = require("./utils/ast-utils");
* @returns {boolean} `true` if the function has block body.
*/
function hasBlockBody(node) {
return node.body.type === "BlockStatement";
return node.body.type === "BlockStatement";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "arrow-parens",
url: "https://eslint.style/rules/arrow-parens",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description: "Require parentheses around arrow function arguments",
recommended: false,
url: "https://eslint.org/docs/latest/rules/arrow-parens",
},
docs: {
description: "Require parentheses around arrow function arguments",
recommended: false,
url: "https://eslint.org/docs/latest/rules/arrow-parens"
},
fixable: "code",
fixable: "code",
schema: [
{
enum: ["always", "as-needed"],
},
{
type: "object",
properties: {
requireForBlockBody: {
type: "boolean",
default: false,
},
},
additionalProperties: false,
},
],
schema: [
{
enum: ["always", "as-needed"]
},
{
type: "object",
properties: {
requireForBlockBody: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
messages: {
unexpectedParens:
"Unexpected parentheses around single function argument.",
expectedParens:
"Expected parentheses around arrow function argument.",
messages: {
unexpectedParens: "Unexpected parentheses around single function argument.",
expectedParens: "Expected parentheses around arrow function argument.",
unexpectedParensInline:
"Unexpected parentheses around single function argument having a body with no curly braces.",
expectedParensBlock:
"Expected parentheses around arrow function argument having a body with curly braces.",
},
},
unexpectedParensInline: "Unexpected parentheses around single function argument having a body with no curly braces.",
expectedParensBlock: "Expected parentheses around arrow function argument having a body with curly braces."
}
},
create(context) {
const asNeeded = context.options[0] === "as-needed";
const requireForBlockBody =
asNeeded &&
context.options[1] &&
context.options[1].requireForBlockBody === true;
create(context) {
const asNeeded = context.options[0] === "as-needed";
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;
const sourceCode = context.sourceCode;
const sourceCode = context.sourceCode;
/**
* Finds opening paren of parameters for the given arrow function, if it exists.
* It is assumed that the given arrow function has exactly one parameter.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @returns {Token|null} the opening paren, or `null` if the given arrow function doesn't have parens of parameters.
*/
function findOpeningParenOfParams(node) {
const tokenBeforeParams = sourceCode.getTokenBefore(node.params[0]);
/**
* Finds opening paren of parameters for the given arrow function, if it exists.
* It is assumed that the given arrow function has exactly one parameter.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @returns {Token|null} the opening paren, or `null` if the given arrow function doesn't have parens of parameters.
*/
function findOpeningParenOfParams(node) {
const tokenBeforeParams = sourceCode.getTokenBefore(node.params[0]);
if (
tokenBeforeParams &&
astUtils.isOpeningParenToken(tokenBeforeParams) &&
node.range[0] <= tokenBeforeParams.range[0]
) {
return tokenBeforeParams;
}
if (
tokenBeforeParams &&
astUtils.isOpeningParenToken(tokenBeforeParams) &&
node.range[0] <= tokenBeforeParams.range[0]
) {
return tokenBeforeParams;
}
return null;
}
return null;
}
/**
* Finds closing paren of parameters for the given arrow function.
* It is assumed that the given arrow function has parens of parameters and that it has exactly one parameter.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @returns {Token} the closing paren of parameters.
*/
function getClosingParenOfParams(node) {
return sourceCode.getTokenAfter(
node.params[0],
astUtils.isClosingParenToken,
);
}
/**
* Finds closing paren of parameters for the given arrow function.
* It is assumed that the given arrow function has parens of parameters and that it has exactly one parameter.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @returns {Token} the closing paren of parameters.
*/
function getClosingParenOfParams(node) {
return sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken);
}
/**
* Determines whether the given arrow function has comments inside parens of parameters.
* It is assumed that the given arrow function has parens of parameters.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @param {Token} openingParen Opening paren of parameters.
* @returns {boolean} `true` if the function has at least one comment inside of parens of parameters.
*/
function hasCommentsInParensOfParams(node, openingParen) {
return sourceCode.commentsExistBetween(
openingParen,
getClosingParenOfParams(node),
);
}
/**
* Determines whether the given arrow function has comments inside parens of parameters.
* It is assumed that the given arrow function has parens of parameters.
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @param {Token} openingParen Opening paren of parameters.
* @returns {boolean} `true` if the function has at least one comment inside of parens of parameters.
*/
function hasCommentsInParensOfParams(node, openingParen) {
return sourceCode.commentsExistBetween(openingParen, getClosingParenOfParams(node));
}
/**
* Determines whether the given arrow function has unexpected tokens before opening paren of parameters,
* in which case it will be assumed that the existing parens of parameters are necessary.
* Only tokens within the range of the arrow function (tokens that are part of the arrow function) are taken into account.
* Example: <T>(a) => b
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @param {Token} openingParen Opening paren of parameters.
* @returns {boolean} `true` if the function has at least one unexpected token.
*/
function hasUnexpectedTokensBeforeOpeningParen(node, openingParen) {
const expectedCount = node.async ? 1 : 0;
/**
* Determines whether the given arrow function has unexpected tokens before opening paren of parameters,
* in which case it will be assumed that the existing parens of parameters are necessary.
* Only tokens within the range of the arrow function (tokens that are part of the arrow function) are taken into account.
* Example: <T>(a) => b
* @param {ASTNode} node `ArrowFunctionExpression` node.
* @param {Token} openingParen Opening paren of parameters.
* @returns {boolean} `true` if the function has at least one unexpected token.
*/
function hasUnexpectedTokensBeforeOpeningParen(node, openingParen) {
const expectedCount = node.async ? 1 : 0;
return (
sourceCode.getFirstToken(node, { skip: expectedCount }) !==
openingParen
);
}
return sourceCode.getFirstToken(node, { skip: expectedCount }) !== openingParen;
}
return {
"ArrowFunctionExpression[params.length=1]"(node) {
const shouldHaveParens =
!asNeeded || (requireForBlockBody && hasBlockBody(node));
const openingParen = findOpeningParenOfParams(node);
const hasParens = openingParen !== null;
const [param] = node.params;
return {
"ArrowFunctionExpression[params.length=1]"(node) {
const shouldHaveParens = !asNeeded || requireForBlockBody && hasBlockBody(node);
const openingParen = findOpeningParenOfParams(node);
const hasParens = openingParen !== null;
const [param] = node.params;
if (shouldHaveParens && !hasParens) {
context.report({
node,
messageId: requireForBlockBody
? "expectedParensBlock"
: "expectedParens",
loc: param.loc,
*fix(fixer) {
yield fixer.insertTextBefore(param, "(");
yield fixer.insertTextAfter(param, ")");
},
});
}
if (shouldHaveParens && !hasParens) {
context.report({
node,
messageId: requireForBlockBody ? "expectedParensBlock" : "expectedParens",
loc: param.loc,
*fix(fixer) {
yield fixer.insertTextBefore(param, "(");
yield fixer.insertTextAfter(param, ")");
}
});
}
if (
!shouldHaveParens &&
hasParens &&
param.type === "Identifier" &&
!param.typeAnnotation &&
!node.returnType &&
!hasCommentsInParensOfParams(node, openingParen) &&
!hasUnexpectedTokensBeforeOpeningParen(node, openingParen)
) {
context.report({
node,
messageId: requireForBlockBody
? "unexpectedParensInline"
: "unexpectedParens",
loc: param.loc,
*fix(fixer) {
const tokenBeforeOpeningParen =
sourceCode.getTokenBefore(openingParen);
const closingParen = getClosingParenOfParams(node);
if (
!shouldHaveParens &&
hasParens &&
param.type === "Identifier" &&
!param.typeAnnotation &&
!node.returnType &&
!hasCommentsInParensOfParams(node, openingParen) &&
!hasUnexpectedTokensBeforeOpeningParen(node, openingParen)
) {
context.report({
node,
messageId: requireForBlockBody ? "unexpectedParensInline" : "unexpectedParens",
loc: param.loc,
*fix(fixer) {
const tokenBeforeOpeningParen = sourceCode.getTokenBefore(openingParen);
const closingParen = getClosingParenOfParams(node);
if (
tokenBeforeOpeningParen &&
tokenBeforeOpeningParen.range[1] ===
openingParen.range[0] &&
!astUtils.canTokensBeAdjacent(
tokenBeforeOpeningParen,
sourceCode.getFirstToken(param),
)
) {
yield fixer.insertTextBefore(openingParen, " ");
}
if (
tokenBeforeOpeningParen &&
tokenBeforeOpeningParen.range[1] === openingParen.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBeforeOpeningParen, sourceCode.getFirstToken(param))
) {
yield fixer.insertTextBefore(openingParen, " ");
}
// remove parens, whitespace inside parens, and possible trailing comma
yield fixer.removeRange([
openingParen.range[0],
param.range[0],
]);
yield fixer.removeRange([
param.range[1],
closingParen.range[1],
]);
},
});
}
},
};
},
// remove parens, whitespace inside parens, and possible trailing comma
yield fixer.removeRange([openingParen.range[0], param.range[0]]);
yield fixer.removeRange([param.range[1], closingParen.range[1]]);
}
});
}
}
};
}
};
+129 -153
View File
@@ -15,174 +15,150 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "arrow-spacing",
url: "https://eslint.style/rules/arrow-spacing",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description:
"Enforce consistent spacing before and after the arrow in arrow functions",
recommended: false,
url: "https://eslint.org/docs/latest/rules/arrow-spacing",
},
docs: {
description: "Enforce consistent spacing before and after the arrow in arrow functions",
recommended: false,
url: "https://eslint.org/docs/latest/rules/arrow-spacing"
},
fixable: "whitespace",
fixable: "whitespace",
schema: [
{
type: "object",
properties: {
before: {
type: "boolean",
default: true,
},
after: {
type: "boolean",
default: true,
},
},
additionalProperties: false,
},
],
schema: [
{
type: "object",
properties: {
before: {
type: "boolean",
default: true
},
after: {
type: "boolean",
default: true
}
},
additionalProperties: false
}
],
messages: {
expectedBefore: "Missing space before =>.",
unexpectedBefore: "Unexpected space before =>.",
messages: {
expectedBefore: "Missing space before =>.",
unexpectedBefore: "Unexpected space before =>.",
expectedAfter: "Missing space after =>.",
unexpectedAfter: "Unexpected space after =>.",
},
},
expectedAfter: "Missing space after =>.",
unexpectedAfter: "Unexpected space after =>."
}
},
create(context) {
// merge rules with default
const rule = Object.assign({}, context.options[0]);
create(context) {
rule.before = rule.before !== false;
rule.after = rule.after !== false;
// merge rules with default
const rule = Object.assign({}, context.options[0]);
const sourceCode = context.sourceCode;
rule.before = rule.before !== false;
rule.after = rule.after !== false;
/**
* Get tokens of arrow(`=>`) and before/after arrow.
* @param {ASTNode} node The arrow function node.
* @returns {Object} Tokens of arrow and before/after arrow.
*/
function getTokens(node) {
const arrow = sourceCode.getTokenBefore(
node.body,
astUtils.isArrowToken,
);
const sourceCode = context.sourceCode;
return {
before: sourceCode.getTokenBefore(arrow),
arrow,
after: sourceCode.getTokenAfter(arrow),
};
}
/**
* Get tokens of arrow(`=>`) and before/after arrow.
* @param {ASTNode} node The arrow function node.
* @returns {Object} Tokens of arrow and before/after arrow.
*/
function getTokens(node) {
const arrow = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
/**
* Count spaces before/after arrow(`=>`) token.
* @param {Object} tokens Tokens before/after arrow.
* @returns {Object} count of space before/after arrow.
*/
function countSpaces(tokens) {
const before = tokens.arrow.range[0] - tokens.before.range[1];
const after = tokens.after.range[0] - tokens.arrow.range[1];
return {
before: sourceCode.getTokenBefore(arrow),
arrow,
after: sourceCode.getTokenAfter(arrow)
};
}
return { before, after };
}
/**
* Count spaces before/after arrow(`=>`) token.
* @param {Object} tokens Tokens before/after arrow.
* @returns {Object} count of space before/after arrow.
*/
function countSpaces(tokens) {
const before = tokens.arrow.range[0] - tokens.before.range[1];
const after = tokens.after.range[0] - tokens.arrow.range[1];
/**
* Determines whether space(s) before after arrow(`=>`) is satisfy rule.
* if before/after value is `true`, there should be space(s).
* if before/after value is `false`, there should be no space.
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function spaces(node) {
const tokens = getTokens(node);
const countSpace = countSpaces(tokens);
return { before, after };
}
if (rule.before) {
// should be space(s) before arrow
if (countSpace.before === 0) {
context.report({
node: tokens.before,
messageId: "expectedBefore",
fix(fixer) {
return fixer.insertTextBefore(tokens.arrow, " ");
},
});
}
} else {
// should be no space before arrow
if (countSpace.before > 0) {
context.report({
node: tokens.before,
messageId: "unexpectedBefore",
fix(fixer) {
return fixer.removeRange([
tokens.before.range[1],
tokens.arrow.range[0],
]);
},
});
}
}
/**
* Determines whether space(s) before after arrow(`=>`) is satisfy rule.
* if before/after value is `true`, there should be space(s).
* if before/after value is `false`, there should be no space.
* @param {ASTNode} node The arrow function node.
* @returns {void}
*/
function spaces(node) {
const tokens = getTokens(node);
const countSpace = countSpaces(tokens);
if (rule.after) {
// should be space(s) after arrow
if (countSpace.after === 0) {
context.report({
node: tokens.after,
messageId: "expectedAfter",
fix(fixer) {
return fixer.insertTextAfter(tokens.arrow, " ");
},
});
}
} else {
// should be no space after arrow
if (countSpace.after > 0) {
context.report({
node: tokens.after,
messageId: "unexpectedAfter",
fix(fixer) {
return fixer.removeRange([
tokens.arrow.range[1],
tokens.after.range[0],
]);
},
});
}
}
}
if (rule.before) {
return {
ArrowFunctionExpression: spaces,
};
},
// should be space(s) before arrow
if (countSpace.before === 0) {
context.report({
node: tokens.before,
messageId: "expectedBefore",
fix(fixer) {
return fixer.insertTextBefore(tokens.arrow, " ");
}
});
}
} else {
// should be no space before arrow
if (countSpace.before > 0) {
context.report({
node: tokens.before,
messageId: "unexpectedBefore",
fix(fixer) {
return fixer.removeRange([tokens.before.range[1], tokens.arrow.range[0]]);
}
});
}
}
if (rule.after) {
// should be space(s) after arrow
if (countSpace.after === 0) {
context.report({
node: tokens.after,
messageId: "expectedAfter",
fix(fixer) {
return fixer.insertTextAfter(tokens.arrow, " ");
}
});
}
} else {
// should be no space after arrow
if (countSpace.after > 0) {
context.report({
node: tokens.after,
messageId: "unexpectedAfter",
fix(fixer) {
return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]);
}
});
}
}
}
return {
ArrowFunctionExpression: spaces
};
}
};
+106 -108
View File
@@ -8,130 +8,128 @@
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
meta: {
type: "suggestion",
docs: {
description:
"Enforce the use of variables within the scope they are defined",
recommended: false,
url: "https://eslint.org/docs/latest/rules/block-scoped-var",
},
docs: {
description: "Enforce the use of variables within the scope they are defined",
recommended: false,
url: "https://eslint.org/docs/latest/rules/block-scoped-var"
},
schema: [],
schema: [],
messages: {
outOfScope:
"'{{name}}' declared on line {{definitionLine}} column {{definitionColumn}} is used outside of binding context.",
},
},
messages: {
outOfScope: "'{{name}}' declared on line {{definitionLine}} column {{definitionColumn}} is used outside of binding context."
}
},
create(context) {
let stack = [];
const sourceCode = context.sourceCode;
create(context) {
let stack = [];
const sourceCode = context.sourceCode;
/**
* Makes a block scope.
* @param {ASTNode} node A node of a scope.
* @returns {void}
*/
function enterScope(node) {
stack.push(node.range);
}
/**
* Makes a block scope.
* @param {ASTNode} node A node of a scope.
* @returns {void}
*/
function enterScope(node) {
stack.push(node.range);
}
/**
* Pops the last block scope.
* @returns {void}
*/
function exitScope() {
stack.pop();
}
/**
* Pops the last block scope.
* @returns {void}
*/
function exitScope() {
stack.pop();
}
/**
* Reports a given reference.
* @param {eslint-scope.Reference} reference A reference to report.
* @param {eslint-scope.Definition} definition A definition for which to report reference.
* @returns {void}
*/
function report(reference, definition) {
const identifier = reference.identifier;
const definitionPosition = definition.name.loc.start;
/**
* Reports a given reference.
* @param {eslint-scope.Reference} reference A reference to report.
* @param {eslint-scope.Definition} definition A definition for which to report reference.
* @returns {void}
*/
function report(reference, definition) {
const identifier = reference.identifier;
const definitionPosition = definition.name.loc.start;
context.report({
node: identifier,
messageId: "outOfScope",
data: {
name: identifier.name,
definitionLine: definitionPosition.line,
definitionColumn: definitionPosition.column + 1,
},
});
}
context.report({
node: identifier,
messageId: "outOfScope",
data: {
name: identifier.name,
definitionLine: definitionPosition.line,
definitionColumn: definitionPosition.column + 1
}
});
}
/**
* Finds and reports references which are outside of valid scopes.
* @param {ASTNode} node A node to get variables.
* @returns {void}
*/
function checkForVariables(node) {
if (node.kind !== "var") {
return;
}
/**
* Finds and reports references which are outside of valid scopes.
* @param {ASTNode} node A node to get variables.
* @returns {void}
*/
function checkForVariables(node) {
if (node.kind !== "var") {
return;
}
// Defines a predicate to check whether or not a given reference is outside of valid scope.
const scopeRange = stack.at(-1);
// Defines a predicate to check whether or not a given reference is outside of valid scope.
const scopeRange = stack[stack.length - 1];
/**
* Check if a reference is out of scope
* @param {ASTNode} reference node to examine
* @returns {boolean} True is its outside the scope
* @private
*/
function isOutsideOfScope(reference) {
const idRange = reference.identifier.range;
/**
* Check if a reference is out of scope
* @param {ASTNode} reference node to examine
* @returns {boolean} True is its outside the scope
* @private
*/
function isOutsideOfScope(reference) {
const idRange = reference.identifier.range;
return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1];
}
return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1];
}
// Gets declared variables, and checks its references.
const variables = sourceCode.getDeclaredVariables(node);
// Gets declared variables, and checks its references.
const variables = sourceCode.getDeclaredVariables(node);
for (let i = 0; i < variables.length; ++i) {
// Reports.
variables[i].references.filter(isOutsideOfScope).forEach(ref =>
report(
ref,
variables[i].defs.find(def => def.parent === node),
),
);
}
}
for (let i = 0; i < variables.length; ++i) {
return {
Program(node) {
stack = [node.range];
},
// Reports.
variables[i]
.references
.filter(isOutsideOfScope)
.forEach(ref => report(ref, variables[i].defs.find(def => def.parent === node)));
}
}
// Manages scopes.
BlockStatement: enterScope,
"BlockStatement:exit": exitScope,
ForStatement: enterScope,
"ForStatement:exit": exitScope,
ForInStatement: enterScope,
"ForInStatement:exit": exitScope,
ForOfStatement: enterScope,
"ForOfStatement:exit": exitScope,
SwitchStatement: enterScope,
"SwitchStatement:exit": exitScope,
CatchClause: enterScope,
"CatchClause:exit": exitScope,
StaticBlock: enterScope,
"StaticBlock:exit": exitScope,
return {
Program(node) {
stack = [node.range];
},
// Finds and reports references which are outside of valid scope.
VariableDeclaration: checkForVariables,
};
},
// Manages scopes.
BlockStatement: enterScope,
"BlockStatement:exit": exitScope,
ForStatement: enterScope,
"ForStatement:exit": exitScope,
ForInStatement: enterScope,
"ForInStatement:exit": exitScope,
ForOfStatement: enterScope,
"ForOfStatement:exit": exitScope,
SwitchStatement: enterScope,
"SwitchStatement:exit": exitScope,
CatchClause: enterScope,
"CatchClause:exit": exitScope,
StaticBlock: enterScope,
"StaticBlock:exit": exitScope,
// Finds and reports references which are outside of valid scope.
VariableDeclaration: checkForVariables
};
}
};
+139 -167
View File
@@ -12,191 +12,163 @@ const util = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "block-spacing",
url: "https://eslint.style/rules/block-spacing",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description:
"Disallow or enforce spaces inside of blocks after opening block and before closing block",
recommended: false,
url: "https://eslint.org/docs/latest/rules/block-spacing",
},
docs: {
description: "Disallow or enforce spaces inside of blocks after opening block and before closing block",
recommended: false,
url: "https://eslint.org/docs/latest/rules/block-spacing"
},
fixable: "whitespace",
fixable: "whitespace",
schema: [{ enum: ["always", "never"] }],
schema: [
{ enum: ["always", "never"] }
],
messages: {
missing: "Requires a space {{location}} '{{token}}'.",
extra: "Unexpected space(s) {{location}} '{{token}}'.",
},
},
messages: {
missing: "Requires a space {{location}} '{{token}}'.",
extra: "Unexpected space(s) {{location}} '{{token}}'."
}
},
create(context) {
const always = context.options[0] !== "never",
messageId = always ? "missing" : "extra",
sourceCode = context.sourceCode;
create(context) {
const always = (context.options[0] !== "never"),
messageId = always ? "missing" : "extra",
sourceCode = context.sourceCode;
/**
* Gets the open brace token from a given node.
* @param {ASTNode} node A BlockStatement/StaticBlock/SwitchStatement node to get.
* @returns {Token} The token of the open brace.
*/
function getOpenBrace(node) {
if (node.type === "SwitchStatement") {
if (node.cases.length > 0) {
return sourceCode.getTokenBefore(node.cases[0]);
}
return sourceCode.getLastToken(node, 1);
}
/**
* Gets the open brace token from a given node.
* @param {ASTNode} node A BlockStatement/StaticBlock/SwitchStatement node to get.
* @returns {Token} The token of the open brace.
*/
function getOpenBrace(node) {
if (node.type === "SwitchStatement") {
if (node.cases.length > 0) {
return sourceCode.getTokenBefore(node.cases[0]);
}
return sourceCode.getLastToken(node, 1);
}
if (node.type === "StaticBlock") {
return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
}
if (node.type === "StaticBlock") {
return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
}
// "BlockStatement"
return sourceCode.getFirstToken(node);
}
// "BlockStatement"
return sourceCode.getFirstToken(node);
}
/**
* Checks whether or not:
* - given tokens are on same line.
* - there is/isn't a space between given tokens.
* @param {Token} left A token to check.
* @param {Token} right The token which is next to `left`.
* @returns {boolean}
* When the option is `"always"`, `true` if there are one or more spaces between given tokens.
* When the option is `"never"`, `true` if there are not any spaces between given tokens.
* If given tokens are not on same line, it's always `true`.
*/
function isValid(left, right) {
return (
!util.isTokenOnSameLine(left, right) ||
sourceCode.isSpaceBetweenTokens(left, right) === always
);
}
/**
* Checks whether or not:
* - given tokens are on same line.
* - there is/isn't a space between given tokens.
* @param {Token} left A token to check.
* @param {Token} right The token which is next to `left`.
* @returns {boolean}
* When the option is `"always"`, `true` if there are one or more spaces between given tokens.
* When the option is `"never"`, `true` if there are not any spaces between given tokens.
* If given tokens are not on same line, it's always `true`.
*/
function isValid(left, right) {
return (
!util.isTokenOnSameLine(left, right) ||
sourceCode.isSpaceBetweenTokens(left, right) === always
);
}
/**
* Checks and reports invalid spacing style inside braces.
* @param {ASTNode} node A BlockStatement/StaticBlock/SwitchStatement node to check.
* @returns {void}
*/
function checkSpacingInsideBraces(node) {
// Gets braces and the first/last token of content.
const openBrace = getOpenBrace(node);
const closeBrace = sourceCode.getLastToken(node);
const firstToken = sourceCode.getTokenAfter(openBrace, {
includeComments: true,
});
const lastToken = sourceCode.getTokenBefore(closeBrace, {
includeComments: true,
});
/**
* Checks and reports invalid spacing style inside braces.
* @param {ASTNode} node A BlockStatement/StaticBlock/SwitchStatement node to check.
* @returns {void}
*/
function checkSpacingInsideBraces(node) {
// Skip if the node is invalid or empty.
if (
openBrace.type !== "Punctuator" ||
openBrace.value !== "{" ||
closeBrace.type !== "Punctuator" ||
closeBrace.value !== "}" ||
firstToken === closeBrace
) {
return;
}
// Gets braces and the first/last token of content.
const openBrace = getOpenBrace(node);
const closeBrace = sourceCode.getLastToken(node);
const firstToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
const lastToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
// Skip line comments for option never
if (!always && firstToken.type === "Line") {
return;
}
// Skip if the node is invalid or empty.
if (openBrace.type !== "Punctuator" ||
openBrace.value !== "{" ||
closeBrace.type !== "Punctuator" ||
closeBrace.value !== "}" ||
firstToken === closeBrace
) {
return;
}
// Check.
if (!isValid(openBrace, firstToken)) {
let loc = openBrace.loc;
// Skip line comments for option never
if (!always && firstToken.type === "Line") {
return;
}
if (messageId === "extra") {
loc = {
start: openBrace.loc.end,
end: firstToken.loc.start,
};
}
// Check.
if (!isValid(openBrace, firstToken)) {
let loc = openBrace.loc;
context.report({
node,
loc,
messageId,
data: {
location: "after",
token: openBrace.value,
},
fix(fixer) {
if (always) {
return fixer.insertTextBefore(firstToken, " ");
}
if (messageId === "extra") {
loc = {
start: openBrace.loc.end,
end: firstToken.loc.start
};
}
return fixer.removeRange([
openBrace.range[1],
firstToken.range[0],
]);
},
});
}
if (!isValid(lastToken, closeBrace)) {
let loc = closeBrace.loc;
context.report({
node,
loc,
messageId,
data: {
location: "after",
token: openBrace.value
},
fix(fixer) {
if (always) {
return fixer.insertTextBefore(firstToken, " ");
}
if (messageId === "extra") {
loc = {
start: lastToken.loc.end,
end: closeBrace.loc.start,
};
}
context.report({
node,
loc,
messageId,
data: {
location: "before",
token: closeBrace.value,
},
fix(fixer) {
if (always) {
return fixer.insertTextAfter(lastToken, " ");
}
return fixer.removeRange([openBrace.range[1], firstToken.range[0]]);
}
});
}
if (!isValid(lastToken, closeBrace)) {
let loc = closeBrace.loc;
return fixer.removeRange([
lastToken.range[1],
closeBrace.range[0],
]);
},
});
}
}
if (messageId === "extra") {
loc = {
start: lastToken.loc.end,
end: closeBrace.loc.start
};
}
context.report({
node,
loc,
messageId,
data: {
location: "before",
token: closeBrace.value
},
fix(fixer) {
if (always) {
return fixer.insertTextAfter(lastToken, " ");
}
return {
BlockStatement: checkSpacingInsideBraces,
StaticBlock: checkSpacingInsideBraces,
SwitchStatement: checkSpacingInsideBraces,
};
},
return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]);
}
});
}
}
return {
BlockStatement: checkSpacingInsideBraces,
StaticBlock: checkSpacingInsideBraces,
SwitchStatement: checkSpacingInsideBraces
};
}
};
+161 -242
View File
@@ -12,267 +12,186 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "brace-style",
url: "https://eslint.style/rules/brace-style",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description: "Enforce consistent brace style for blocks",
recommended: false,
url: "https://eslint.org/docs/latest/rules/brace-style",
},
docs: {
description: "Enforce consistent brace style for blocks",
recommended: false,
url: "https://eslint.org/docs/latest/rules/brace-style"
},
schema: [
{
enum: ["1tbs", "stroustrup", "allman"],
},
{
type: "object",
properties: {
allowSingleLine: {
type: "boolean",
default: false,
},
},
additionalProperties: false,
},
],
schema: [
{
enum: ["1tbs", "stroustrup", "allman"]
},
{
type: "object",
properties: {
allowSingleLine: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
fixable: "whitespace",
fixable: "whitespace",
messages: {
nextLineOpen:
"Opening curly brace does not appear on the same line as controlling statement.",
sameLineOpen:
"Opening curly brace appears on the same line as controlling statement.",
blockSameLine:
"Statement inside of curly braces should be on next line.",
nextLineClose:
"Closing curly brace does not appear on the same line as the subsequent block.",
singleLineClose:
"Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
sameLineClose:
"Closing curly brace appears on the same line as the subsequent block.",
},
},
messages: {
nextLineOpen: "Opening curly brace does not appear on the same line as controlling statement.",
sameLineOpen: "Opening curly brace appears on the same line as controlling statement.",
blockSameLine: "Statement inside of curly braces should be on next line.",
nextLineClose: "Closing curly brace does not appear on the same line as the subsequent block.",
singleLineClose: "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
sameLineClose: "Closing curly brace appears on the same line as the subsequent block."
}
},
create(context) {
const style = context.options[0] || "1tbs",
params = context.options[1] || {},
sourceCode = context.sourceCode;
create(context) {
const style = context.options[0] || "1tbs",
params = context.options[1] || {},
sourceCode = context.sourceCode;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/**
* Fixes a place where a newline unexpectedly appears
* @param {Token} firstToken The token before the unexpected newline
* @param {Token} secondToken The token after the unexpected newline
* @returns {Function} A fixer function to remove the newlines between the tokens
*/
function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]];
const textBetween = sourceCode.text.slice(
textRange[0],
textRange[1],
);
/**
* Fixes a place where a newline unexpectedly appears
* @param {Token} firstToken The token before the unexpected newline
* @param {Token} secondToken The token after the unexpected newline
* @returns {Function} A fixer function to remove the newlines between the tokens
*/
function removeNewlineBetween(firstToken, secondToken) {
const textRange = [firstToken.range[1], secondToken.range[0]];
const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
// Don't do a fix if there is a comment between the tokens
if (textBetween.trim()) {
return null;
}
return fixer => fixer.replaceTextRange(textRange, " ");
}
// Don't do a fix if there is a comment between the tokens
if (textBetween.trim()) {
return null;
}
return fixer => fixer.replaceTextRange(textRange, " ");
}
/**
* Validates a pair of curly brackets based on the user's config
* @param {Token} openingCurly The opening curly bracket
* @param {Token} closingCurly The closing curly bracket
* @returns {void}
*/
function validateCurlyPair(openingCurly, closingCurly) {
const tokenBeforeOpeningCurly =
sourceCode.getTokenBefore(openingCurly);
const tokenAfterOpeningCurly =
sourceCode.getTokenAfter(openingCurly);
const tokenBeforeClosingCurly =
sourceCode.getTokenBefore(closingCurly);
const singleLineException =
params.allowSingleLine &&
astUtils.isTokenOnSameLine(openingCurly, closingCurly);
/**
* Validates a pair of curly brackets based on the user's config
* @param {Token} openingCurly The opening curly bracket
* @param {Token} closingCurly The closing curly bracket
* @returns {void}
*/
function validateCurlyPair(openingCurly, closingCurly) {
const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly);
const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly);
const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly);
const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly);
if (
style !== "allman" &&
!astUtils.isTokenOnSameLine(
tokenBeforeOpeningCurly,
openingCurly,
)
) {
context.report({
node: openingCurly,
messageId: "nextLineOpen",
fix: removeNewlineBetween(
tokenBeforeOpeningCurly,
openingCurly,
),
});
}
if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) {
context.report({
node: openingCurly,
messageId: "nextLineOpen",
fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
});
}
if (
style === "allman" &&
astUtils.isTokenOnSameLine(
tokenBeforeOpeningCurly,
openingCurly,
) &&
!singleLineException
) {
context.report({
node: openingCurly,
messageId: "sameLineOpen",
fix: fixer => fixer.insertTextBefore(openingCurly, "\n"),
});
}
if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
context.report({
node: openingCurly,
messageId: "sameLineOpen",
fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
});
}
if (
astUtils.isTokenOnSameLine(
openingCurly,
tokenAfterOpeningCurly,
) &&
tokenAfterOpeningCurly !== closingCurly &&
!singleLineException
) {
context.report({
node: openingCurly,
messageId: "blockSameLine",
fix: fixer => fixer.insertTextAfter(openingCurly, "\n"),
});
}
if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
context.report({
node: openingCurly,
messageId: "blockSameLine",
fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
});
}
if (
tokenBeforeClosingCurly !== openingCurly &&
!singleLineException &&
astUtils.isTokenOnSameLine(
tokenBeforeClosingCurly,
closingCurly,
)
) {
context.report({
node: closingCurly,
messageId: "singleLineClose",
fix: fixer => fixer.insertTextBefore(closingCurly, "\n"),
});
}
}
if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
context.report({
node: closingCurly,
messageId: "singleLineClose",
fix: fixer => fixer.insertTextBefore(closingCurly, "\n")
});
}
}
/**
* Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
* @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
* @returns {void}
*/
function validateCurlyBeforeKeyword(curlyToken) {
const keywordToken = sourceCode.getTokenAfter(curlyToken);
/**
* Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
* @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
* @returns {void}
*/
function validateCurlyBeforeKeyword(curlyToken) {
const keywordToken = sourceCode.getTokenAfter(curlyToken);
if (
style === "1tbs" &&
!astUtils.isTokenOnSameLine(curlyToken, keywordToken)
) {
context.report({
node: curlyToken,
messageId: "nextLineClose",
fix: removeNewlineBetween(curlyToken, keywordToken),
});
}
if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
messageId: "nextLineClose",
fix: removeNewlineBetween(curlyToken, keywordToken)
});
}
if (
style !== "1tbs" &&
astUtils.isTokenOnSameLine(curlyToken, keywordToken)
) {
context.report({
node: curlyToken,
messageId: "sameLineClose",
fix: fixer => fixer.insertTextAfter(curlyToken, "\n"),
});
}
}
if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
context.report({
node: curlyToken,
messageId: "sameLineClose",
fix: fixer => fixer.insertTextAfter(curlyToken, "\n")
});
}
}
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
return {
BlockStatement(node) {
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
validateCurlyPair(
sourceCode.getFirstToken(node),
sourceCode.getLastToken(node),
);
}
},
StaticBlock(node) {
validateCurlyPair(
sourceCode.getFirstToken(node, { skip: 1 }), // skip the `static` token
sourceCode.getLastToken(node),
);
},
ClassBody(node) {
validateCurlyPair(
sourceCode.getFirstToken(node),
sourceCode.getLastToken(node),
);
},
SwitchStatement(node) {
const closingCurly = sourceCode.getLastToken(node);
const openingCurly = sourceCode.getTokenBefore(
node.cases.length ? node.cases[0] : closingCurly,
);
return {
BlockStatement(node) {
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
}
},
StaticBlock(node) {
validateCurlyPair(
sourceCode.getFirstToken(node, { skip: 1 }), // skip the `static` token
sourceCode.getLastToken(node)
);
},
ClassBody(node) {
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
},
SwitchStatement(node) {
const closingCurly = sourceCode.getLastToken(node);
const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly);
validateCurlyPair(openingCurly, closingCurly);
},
IfStatement(node) {
if (
node.consequent.type === "BlockStatement" &&
node.alternate
) {
// Handle the keyword after the `if` block (before `else`)
validateCurlyBeforeKeyword(
sourceCode.getLastToken(node.consequent),
);
}
},
TryStatement(node) {
// Handle the keyword after the `try` block (before `catch` or `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
validateCurlyPair(openingCurly, closingCurly);
},
IfStatement(node) {
if (node.consequent.type === "BlockStatement" && node.alternate) {
if (node.handler && node.finalizer) {
// Handle the keyword after the `catch` block (before `finally`)
validateCurlyBeforeKeyword(
sourceCode.getLastToken(node.handler.body),
);
}
},
};
},
// Handle the keyword after the `if` block (before `else`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent));
}
},
TryStatement(node) {
// Handle the keyword after the `try` block (before `catch` or `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
if (node.handler && node.finalizer) {
// Handle the keyword after the `catch` block (before `finally`)
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body));
}
}
};
}
};
+149 -178
View File
@@ -9,208 +9,179 @@
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Node.js rules were moved out of ESLint core.",
url: "https://eslint.org/docs/latest/use/migrating-to-7.0.0#deprecate-node-rules",
deprecatedSince: "7.0.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"eslint-plugin-n now maintains deprecated Node.js-related rules.",
plugin: {
name: "eslint-plugin-n",
url: "https://github.com/eslint-community/eslint-plugin-n",
},
rule: {
name: "callback-return",
url: "https://github.com/eslint-community/eslint-plugin-n/tree/master/docs/rules/callback-return.md",
},
},
],
},
meta: {
deprecated: true,
type: "suggestion",
replacedBy: [],
docs: {
description: "Require `return` statements after callbacks",
recommended: false,
url: "https://eslint.org/docs/latest/rules/callback-return",
},
type: "suggestion",
schema: [
{
type: "array",
items: { type: "string" },
},
],
docs: {
description: "Require `return` statements after callbacks",
recommended: false,
url: "https://eslint.org/docs/latest/rules/callback-return"
},
messages: {
missingReturn: "Expected return with your callback function.",
},
},
schema: [{
type: "array",
items: { type: "string" }
}],
create(context) {
const callbacks = context.options[0] || ["callback", "cb", "next"],
sourceCode = context.sourceCode;
messages: {
missingReturn: "Expected return with your callback function."
}
},
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
create(context) {
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null;
}
if (!types.includes(node.parent.type)) {
return findClosestParentOfType(node.parent, types);
}
return node.parent;
}
const callbacks = context.options[0] || ["callback", "cb", "next"],
sourceCode = context.sourceCode;
/**
* Check to see if a node contains only identifiers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifiers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true;
}
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true;
}
if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object);
}
}
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null;
}
if (!types.includes(node.parent.type)) {
return findClosestParentOfType(node.parent, types);
}
return node.parent;
}
return false;
}
/**
* Check to see if a node contains only identifiers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifiers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true;
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return (
containsOnlyIdentifiers(node.callee) &&
callbacks.includes(sourceCode.getText(node.callee))
);
}
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true;
}
if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object);
}
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
// ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false;
}
return false;
}
// cb()
if (parentNode.expression === node) {
return true;
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return containsOnlyIdentifiers(node.callee) && callbacks.includes(sourceCode.getText(node.callee));
}
// special case for cb && cb() and similar
if (
parentNode.expression.type === "BinaryExpression" ||
parentNode.expression.type === "LogicalExpression"
) {
if (parentNode.expression.right === node) {
return true;
}
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
return false;
}
// ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
// cb()
if (parentNode.expression === node) {
return true;
}
return {
CallExpression(node) {
// if we're not a callback we can return
if (!isCallback(node)) {
return;
}
// special case for cb && cb() and similar
if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
if (parentNode.expression.right === node) {
return true;
}
}
// find the closest block, return or loop
const closestBlock =
findClosestParentOfType(node, [
"BlockStatement",
"ReturnStatement",
"ArrowFunctionExpression",
]) || {};
return false;
}
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return;
}
return {
CallExpression(node) {
// block statements are part of functions and most if statements
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem = closestBlock.body.at(-1);
// if we're not a callback we can return
if (!isCallback(node)) {
return;
}
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type;
// find the closest block, return or loop
const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {};
// but only if the block is part of a function
if (
parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return;
}
}
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return;
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (
isCallbackExpression(node, closestBlock.body.at(-2))
) {
return;
}
}
}
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return;
}
// as long as you're the child of a function at this point you should be asked to return
if (
findClosestParentOfType(node, [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
])
) {
context.report({ node, messageId: "missingReturn" });
}
},
};
},
// block statements are part of functions and most if statements
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem = closestBlock.body[closestBlock.body.length - 1];
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type;
// but only if the block is part of a function
if (parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return;
}
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
return;
}
}
}
// as long as you're the child of a function at this point you should be asked to return
if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
context.report({ node, messageId: "missingReturn" });
}
}
};
}
};
+339 -362
View File
@@ -15,408 +15,385 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
meta: {
type: "suggestion",
defaultOptions: [
{
allow: [],
ignoreDestructuring: false,
ignoreGlobals: false,
ignoreImports: false,
properties: "always",
},
],
docs: {
description: "Enforce camelcase naming convention",
recommended: false,
url: "https://eslint.org/docs/latest/rules/camelcase"
},
docs: {
description: "Enforce camelcase naming convention",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/camelcase",
},
schema: [
{
type: "object",
properties: {
ignoreDestructuring: {
type: "boolean",
default: false
},
ignoreImports: {
type: "boolean",
default: false
},
ignoreGlobals: {
type: "boolean",
default: false
},
properties: {
enum: ["always", "never"]
},
allow: {
type: "array",
items: [
{
type: "string"
}
],
minItems: 0,
uniqueItems: true
}
},
additionalProperties: false
}
],
schema: [
{
type: "object",
properties: {
ignoreDestructuring: {
type: "boolean",
},
ignoreImports: {
type: "boolean",
},
ignoreGlobals: {
type: "boolean",
},
properties: {
enum: ["always", "never"],
},
allow: {
type: "array",
items: {
type: "string",
},
minItems: 0,
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
notCamelCase: "Identifier '{{name}}' is not in camel case.",
notCamelCasePrivate: "#{{name}} is not in camel case."
}
},
messages: {
notCamelCase: "Identifier '{{name}}' is not in camel case.",
notCamelCasePrivate: "#{{name}} is not in camel case.",
},
},
create(context) {
const options = context.options[0] || {};
const properties = options.properties === "never" ? "never" : "always";
const ignoreDestructuring = options.ignoreDestructuring;
const ignoreImports = options.ignoreImports;
const ignoreGlobals = options.ignoreGlobals;
const allow = options.allow || [];
const sourceCode = context.sourceCode;
create(context) {
const [
{
allow,
ignoreDestructuring,
ignoreGlobals,
ignoreImports,
properties,
},
] = context.options;
const sourceCode = context.sourceCode;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
const reported = new Set();
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
const reported = new Set();
/**
* Checks if a string contains an underscore and isn't all upper-case
* @param {string} name The string to check.
* @returns {boolean} if the string is underscored
* @private
*/
function isUnderscored(name) {
const nameBody = name.replace(/^_+|_+$/gu, "");
/**
* Checks if a string contains an underscore and isn't all upper-case
* @param {string} name The string to check.
* @returns {boolean} if the string is underscored
* @private
*/
function isUnderscored(name) {
const nameBody = name.replace(/^_+|_+$/gu, "");
// if there's an underscore, it might be A_CONSTANT, which is okay
return nameBody.includes("_") && nameBody !== nameBody.toUpperCase();
}
// if there's an underscore, it might be A_CONSTANT, which is okay
return (
nameBody.includes("_") && nameBody !== nameBody.toUpperCase()
);
}
/**
* Checks if a string match the ignore list
* @param {string} name The string to check.
* @returns {boolean} if the string is ignored
* @private
*/
function isAllowed(name) {
return allow.some(
entry => name === entry || name.match(new RegExp(entry, "u"))
);
}
/**
* Checks if a string match the ignore list
* @param {string} name The string to check.
* @returns {boolean} if the string is ignored
* @private
*/
function isAllowed(name) {
return allow.some(
entry => name === entry || name.match(new RegExp(entry, "u")),
);
}
/**
* Checks if a given name is good or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is good.
* @private
*/
function isGoodName(name) {
return !isUnderscored(name) || isAllowed(name);
}
/**
* Checks if a given name is good or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is good.
* @private
*/
function isGoodName(name) {
return !isUnderscored(name) || isAllowed(name);
}
/**
* Checks if a given identifier reference or member expression is an assignment
* target.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is an assignment target.
*/
function isAssignmentTarget(node) {
const parent = node.parent;
/**
* Checks if a given identifier reference or member expression is an assignment
* target.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is an assignment target.
*/
function isAssignmentTarget(node) {
const parent = node.parent;
switch (parent.type) {
case "AssignmentExpression":
case "AssignmentPattern":
return parent.left === node;
switch (parent.type) {
case "AssignmentExpression":
case "AssignmentPattern":
return parent.left === node;
case "Property":
return (
parent.parent.type === "ObjectPattern" &&
parent.value === node
);
case "ArrayPattern":
case "RestElement":
return true;
case "Property":
return (
parent.parent.type === "ObjectPattern" &&
parent.value === node
);
case "ArrayPattern":
case "RestElement":
return true;
default:
return false;
}
}
default:
return false;
}
}
/**
* Checks if a given binding identifier uses the original name as-is.
* - If it's in object destructuring or object expression, the original name is its property name.
* - If it's in import declaration, the original name is its exported name.
* @param {ASTNode} node The `Identifier` node to check.
* @returns {boolean} `true` if the identifier uses the original name as-is.
*/
function equalsToOriginalName(node) {
const localName = node.name;
const valueNode = node.parent.type === "AssignmentPattern"
? node.parent
: node;
const parent = valueNode.parent;
/**
* Checks if a given binding identifier uses the original name as-is.
* - If it's in object destructuring or object expression, the original name is its property name.
* - If it's in import declaration, the original name is its exported name.
* @param {ASTNode} node The `Identifier` node to check.
* @returns {boolean} `true` if the identifier uses the original name as-is.
*/
function equalsToOriginalName(node) {
const localName = node.name;
const valueNode =
node.parent.type === "AssignmentPattern" ? node.parent : node;
const parent = valueNode.parent;
switch (parent.type) {
case "Property":
return (
(parent.parent.type === "ObjectPattern" || parent.parent.type === "ObjectExpression") &&
parent.value === valueNode &&
!parent.computed &&
parent.key.type === "Identifier" &&
parent.key.name === localName
);
switch (parent.type) {
case "Property":
return (
(parent.parent.type === "ObjectPattern" ||
parent.parent.type === "ObjectExpression") &&
parent.value === valueNode &&
!parent.computed &&
parent.key.type === "Identifier" &&
parent.key.name === localName
);
case "ImportSpecifier":
return (
parent.local === node &&
astUtils.getModuleExportName(parent.imported) === localName
);
case "ImportSpecifier":
return (
parent.local === node &&
astUtils.getModuleExportName(parent.imported) ===
localName
);
default:
return false;
}
}
default:
return false;
}
}
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
if (reported.has(node.range[0])) {
return;
}
reported.add(node.range[0]);
/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @returns {void}
* @private
*/
function report(node) {
if (reported.has(node.range[0])) {
return;
}
reported.add(node.range[0]);
// Report it.
context.report({
node,
messageId: node.type === "PrivateIdentifier"
? "notCamelCasePrivate"
: "notCamelCase",
data: { name: node.name }
});
}
// Report it.
context.report({
node,
messageId:
node.type === "PrivateIdentifier"
? "notCamelCasePrivate"
: "notCamelCase",
data: { name: node.name },
});
}
/**
* Reports an identifier reference or a binding identifier.
* @param {ASTNode} node The `Identifier` node to report.
* @returns {void}
*/
function reportReferenceId(node) {
/**
* Reports an identifier reference or a binding identifier.
* @param {ASTNode} node The `Identifier` node to report.
* @returns {void}
*/
function reportReferenceId(node) {
/*
* For backward compatibility, if it's in callings then ignore it.
* Not sure why it is.
*/
if (
node.parent.type === "CallExpression" ||
node.parent.type === "NewExpression"
) {
return;
}
/*
* For backward compatibility, if it's in callings then ignore it.
* Not sure why it is.
*/
if (
node.parent.type === "CallExpression" ||
node.parent.type === "NewExpression"
) {
return;
}
/*
* For backward compatibility, if it's a default value of
* destructuring/parameters then ignore it.
* Not sure why it is.
*/
if (
node.parent.type === "AssignmentPattern" &&
node.parent.right === node
) {
return;
}
/*
* For backward compatibility, if it's a default value of
* destructuring/parameters then ignore it.
* Not sure why it is.
*/
if (
node.parent.type === "AssignmentPattern" &&
node.parent.right === node
) {
return;
}
/*
* The `ignoreDestructuring` flag skips the identifiers that uses
* the property name as-is.
*/
if (ignoreDestructuring && equalsToOriginalName(node)) {
return;
}
/*
* The `ignoreDestructuring` flag skips the identifiers that uses
* the property name as-is.
*/
if (ignoreDestructuring && equalsToOriginalName(node)) {
return;
}
/*
* Import attribute keys are always ignored
*/
if (astUtils.isImportAttributeKey(node)) {
return;
}
report(node);
}
report(node);
}
return {
return {
// Report camelcase of global variable references ------------------
Program(node) {
const scope = sourceCode.getScope(node);
// Report camelcase of global variable references ------------------
Program(node) {
const scope = sourceCode.getScope(node);
if (!ignoreGlobals) {
// Defined globals in config files or directive comments.
for (const variable of scope.variables) {
if (
variable.identifiers.length > 0 ||
isGoodName(variable.name)
) {
continue;
}
for (const reference of variable.references) {
/*
* For backward compatibility, this rule reports read-only
* references as well.
*/
reportReferenceId(reference.identifier);
}
}
}
if (!ignoreGlobals) {
// Undefined globals.
for (const reference of scope.through) {
const id = reference.identifier;
// Defined globals in config files or directive comments.
for (const variable of scope.variables) {
if (
variable.identifiers.length > 0 ||
isGoodName(variable.name)
) {
continue;
}
for (const reference of variable.references) {
if (
isGoodName(id.name) ||
astUtils.isImportAttributeKey(id)
) {
continue;
}
/*
* For backward compatibility, this rule reports read-only
* references as well.
*/
reportReferenceId(reference.identifier);
}
}
}
/*
* For backward compatibility, this rule reports read-only
* references as well.
*/
reportReferenceId(id);
}
},
// Undefined globals.
for (const reference of scope.through) {
const id = reference.identifier;
// Report camelcase of declared variables --------------------------
[[
"VariableDeclaration",
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"CatchClause",
]](node) {
for (const variable of sourceCode.getDeclaredVariables(node)) {
if (isGoodName(variable.name)) {
continue;
}
const id = variable.identifiers[0];
if (isGoodName(id.name)) {
continue;
}
// Report declaration.
if (!(ignoreDestructuring && equalsToOriginalName(id))) {
report(id);
}
/*
* For backward compatibility, this rule reports read-only
* references as well.
*/
reportReferenceId(id);
}
},
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
for (const reference of variable.references) {
if (reference.init) {
continue; // Skip the write references of initializers.
}
reportReferenceId(reference.identifier);
}
}
},
// Report camelcase of declared variables --------------------------
[[
"VariableDeclaration",
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"CatchClause"
]](node) {
for (const variable of sourceCode.getDeclaredVariables(node)) {
if (isGoodName(variable.name)) {
continue;
}
const id = variable.identifiers[0];
// Report camelcase in properties ----------------------------------
[[
"ObjectExpression > Property[computed!=true] > Identifier.key",
"MethodDefinition[computed!=true] > Identifier.key",
"PropertyDefinition[computed!=true] > Identifier.key",
"MethodDefinition > PrivateIdentifier.key",
"PropertyDefinition > PrivateIdentifier.key",
]](node) {
if (
properties === "never" ||
astUtils.isImportAttributeKey(node) ||
isGoodName(node.name)
) {
return;
}
report(node);
},
"MemberExpression[computed!=true] > Identifier.property"(node) {
if (
properties === "never" ||
!isAssignmentTarget(node.parent) || // ← ignore read-only references.
isGoodName(node.name)
) {
return;
}
report(node);
},
// Report declaration.
if (!(ignoreDestructuring && equalsToOriginalName(id))) {
report(id);
}
// Report camelcase in import --------------------------------------
ImportDeclaration(node) {
for (const variable of sourceCode.getDeclaredVariables(node)) {
if (isGoodName(variable.name)) {
continue;
}
const id = variable.identifiers[0];
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
for (const reference of variable.references) {
if (reference.init) {
continue; // Skip the write references of initializers.
}
reportReferenceId(reference.identifier);
}
}
},
// Report declaration.
if (!(ignoreImports && equalsToOriginalName(id))) {
report(id);
}
// Report camelcase in properties ----------------------------------
[[
"ObjectExpression > Property[computed!=true] > Identifier.key",
"MethodDefinition[computed!=true] > Identifier.key",
"PropertyDefinition[computed!=true] > Identifier.key",
"MethodDefinition > PrivateIdentifier.key",
"PropertyDefinition > PrivateIdentifier.key"
]](node) {
if (properties === "never" || isGoodName(node.name)) {
return;
}
report(node);
},
"MemberExpression[computed!=true] > Identifier.property"(node) {
if (
properties === "never" ||
!isAssignmentTarget(node.parent) || // ← ignore read-only references.
isGoodName(node.name)
) {
return;
}
report(node);
},
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
for (const reference of variable.references) {
reportReferenceId(reference.identifier);
}
}
},
// Report camelcase in import --------------------------------------
ImportDeclaration(node) {
for (const variable of sourceCode.getDeclaredVariables(node)) {
if (isGoodName(variable.name)) {
continue;
}
const id = variable.identifiers[0];
// Report camelcase in re-export -----------------------------------
[[
"ExportAllDeclaration > Identifier.exported",
"ExportSpecifier > Identifier.exported",
]](node) {
if (isGoodName(node.name)) {
return;
}
report(node);
},
// Report declaration.
if (!(ignoreImports && equalsToOriginalName(id))) {
report(id);
}
// Report camelcase in labels --------------------------------------
[[
"LabeledStatement > Identifier.label",
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
for (const reference of variable.references) {
reportReferenceId(reference.identifier);
}
}
},
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
"BreakStatement > Identifier.label",
"ContinueStatement > Identifier.label",
]](node) {
if (isGoodName(node.name)) {
return;
}
report(node);
},
};
},
// Report camelcase in re-export -----------------------------------
[[
"ExportAllDeclaration > Identifier.exported",
"ExportSpecifier > Identifier.exported"
]](node) {
if (isGoodName(node.name)) {
return;
}
report(node);
},
// Report camelcase in labels --------------------------------------
[[
"LabeledStatement > Identifier.label",
/*
* For backward compatibility, report references as well.
* It looks unnecessary because declarations are reported.
*/
"BreakStatement > Identifier.label",
"ContinueStatement > Identifier.label"
]](node) {
if (isGoodName(node.name)) {
return;
}
report(node);
}
};
}
};
+196 -221
View File
@@ -8,6 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("./utils/patterns/letters");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
@@ -15,9 +16,8 @@ const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/gu,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u, // TODO: Combine w/ max-len pattern?
LETTER_PATTERN = /\p{L}/u;
WHITESPACE = /\s/gu,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
/*
* Base schema body for defining the basic capitalization rule, ignorePattern,
@@ -25,24 +25,24 @@ const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
* This can be used in a few different ways in the actual schema.
*/
const SCHEMA_BODY = {
type: "object",
properties: {
ignorePattern: {
type: "string",
},
ignoreInlineComments: {
type: "boolean",
},
ignoreConsecutiveComments: {
type: "boolean",
},
},
additionalProperties: false,
type: "object",
properties: {
ignorePattern: {
type: "string"
},
ignoreInlineComments: {
type: "boolean"
},
ignoreConsecutiveComments: {
type: "boolean"
}
},
additionalProperties: false
};
const DEFAULTS = {
ignorePattern: "",
ignoreInlineComments: false,
ignoreConsecutiveComments: false,
ignorePattern: "",
ignoreInlineComments: false,
ignoreConsecutiveComments: false
};
/**
@@ -59,7 +59,7 @@ const DEFAULTS = {
* @returns {Object} The normalized options.
*/
function getNormalizedOptions(rawOptions, which) {
return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
}
/**
@@ -69,10 +69,10 @@ function getNormalizedOptions(rawOptions, which) {
* normalized options objects.
*/
function getAllNormalizedOptions(rawOptions = {}) {
return {
Line: getNormalizedOptions(rawOptions, "line"),
Block: getNormalizedOptions(rawOptions, "block"),
};
return {
Line: getNormalizedOptions(rawOptions, "line"),
Block: getNormalizedOptions(rawOptions, "block")
};
}
/**
@@ -84,242 +84,217 @@ function getAllNormalizedOptions(rawOptions = {}) {
* @returns {void}
*/
function createRegExpForIgnorePatterns(normalizedOptions) {
Object.keys(normalizedOptions).forEach(key => {
const ignorePatternStr = normalizedOptions[key].ignorePattern;
Object.keys(normalizedOptions).forEach(key => {
const ignorePatternStr = normalizedOptions[key].ignorePattern;
if (ignorePatternStr) {
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
if (ignorePatternStr) {
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
normalizedOptions[key].ignorePatternRegExp = regExp;
}
});
normalizedOptions[key].ignorePatternRegExp = regExp;
}
});
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
meta: {
type: "suggestion",
docs: {
description:
"Enforce or disallow capitalization of the first letter of a comment",
recommended: false,
frozen: true,
url: "https://eslint.org/docs/latest/rules/capitalized-comments",
},
docs: {
description: "Enforce or disallow capitalization of the first letter of a comment",
recommended: false,
url: "https://eslint.org/docs/latest/rules/capitalized-comments"
},
fixable: "code",
fixable: "code",
schema: [
{ enum: ["always", "never"] },
{
oneOf: [
SCHEMA_BODY,
{
type: "object",
properties: {
line: SCHEMA_BODY,
block: SCHEMA_BODY,
},
additionalProperties: false,
},
],
},
],
schema: [
{ enum: ["always", "never"] },
{
oneOf: [
SCHEMA_BODY,
{
type: "object",
properties: {
line: SCHEMA_BODY,
block: SCHEMA_BODY
},
additionalProperties: false
}
]
}
],
messages: {
unexpectedLowercaseComment:
"Comments should not begin with a lowercase character.",
unexpectedUppercaseComment:
"Comments should not begin with an uppercase character.",
},
},
messages: {
unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
}
},
create(context) {
const capitalize = context.options[0] || "always",
normalizedOptions = getAllNormalizedOptions(context.options[1]),
sourceCode = context.sourceCode;
create(context) {
createRegExpForIgnorePatterns(normalizedOptions);
const capitalize = context.options[0] || "always",
normalizedOptions = getAllNormalizedOptions(context.options[1]),
sourceCode = context.sourceCode;
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
createRegExpForIgnorePatterns(normalizedOptions);
/**
* Checks whether a comment is an inline comment.
*
* For the purpose of this rule, a comment is inline if:
* 1. The comment is preceded by a token on the same line; and
* 2. The command is followed by a token on the same line.
*
* Note that the comment itself need not be single-line!
*
* Also, it follows from this definition that only block comments can
* be considered as possibly inline. This is because line comments
* would consume any following tokens on the same line as the comment.
* @param {ASTNode} comment The comment node to check.
* @returns {boolean} True if the comment is an inline comment, false
* otherwise.
*/
function isInlineComment(comment) {
const previousToken = sourceCode.getTokenBefore(comment, {
includeComments: true,
}),
nextToken = sourceCode.getTokenAfter(comment, {
includeComments: true,
});
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
return Boolean(
previousToken &&
nextToken &&
comment.loc.start.line === previousToken.loc.end.line &&
comment.loc.end.line === nextToken.loc.start.line,
);
}
/**
* Checks whether a comment is an inline comment.
*
* For the purpose of this rule, a comment is inline if:
* 1. The comment is preceded by a token on the same line; and
* 2. The command is followed by a token on the same line.
*
* Note that the comment itself need not be single-line!
*
* Also, it follows from this definition that only block comments can
* be considered as possibly inline. This is because line comments
* would consume any following tokens on the same line as the comment.
* @param {ASTNode} comment The comment node to check.
* @returns {boolean} True if the comment is an inline comment, false
* otherwise.
*/
function isInlineComment(comment) {
const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
/**
* Determine if a comment follows another comment.
* @param {ASTNode} comment The comment to check.
* @returns {boolean} True if the comment follows a valid comment.
*/
function isConsecutiveComment(comment) {
const previousTokenOrComment = sourceCode.getTokenBefore(comment, {
includeComments: true,
});
return Boolean(
previousToken &&
nextToken &&
comment.loc.start.line === previousToken.loc.end.line &&
comment.loc.end.line === nextToken.loc.start.line
);
}
return Boolean(
previousTokenOrComment &&
["Block", "Line"].includes(previousTokenOrComment.type),
);
}
/**
* Determine if a comment follows another comment.
* @param {ASTNode} comment The comment to check.
* @returns {boolean} True if the comment follows a valid comment.
*/
function isConsecutiveComment(comment) {
const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
/**
* Check a comment to determine if it is valid for this rule.
* @param {ASTNode} comment The comment node to process.
* @param {Object} options The options for checking this comment.
* @returns {boolean} True if the comment is valid, false otherwise.
*/
function isCommentValid(comment, options) {
// 1. Check for default ignore pattern.
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
return true;
}
return Boolean(
previousTokenOrComment &&
["Block", "Line"].includes(previousTokenOrComment.type)
);
}
// 2. Check for custom ignore pattern.
const commentWithoutAsterisks = comment.value.replace(/\*/gu, "");
/**
* Check a comment to determine if it is valid for this rule.
* @param {ASTNode} comment The comment node to process.
* @param {Object} options The options for checking this comment.
* @returns {boolean} True if the comment is valid, false otherwise.
*/
function isCommentValid(comment, options) {
if (
options.ignorePatternRegExp &&
options.ignorePatternRegExp.test(commentWithoutAsterisks)
) {
return true;
}
// 1. Check for default ignore pattern.
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
return true;
}
// 3. Check for inline comments.
if (options.ignoreInlineComments && isInlineComment(comment)) {
return true;
}
// 2. Check for custom ignore pattern.
const commentWithoutAsterisks = comment.value
.replace(/\*/gu, "");
// 4. Is this a consecutive comment (and are we tolerating those)?
if (
options.ignoreConsecutiveComments &&
isConsecutiveComment(comment)
) {
return true;
}
if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
return true;
}
// 5. Does the comment start with a possible URL?
if (MAYBE_URL.test(commentWithoutAsterisks)) {
return true;
}
// 3. Check for inline comments.
if (options.ignoreInlineComments && isInlineComment(comment)) {
return true;
}
// 6. Is the initial word character a letter?
const commentWordCharsOnly = commentWithoutAsterisks.replace(
WHITESPACE,
"",
);
// 4. Is this a consecutive comment (and are we tolerating those)?
if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
return true;
}
if (commentWordCharsOnly.length === 0) {
return true;
}
// 5. Does the comment start with a possible URL?
if (MAYBE_URL.test(commentWithoutAsterisks)) {
return true;
}
// Get the first Unicode character (1 or 2 code units).
const [firstWordChar] = commentWordCharsOnly;
// 6. Is the initial word character a letter?
const commentWordCharsOnly = commentWithoutAsterisks
.replace(WHITESPACE, "");
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
}
if (commentWordCharsOnly.length === 0) {
return true;
}
// 7. Check the case of the initial word character.
const isUppercase =
firstWordChar !== firstWordChar.toLocaleLowerCase(),
isLowercase =
firstWordChar !== firstWordChar.toLocaleUpperCase();
const firstWordChar = commentWordCharsOnly[0];
if (capitalize === "always" && isLowercase) {
return false;
}
if (capitalize === "never" && isUppercase) {
return false;
}
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
}
return true;
}
// 7. Check the case of the initial word character.
const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
/**
* Process a comment to determine if it needs to be reported.
* @param {ASTNode} comment The comment node to process.
* @returns {void}
*/
function processComment(comment) {
const options = normalizedOptions[comment.type],
commentValid = isCommentValid(comment, options);
if (capitalize === "always" && isLowercase) {
return false;
}
if (capitalize === "never" && isUppercase) {
return false;
}
if (!commentValid) {
const messageId =
capitalize === "always"
? "unexpectedLowercaseComment"
: "unexpectedUppercaseComment";
return true;
}
context.report({
node: null, // Intentionally using loc instead
loc: comment.loc,
messageId,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
const char = match[0];
/**
* Process a comment to determine if it needs to be reported.
* @param {ASTNode} comment The comment node to process.
* @returns {void}
*/
function processComment(comment) {
const options = normalizedOptions[comment.type],
commentValid = isCommentValid(comment, options);
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
const charIndex = comment.range[0] + match.index + 2;
if (!commentValid) {
const messageId = capitalize === "always"
? "unexpectedLowercaseComment"
: "unexpectedUppercaseComment";
return fixer.replaceTextRange(
[charIndex, charIndex + char.length],
capitalize === "always"
? char.toLocaleUpperCase()
: char.toLocaleLowerCase(),
);
},
});
}
}
context.report({
node: null, // Intentionally using loc instead
loc: comment.loc,
messageId,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return fixer.replaceTextRange(
return {
Program() {
const comments = sourceCode.getAllComments();
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
);
}
});
}
}
comments
.filter(token => token.type !== "Shebang")
.forEach(processComment);
},
};
},
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
Program() {
const comments = sourceCode.getAllComments();
comments.filter(token => token.type !== "Shebang").forEach(processComment);
}
};
}
};
+150 -213
View File
@@ -15,236 +15,173 @@ const astUtils = require("./utils/ast-utils");
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
dialects: ["javascript", "typescript"],
language: "javascript",
type: "suggestion",
meta: {
type: "suggestion",
defaultOptions: [
{
enforceForClassFields: true,
exceptMethods: [],
ignoreOverrideMethods: false,
},
],
docs: {
description: "Enforce that class methods utilize `this`",
recommended: false,
url: "https://eslint.org/docs/latest/rules/class-methods-use-this"
},
docs: {
description: "Enforce that class methods utilize `this`",
recommended: false,
url: "https://eslint.org/docs/latest/rules/class-methods-use-this",
},
schema: [{
type: "object",
properties: {
exceptMethods: {
type: "array",
items: {
type: "string"
}
},
enforceForClassFields: {
type: "boolean",
default: true
}
},
additionalProperties: false
}],
schema: [
{
type: "object",
properties: {
exceptMethods: {
type: "array",
items: {
type: "string",
},
},
enforceForClassFields: {
type: "boolean",
},
ignoreOverrideMethods: {
type: "boolean",
},
ignoreClassesWithImplements: {
enum: ["all", "public-fields"],
},
},
additionalProperties: false,
},
],
messages: {
missingThis: "Expected 'this' to be used by class {{name}}."
}
},
create(context) {
const config = Object.assign({}, context.options[0]);
const enforceForClassFields = config.enforceForClassFields !== false;
const exceptMethods = new Set(config.exceptMethods || []);
messages: {
missingThis: "Expected 'this' to be used by class {{name}}.",
},
},
create(context) {
const [options] = context.options;
const {
enforceForClassFields,
ignoreOverrideMethods,
ignoreClassesWithImplements,
} = options;
const exceptMethods = new Set(options.exceptMethods);
const stack = [];
const stack = [];
/**
* Push `this` used flag initialized with `false` onto the stack.
* @returns {void}
*/
function pushContext() {
stack.push(false);
}
/**
* Push `this` used flag initialized with `false` onto the stack.
* @returns {void}
*/
function pushContext() {
stack.push(false);
}
/**
* Pop `this` used flag from the stack.
* @returns {boolean | undefined} `this` used flag
*/
function popContext() {
return stack.pop();
}
/**
* Pop `this` used flag from the stack.
* @returns {boolean | undefined} `this` used flag
*/
function popContext() {
return stack.pop();
}
/**
* Initializes the current context to false and pushes it onto the stack.
* These booleans represent whether 'this' has been used in the context.
* @returns {void}
* @private
*/
function enterFunction() {
pushContext();
}
/**
* Initializes the current context to false and pushes it onto the stack.
* These booleans represent whether 'this' has been used in the context.
* @returns {void}
* @private
*/
function enterFunction() {
pushContext();
}
/**
* Check if the node is an instance method
* @param {ASTNode} node node to check
* @returns {boolean} True if its an instance method
* @private
*/
function isInstanceMethod(node) {
switch (node.type) {
case "MethodDefinition":
return !node.static && node.kind !== "constructor";
case "PropertyDefinition":
return !node.static && enforceForClassFields;
default:
return false;
}
}
/**
* Check if the node is an instance method
* @param {ASTNode} node node to check
* @returns {boolean} True if its an instance method
* @private
*/
function isInstanceMethod(node) {
switch (node.type) {
case "MethodDefinition":
return !node.static && node.kind !== "constructor";
case "AccessorProperty":
case "PropertyDefinition":
return !node.static && enforceForClassFields;
default:
return false;
}
}
/**
* Check if the node is an instance method not excluded by config
* @param {ASTNode} node node to check
* @returns {boolean} True if it is an instance method, and not excluded by config
* @private
*/
function isIncludedInstanceMethod(node) {
if (isInstanceMethod(node)) {
if (node.computed) {
return true;
}
/**
* Check if the node's parent class implements any interfaces
* @param {ASTNode} node node to check
* @returns {boolean} True if parent class implements interfaces
* @private
*/
function hasImplements(node) {
const classNode = node.parent.parent;
return (
classNode?.type === "ClassDeclaration" &&
classNode.implements?.length > 0
);
}
const hashIfNeeded = node.key.type === "PrivateIdentifier" ? "#" : "";
const name = node.key.type === "Literal"
? astUtils.getStaticStringValue(node.key)
: (node.key.name || "");
/**
* Check if the node is an instance method not excluded by config
* @param {ASTNode} node node to check
* @returns {boolean} True if it is an instance method, and not excluded by config
* @private
*/
function isIncludedInstanceMethod(node) {
if (isInstanceMethod(node)) {
if (node.computed) {
return true;
}
return !exceptMethods.has(hashIfNeeded + name);
}
return false;
}
if (ignoreOverrideMethods && node.override) {
return false;
}
/**
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
* Static methods and the constructor are exempt.
* Then pops the context off the stack.
* @param {ASTNode} node A function node that was entered.
* @returns {void}
* @private
*/
function exitFunction(node) {
const methodUsesThis = popContext();
if (ignoreClassesWithImplements) {
const implementsInterfaces = hasImplements(node);
if (implementsInterfaces) {
if (
ignoreClassesWithImplements === "all" ||
(ignoreClassesWithImplements === "public-fields" &&
node.key.type !== "PrivateIdentifier" &&
(!node.accessibility ||
node.accessibility === "public"))
) {
return false;
}
}
}
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
context.report({
node,
loc: astUtils.getFunctionHeadLoc(node, context.sourceCode),
messageId: "missingThis",
data: {
name: astUtils.getFunctionNameWithKind(node)
}
});
}
}
const hashIfNeeded =
node.key.type === "PrivateIdentifier" ? "#" : "";
const name =
node.key.type === "Literal"
? astUtils.getStaticStringValue(node.key)
: node.key.name || "";
/**
* Mark the current context as having used 'this'.
* @returns {void}
* @private
*/
function markThisUsed() {
if (stack.length) {
stack[stack.length - 1] = true;
}
}
return !exceptMethods.has(hashIfNeeded + name);
}
return false;
}
return {
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
/**
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
* Static methods and the constructor are exempt.
* Then pops the context off the stack.
* @param {ASTNode} node A function node that was entered.
* @returns {void}
* @private
*/
function exitFunction(node) {
const methodUsesThis = popContext();
/*
* Class field value are implicit functions.
*/
"PropertyDefinition > *.key:exit": pushContext,
"PropertyDefinition:exit": popContext,
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
context.report({
node,
loc: astUtils.getFunctionHeadLoc(node, context.sourceCode),
messageId: "missingThis",
data: {
name: astUtils.getFunctionNameWithKind(node),
},
});
}
}
/*
* Class static blocks are implicit functions. They aren't required to use `this`,
* but we have to push context so that it captures any use of `this` in the static block
* separately from enclosing contexts, because static blocks have their own `this` and it
* shouldn't count as used `this` in enclosing contexts.
*/
StaticBlock: pushContext,
"StaticBlock:exit": popContext,
/**
* Mark the current context as having used 'this'.
* @returns {void}
* @private
*/
function markThisUsed() {
if (stack.length) {
stack[stack.length - 1] = true;
}
}
return {
FunctionDeclaration: enterFunction,
"FunctionDeclaration:exit": exitFunction,
FunctionExpression: enterFunction,
"FunctionExpression:exit": exitFunction,
/*
* Class field value are implicit functions.
*/
"AccessorProperty > *.key:exit": pushContext,
"AccessorProperty:exit": popContext,
"PropertyDefinition > *.key:exit": pushContext,
"PropertyDefinition:exit": popContext,
/*
* Class static blocks are implicit functions. They aren't required to use `this`,
* but we have to push context so that it captures any use of `this` in the static block
* separately from enclosing contexts, because static blocks have their own `this` and it
* shouldn't count as used `this` in enclosing contexts.
*/
StaticBlock: pushContext,
"StaticBlock:exit": popContext,
ThisExpression: markThisUsed,
Super: markThisUsed,
...(enforceForClassFields && {
"AccessorProperty > ArrowFunctionExpression.value":
enterFunction,
"AccessorProperty > ArrowFunctionExpression.value:exit":
exitFunction,
"PropertyDefinition > ArrowFunctionExpression.value":
enterFunction,
"PropertyDefinition > ArrowFunctionExpression.value:exit":
exitFunction,
}),
};
},
ThisExpression: markThisUsed,
Super: markThisUsed,
...(
enforceForClassFields && {
"PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
"PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction
}
)
};
}
};
+295 -346
View File
@@ -17,11 +17,11 @@ const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
const DEFAULT_OPTIONS = Object.freeze({
arrays: "never",
objects: "never",
imports: "never",
exports: "never",
functions: "never",
arrays: "never",
objects: "never",
imports: "never",
exports: "never",
functions: "never"
});
/**
@@ -31,11 +31,11 @@ const DEFAULT_OPTIONS = Object.freeze({
* @returns {boolean} `true` if a trailing comma is allowed.
*/
function isTrailingCommaAllowed(lastItem) {
return !(
lastItem.type === "RestElement" ||
lastItem.type === "RestProperty" ||
lastItem.type === "ExperimentalRestProperty"
);
return !(
lastItem.type === "RestElement" ||
lastItem.type === "RestProperty" ||
lastItem.type === "ExperimentalRestProperty"
);
}
/**
@@ -45,380 +45,329 @@ function isTrailingCommaAllowed(lastItem) {
* @returns {Object} The normalized option value.
*/
function normalizeOptions(optionValue, ecmaVersion) {
if (typeof optionValue === "string") {
return {
arrays: optionValue,
objects: optionValue,
imports: optionValue,
exports: optionValue,
functions: ecmaVersion < 2017 ? "ignore" : optionValue,
};
}
if (typeof optionValue === "object" && optionValue !== null) {
return {
arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
objects: optionValue.objects || DEFAULT_OPTIONS.objects,
imports: optionValue.imports || DEFAULT_OPTIONS.imports,
exports: optionValue.exports || DEFAULT_OPTIONS.exports,
functions: optionValue.functions || DEFAULT_OPTIONS.functions,
};
}
if (typeof optionValue === "string") {
return {
arrays: optionValue,
objects: optionValue,
imports: optionValue,
exports: optionValue,
functions: ecmaVersion < 2017 ? "ignore" : optionValue
};
}
if (typeof optionValue === "object" && optionValue !== null) {
return {
arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
objects: optionValue.objects || DEFAULT_OPTIONS.objects,
imports: optionValue.imports || DEFAULT_OPTIONS.imports,
exports: optionValue.exports || DEFAULT_OPTIONS.exports,
functions: optionValue.functions || DEFAULT_OPTIONS.functions
};
}
return DEFAULT_OPTIONS;
return DEFAULT_OPTIONS;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
deprecated: {
message: "Formatting rules are being moved out of ESLint core.",
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
deprecatedSince: "8.53.0",
availableUntil: "11.0.0",
replacedBy: [
{
message:
"ESLint Stylistic now maintains deprecated stylistic core rules.",
url: "https://eslint.style/guide/migration",
plugin: {
name: "@stylistic/eslint-plugin",
url: "https://eslint.style",
},
rule: {
name: "comma-dangle",
url: "https://eslint.style/rules/comma-dangle",
},
},
],
},
type: "layout",
meta: {
deprecated: true,
replacedBy: [],
type: "layout",
docs: {
description: "Require or disallow trailing commas",
recommended: false,
url: "https://eslint.org/docs/latest/rules/comma-dangle",
},
docs: {
description: "Require or disallow trailing commas",
recommended: false,
url: "https://eslint.org/docs/latest/rules/comma-dangle"
},
fixable: "code",
fixable: "code",
schema: {
definitions: {
value: {
enum: [
"always-multiline",
"always",
"never",
"only-multiline",
],
},
valueWithIgnore: {
enum: [
"always-multiline",
"always",
"ignore",
"never",
"only-multiline",
],
},
},
type: "array",
items: [
{
oneOf: [
{
$ref: "#/definitions/value",
},
{
type: "object",
properties: {
arrays: {
$ref: "#/definitions/valueWithIgnore",
},
objects: {
$ref: "#/definitions/valueWithIgnore",
},
imports: {
$ref: "#/definitions/valueWithIgnore",
},
exports: {
$ref: "#/definitions/valueWithIgnore",
},
functions: {
$ref: "#/definitions/valueWithIgnore",
},
},
additionalProperties: false,
},
],
},
],
additionalItems: false,
},
schema: {
definitions: {
value: {
enum: [
"always-multiline",
"always",
"never",
"only-multiline"
]
},
valueWithIgnore: {
enum: [
"always-multiline",
"always",
"ignore",
"never",
"only-multiline"
]
}
},
type: "array",
items: [
{
oneOf: [
{
$ref: "#/definitions/value"
},
{
type: "object",
properties: {
arrays: { $ref: "#/definitions/valueWithIgnore" },
objects: { $ref: "#/definitions/valueWithIgnore" },
imports: { $ref: "#/definitions/valueWithIgnore" },
exports: { $ref: "#/definitions/valueWithIgnore" },
functions: { $ref: "#/definitions/valueWithIgnore" }
},
additionalProperties: false
}
]
}
],
additionalItems: false
},
messages: {
unexpected: "Unexpected trailing comma.",
missing: "Missing trailing comma.",
},
},
messages: {
unexpected: "Unexpected trailing comma.",
missing: "Missing trailing comma."
}
},
create(context) {
const options = normalizeOptions(
context.options[0],
context.languageOptions.ecmaVersion,
);
create(context) {
const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
const sourceCode = context.sourceCode;
const sourceCode = context.sourceCode;
/**
* Gets the last item of the given node.
* @param {ASTNode} node The node to get.
* @returns {ASTNode|null} The last node or null.
*/
function getLastItem(node) {
/**
* Returns the last element of an array
* @param {any[]} array The input array
* @returns {any} The last element
*/
function last(array) {
return array.at(-1);
}
/**
* Gets the last item of the given node.
* @param {ASTNode} node The node to get.
* @returns {ASTNode|null} The last node or null.
*/
function getLastItem(node) {
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
return last(node.properties);
case "ArrayExpression":
case "ArrayPattern":
return last(node.elements);
case "ImportDeclaration":
case "ExportNamedDeclaration":
return last(node.specifiers);
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
return last(node.params);
case "CallExpression":
case "NewExpression":
return last(node.arguments);
default:
return null;
}
}
/**
* Returns the last element of an array
* @param {any[]} array The input array
* @returns {any} The last element
*/
function last(array) {
return array[array.length - 1];
}
/**
* Gets the trailing comma token of the given node.
* If the trailing comma does not exist, this returns the token which is
* the insertion point of the trailing comma token.
* @param {ASTNode} node The node to get.
* @param {ASTNode} lastItem The last item of the node.
* @returns {Token} The trailing comma token or the insertion point.
*/
function getTrailingToken(node, lastItem) {
switch (node.type) {
case "ObjectExpression":
case "ArrayExpression":
case "CallExpression":
case "NewExpression":
return sourceCode.getLastToken(node, 1);
default: {
const nextToken = sourceCode.getTokenAfter(lastItem);
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
return last(node.properties);
case "ArrayExpression":
case "ArrayPattern":
return last(node.elements);
case "ImportDeclaration":
case "ExportNamedDeclaration":
return last(node.specifiers);
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
return last(node.params);
case "CallExpression":
case "NewExpression":
return last(node.arguments);
default:
return null;
}
}
if (astUtils.isCommaToken(nextToken)) {
return nextToken;
}
return sourceCode.getLastToken(lastItem);
}
}
}
/**
* Gets the trailing comma token of the given node.
* If the trailing comma does not exist, this returns the token which is
* the insertion point of the trailing comma token.
* @param {ASTNode} node The node to get.
* @param {ASTNode} lastItem The last item of the node.
* @returns {Token} The trailing comma token or the insertion point.
*/
function getTrailingToken(node, lastItem) {
switch (node.type) {
case "ObjectExpression":
case "ArrayExpression":
case "CallExpression":
case "NewExpression":
return sourceCode.getLastToken(node, 1);
default: {
const nextToken = sourceCode.getTokenAfter(lastItem);
/**
* Checks whether or not a given node is multiline.
* This rule handles a given node as multiline when the closing parenthesis
* and the last element are not on the same line.
* @param {ASTNode} node A node to check.
* @returns {boolean} `true` if the node is multiline.
*/
function isMultiline(node) {
const lastItem = getLastItem(node);
if (astUtils.isCommaToken(nextToken)) {
return nextToken;
}
return sourceCode.getLastToken(lastItem);
}
}
}
if (!lastItem) {
return false;
}
/**
* Checks whether or not a given node is multiline.
* This rule handles a given node as multiline when the closing parenthesis
* and the last element are not on the same line.
* @param {ASTNode} node A node to check.
* @returns {boolean} `true` if the node is multiline.
*/
function isMultiline(node) {
const lastItem = getLastItem(node);
const penultimateToken = getTrailingToken(node, lastItem);
const lastToken = sourceCode.getTokenAfter(penultimateToken);
if (!lastItem) {
return false;
}
return lastToken.loc.end.line !== penultimateToken.loc.end.line;
}
const penultimateToken = getTrailingToken(node, lastItem);
const lastToken = sourceCode.getTokenAfter(penultimateToken);
/**
* Reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forbidTrailingComma(node) {
const lastItem = getLastItem(node);
return lastToken.loc.end.line !== penultimateToken.loc.end.line;
}
if (
!lastItem ||
(node.type === "ImportDeclaration" &&
lastItem.type !== "ImportSpecifier")
) {
return;
}
/**
* Reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forbidTrailingComma(node) {
const lastItem = getLastItem(node);
const trailingToken = getTrailingToken(node, lastItem);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
if (astUtils.isCommaToken(trailingToken)) {
context.report({
node: lastItem,
loc: trailingToken.loc,
messageId: "unexpected",
*fix(fixer) {
yield fixer.remove(trailingToken);
const trailingToken = getTrailingToken(node, lastItem);
/*
* Extend the range of the fix to include surrounding tokens to ensure
* that the element after which the comma is removed stays _last_.
* This intentionally makes conflicts in fix ranges with rules that may be
* adding or removing elements in the same autofix pass.
* https://github.com/eslint/eslint/issues/15660
*/
yield fixer.insertTextBefore(
sourceCode.getTokenBefore(trailingToken),
"",
);
yield fixer.insertTextAfter(
sourceCode.getTokenAfter(trailingToken),
"",
);
},
});
}
}
if (astUtils.isCommaToken(trailingToken)) {
context.report({
node: lastItem,
loc: trailingToken.loc,
messageId: "unexpected",
*fix(fixer) {
yield fixer.remove(trailingToken);
/**
* Reports the last element of a given node if it does not have a trailing
* comma.
*
* If a given node is `ArrayPattern` which has `RestElement`, the trailing
* comma is disallowed, so report if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingComma(node) {
const lastItem = getLastItem(node);
/*
* Extend the range of the fix to include surrounding tokens to ensure
* that the element after which the comma is removed stays _last_.
* This intentionally makes conflicts in fix ranges with rules that may be
* adding or removing elements in the same autofix pass.
* https://github.com/eslint/eslint/issues/15660
*/
yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
}
});
}
}
if (
!lastItem ||
(node.type === "ImportDeclaration" &&
lastItem.type !== "ImportSpecifier")
) {
return;
}
if (!isTrailingCommaAllowed(lastItem)) {
forbidTrailingComma(node);
return;
}
/**
* Reports the last element of a given node if it does not have a trailing
* comma.
*
* If a given node is `ArrayPattern` which has `RestElement`, the trailing
* comma is disallowed, so report if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingComma(node) {
const lastItem = getLastItem(node);
const trailingToken = getTrailingToken(node, lastItem);
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
return;
}
if (!isTrailingCommaAllowed(lastItem)) {
forbidTrailingComma(node);
return;
}
if (trailingToken.value !== ",") {
context.report({
node: lastItem,
loc: {
start: trailingToken.loc.end,
end: astUtils.getNextLocation(
sourceCode,
trailingToken.loc.end,
),
},
messageId: "missing",
*fix(fixer) {
yield fixer.insertTextAfter(trailingToken, ",");
const trailingToken = getTrailingToken(node, lastItem);
/*
* Extend the range of the fix to include surrounding tokens to ensure
* that the element after which the comma is inserted stays _last_.
* This intentionally makes conflicts in fix ranges with rules that may be
* adding or removing elements in the same autofix pass.
* https://github.com/eslint/eslint/issues/15660
*/
yield fixer.insertTextBefore(trailingToken, "");
yield fixer.insertTextAfter(
sourceCode.getTokenAfter(trailingToken),
"",
);
},
});
}
}
if (trailingToken.value !== ",") {
context.report({
node: lastItem,
loc: {
start: trailingToken.loc.end,
end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
},
messageId: "missing",
*fix(fixer) {
yield fixer.insertTextAfter(trailingToken, ",");
/**
* If a given node is multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingCommaIfMultiline(node) {
if (isMultiline(node)) {
forceTrailingComma(node);
} else {
forbidTrailingComma(node);
}
}
/*
* Extend the range of the fix to include surrounding tokens to ensure
* that the element after which the comma is inserted stays _last_.
* This intentionally makes conflicts in fix ranges with rules that may be
* adding or removing elements in the same autofix pass.
* https://github.com/eslint/eslint/issues/15660
*/
yield fixer.insertTextBefore(trailingToken, "");
yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
}
});
}
}
/**
* Only if a given node is not multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function allowTrailingCommaIfMultiline(node) {
if (!isMultiline(node)) {
forbidTrailingComma(node);
}
}
/**
* If a given node is multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function forceTrailingCommaIfMultiline(node) {
if (isMultiline(node)) {
forceTrailingComma(node);
} else {
forbidTrailingComma(node);
}
}
const predicate = {
always: forceTrailingComma,
"always-multiline": forceTrailingCommaIfMultiline,
"only-multiline": allowTrailingCommaIfMultiline,
never: forbidTrailingComma,
ignore() {},
};
/**
* Only if a given node is not multiline, reports the last element of a given node
* when it does not have a trailing comma.
* Otherwise, reports a trailing comma if it exists.
* @param {ASTNode} node A node to check. Its type is one of
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
* ImportDeclaration, and ExportNamedDeclaration.
* @returns {void}
*/
function allowTrailingCommaIfMultiline(node) {
if (!isMultiline(node)) {
forbidTrailingComma(node);
}
}
return {
ObjectExpression: predicate[options.objects],
ObjectPattern: predicate[options.objects],
const predicate = {
always: forceTrailingComma,
"always-multiline": forceTrailingCommaIfMultiline,
"only-multiline": allowTrailingCommaIfMultiline,
never: forbidTrailingComma,
ignore() {}
};
ArrayExpression: predicate[options.arrays],
ArrayPattern: predicate[options.arrays],
return {
ObjectExpression: predicate[options.objects],
ObjectPattern: predicate[options.objects],
ImportDeclaration: predicate[options.imports],
ArrayExpression: predicate[options.arrays],
ArrayPattern: predicate[options.arrays],
ExportNamedDeclaration: predicate[options.exports],
ImportDeclaration: predicate[options.imports],
FunctionDeclaration: predicate[options.functions],
FunctionExpression: predicate[options.functions],
ArrowFunctionExpression: predicate[options.functions],
CallExpression: predicate[options.functions],
NewExpression: predicate[options.functions],
};
},
ExportNamedDeclaration: predicate[options.exports],
FunctionDeclaration: predicate[options.functions],
FunctionExpression: predicate[options.functions],
ArrowFunctionExpression: predicate[options.functions],
CallExpression: predicate[options.functions],
NewExpression: predicate[options.functions]
};
}
};
Loaded 100 of 787 files, more files were not shown because too many files have changed in this diff. Show more