routie dev init since i didn't adhere to any proper guidance up until now

This commit is contained in:
2026-04-29 22:27:29 -06:00
commit e1dabb71e2
15301 changed files with 3562618 additions and 0 deletions
@@ -0,0 +1,9 @@
import globals from 'globals';
const config = {
languageOptions: {
globals: globals.builtin,
},
};
export default config;
+17
View File
@@ -0,0 +1,17 @@
import type {ESLint, Linter} from 'eslint';
declare const eslintPluginUnicorn: ESLint.Plugin & {
configs: {
recommended: Linter.Config;
unopinionated: Linter.Config;
all: Linter.Config;
/** @deprecated Use `all` instead. The `flat/` prefix is no longer needed. */
'flat/all': Linter.Config;
/** @deprecated Use `recommended` instead. The `flat/` prefix is no longer needed. */
'flat/recommended': Linter.Config;
};
};
export default eslintPluginUnicorn;
+81
View File
@@ -0,0 +1,81 @@
import createDeprecatedRules from './rules/utils/create-deprecated-rules.js';
import flatConfigBase from './configs/flat-config-base.js';
import * as rawRules from './rules/index.js';
import {toEslintRules} from './rules/rule/index.js';
import packageJson from './package.json' with {type: 'json'};
const rules = toEslintRules(rawRules);
const deprecatedRules = createDeprecatedRules({
'no-instanceof-array': {
message: 'Replaced by `unicorn/no-instanceof-builtins` which covers more cases.',
replacedBy: ['unicorn/no-instanceof-builtins'],
},
'no-length-as-slice-end': {
message: 'Replaced by `unicorn/no-unnecessary-slice-end` which covers more cases.',
replacedBy: ['unicorn/no-unnecessary-slice-end'],
},
'no-array-push-push': {
message: 'Replaced by `unicorn/prefer-single-call` which covers more cases.',
replacedBy: ['unicorn/prefer-single-call'],
},
});
const externalRules = {
// Covered by `unicorn/no-negated-condition`
'no-negated-condition': 'off',
// Covered by `unicorn/no-nested-ternary`
'no-nested-ternary': 'off',
};
const recommendedRules = Object.fromEntries(Object.entries(rules).map(([id, rule]) => [
`unicorn/${id}`,
rule.meta.docs.recommended ? 'error' : 'off',
]));
const unopinionatedRules = Object.fromEntries(Object.entries(rules).map(([id, rule]) => [
`unicorn/${id}`,
rule.meta.docs.recommended === 'unopinionated' ? 'error' : 'off',
]));
const allRules = Object.fromEntries(Object.keys(rules).map(id => [
`unicorn/${id}`,
'error',
]));
const createConfig = (rules, flatConfigName) => ({
...flatConfigBase,
name: flatConfigName,
plugins: {
unicorn,
},
rules: {
...externalRules,
...rules,
},
});
const unicorn = {
meta: {
name: packageJson.name,
version: packageJson.version,
},
rules: {
...rules,
...deprecatedRules,
},
};
const configs = {
recommended: createConfig(recommendedRules, 'unicorn/recommended'),
unopinionated: createConfig(unopinionatedRules, 'unicorn/unopinionated'),
all: createConfig(allRules, 'unicorn/all'),
// TODO: Remove this at some point. Kept for now to avoid breaking users.
'flat/recommended': createConfig(recommendedRules, 'unicorn/flat/recommended'),
'flat/all': createConfig(allRules, 'unicorn/flat/all'),
};
unicorn.configs = configs;
export default unicorn;
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
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.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
'use strict';
module.exports = require('./globals.json');
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
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.
@@ -0,0 +1,121 @@
{
"name": "globals",
"version": "17.5.0",
"description": "Global identifiers from different JavaScript environments",
"license": "MIT",
"repository": "sindresorhus/globals",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"sideEffects": false,
"engines": {
"node": ">=18"
},
"scripts": {
"test": "npm run build && xo && ava && tsd",
"prepare": "npm run build",
"update": "node scripts/update.mjs",
"update:browser": "node scripts/update.mjs --job=browser",
"update:builtin": "node scripts/update.mjs --job=builtin",
"update:builtin-yearly": "node scripts/update.mjs --job=builtin-yearly",
"update:nodeBuiltin": "node scripts/update.mjs --job=nodeBuiltin",
"update:bunBuiltin": "node scripts/update.mjs --job=bunBuiltin",
"update:denoBuiltin": "node scripts/update.mjs --job=denoBuiltin",
"update:worker": "node scripts/update.mjs --job=worker",
"update:serviceworker": "node scripts/update.mjs --job=serviceworker",
"update:sharedWorker": "node scripts/update.mjs --job=sharedWorker",
"update:audioWorklet": "node scripts/update.mjs --job=audioWorklet",
"update:shelljs": "node scripts/update.mjs --job=shelljs",
"update:jest": "node scripts/update.mjs --job=jest",
"update:vitest": "node scripts/update.mjs --job=vitest",
"build": "run-s build:data build:types",
"build:data": "node scripts/generate-data.mjs",
"build:types": "node scripts/generate-types.mjs"
},
"files": [
"index.js",
"index.d.ts",
"globals.json"
],
"keywords": [
"globals",
"global",
"identifiers",
"variables",
"vars",
"jshint",
"eslint",
"environments"
],
"devDependencies": {
"@vitest/eslint-plugin": "^1.1.44",
"ava": "^6.3.0",
"cheerio": "^1.0.0",
"eslint-plugin-jest": "^28.11.0",
"get-port": "^7.1.0",
"is-identifier": "^1.0.1",
"nano-spawn": "^0.2.0",
"npm-run-all2": "^8.0.1",
"outdent": "^0.8.0",
"puppeteer": "^24.40.0",
"shelljs": "^0.9.2",
"tsd": "^0.32.0",
"type-fest": "^4.41.0",
"xo": "^0.60.0"
},
"xo": {
"rules": {
"unicorn/prefer-module": "off"
},
"overrides": [
{
"files": [
"data/*.mjs"
],
"rules": {
"import/no-anonymous-default-export": "off",
"camelcase": "off",
"unicorn/filename-case": [
"error",
{
"cases": {
"camelCase": true,
"kebabCase": true
}
}
]
}
},
{
"files": [
"scripts/*.mjs"
],
"rules": {
"n/no-unsupported-features/node-builtins": "off"
}
},
{
"files": [
"scripts/browser/assets/**/*.mjs"
],
"envs": [
"browser",
"worker",
"serviceworker"
],
"rules": {
"n/no-unsupported-features/node-builtins": "off",
"unicorn/prefer-add-event-listener": "off"
}
}
]
},
"tsd": {
"compilerOptions": {
"resolveJsonModule": true
}
}
}
@@ -0,0 +1,42 @@
# globals
> Global identifiers from different JavaScript environments
It's just a [JSON file](globals.json), so you can use it in any environment.
This package is used by ESLint 8 and earlier. For ESLint 9 and later, you should depend on this package directly in [your ESLint config](https://eslint.org/docs/latest/use/configure/language-options#predefined-global-variables).
## Install
```sh
npm install globals
```
## Usage
```js
import globals from 'globals';
console.log(globals.browser);
/*
{
addEventListener: false,
applicationCache: false,
ArrayBuffer: false,
atob: false,
}
*/
```
Each global is given a value of `true` or `false`. A value of `true` indicates that the variable may be overwritten. A value of `false` indicates that the variable should be considered read-only. This information is used by static analysis tools to flag incorrect behavior. We assume all variables should be `false` unless we hear otherwise.
For Node.js this package provides two sets of globals:
- `globals.nodeBuiltin`: Globals available to all code running in Node.js.
These will usually be available as properties on the `globalThis` object and include `process`, `Buffer`, but not CommonJS arguments like `require`.
See: https://nodejs.org/api/globals.html
- `globals.node`: A combination of the globals from `nodeBuiltin` plus all CommonJS arguments ("CommonJS module scope").
See: https://nodejs.org/api/modules.html#modules_the_module_scope
When analyzing code that is known to run outside of a CommonJS wrapper, for example, JavaScript modules, `nodeBuiltin` can find accidental CommonJS references.
+124
View File
@@ -0,0 +1,124 @@
{
"name": "eslint-plugin-unicorn",
"version": "64.0.0",
"description": "More than 100 powerful ESLint rules",
"license": "MIT",
"repository": "sindresorhus/eslint-plugin-unicorn",
"funding": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": {
"types": "./index.d.ts",
"default": "./index.js"
},
"sideEffects": false,
"engines": {
"node": "^20.10.0 || >=21.0.0"
},
"scripts": {
"create-rule": "node ./scripts/create-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
"create-rules-index-file": "node ./scripts/create-rules-index-file.js",
"fix": "run-p --continue-on-error \"fix:*\"",
"fix:eslint-docs": "eslint-doc-generator",
"fix:js": "npm run lint:js -- --fix",
"fix:markdown": "npm run lint:markdown -- --fix",
"fix:snapshots": "ava --update-snapshots",
"integration": "node ./test/integration/test.js",
"lint": "run-p --continue-on-error \"lint:*\"",
"lint:eslint-docs": "npm run fix:eslint-docs -- --check",
"lint:js": "eslint",
"lint:markdown": "markdownlint \"**/*.md\"",
"lint:package-json": "npmPkgJsonLint .",
"rename-rule": "node ./scripts/rename-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
"run-rules-on-codebase": "eslint --config=./eslint.dogfooding.config.js",
"smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.js",
"test": "npm-run-all --continue-on-error lint \"test:*\"",
"test:js": "c8 ava"
},
"files": [
"index.js",
"index.d.ts",
"rules",
"configs"
],
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"unicorn",
"linter",
"lint",
"style",
"xo"
],
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@eslint-community/eslint-utils": "^4.9.1",
"change-case": "^5.4.4",
"ci-info": "^4.4.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.49.0",
"find-up-simple": "^1.0.1",
"globals": "^17.4.0",
"indent-string": "^5.0.0",
"is-builtin-module": "^5.0.0",
"jsesc": "^3.1.0",
"pluralize": "^8.0.0",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.13.0",
"semver": "^7.7.4",
"strip-indent": "^4.1.1"
},
"devDependencies": {
"@babel/code-frame": "^7.28.3",
"@eslint/eslintrc": "^3.3.5",
"@lubien/fixture-beta-package": "^1.0.0-beta.1",
"@typescript-eslint/parser": "^8.57.2",
"@typescript-eslint/types": "^8.57.2",
"ava": "^7.0.0",
"c8": "^11.0.0",
"enquirer": "^2.4.1",
"eslint": "^10.1.0",
"eslint-ava-rule-tester": "^5.0.1",
"eslint-config-xo": "^0.51.0",
"eslint-doc-generator": "^3.3.2",
"eslint-plugin-eslint-plugin": "^7.3.2",
"eslint-plugin-jsdoc": "^62.8.1",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-remote-tester": "^4.0.4",
"eslint-remote-tester-repositories": "^2.0.2",
"eslint-scope": "^9.1.2",
"espree": "^11.2.0",
"listr2": "^10.2.1",
"markdownlint-cli": "^0.48.0",
"nano-spawn": "^2.0.0",
"node-style-text": "^2.1.2",
"npm-package-json-lint": "^9.1.0",
"npm-run-all2": "^8.0.4",
"open-editor": "^6.0.0",
"outdent": "^0.8.0",
"pretty-ms": "^9.3.0",
"typescript": "^5.9.3",
"vue-eslint-parser": "^10.4.0",
"yaml": "^2.8.3"
},
"peerDependencies": {
"eslint": ">=9.38.0"
},
"ava": {
"files": [
"test/*.js",
"test/unit/*.js"
]
},
"c8": {
"reporter": [
"text",
"lcov"
]
}
}
+266
View File
@@ -0,0 +1,266 @@
# eslint-plugin-unicorn [![Coverage Status](https://codecov.io/gh/sindresorhus/eslint-plugin-unicorn/branch/main/graph/badge.svg)](https://codecov.io/gh/sindresorhus/eslint-plugin-unicorn/branch/main) [![npm version](https://img.shields.io/npm/v/eslint-plugin-unicorn.svg?style=flat)](https://npmjs.com/package/eslint-plugin-unicorn)
<!-- markdownlint-disable-next-line no-inline-html -->
<img src="https://cloud.githubusercontent.com/assets/170270/18659176/1cc373d0-7f33-11e6-890f-0ba35362ee7e.jpg" width="180" align="right" alt="Unicorn">
> More than 100 powerful ESLint rules
You might want to check out [XO](https://github.com/xojs/xo), which includes this plugin.
[**Propose or contribute a new rule ➡**](.github/contributing.md)
## Install
```sh
npm install --save-dev eslint eslint-plugin-unicorn
```
**Requires ESLint `>=9.20.0`, [flat config](https://eslint.org/docs/latest/use/configure/configuration-files), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm).**
## Usage
Use a [preset config](#preset-configs) or configure each rule in `eslint.config.js`.
If you don't use the preset, ensure you use the same `languageOptions` config as below.
```js
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
export default [
{
languageOptions: {
globals: globals.builtin,
},
plugins: {
unicorn: eslintPluginUnicorn,
},
rules: {
'unicorn/better-regex': 'error',
'unicorn/…': 'error',
},
},
// …
];
```
## Rules
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` -->
<!-- begin auto-generated rules list -->
💼 [Configurations](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) enabled in.\
✅ Set in the `recommended` [configuration](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).\
☑️ Set in the `unopinionated` [configuration](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).\
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
| Name                                    | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- | :- | :- |
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | | 🔧 | |
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. | ✅ | 🔧 | |
| [consistent-assert](docs/rules/consistent-assert.md) | Enforce consistent assertion style with `node:assert`. | ✅ | 🔧 | |
| [consistent-date-clone](docs/rules/consistent-date-clone.md) | Prefer passing `Date` directly to the constructor when cloning. | ✅ ☑️ | 🔧 | |
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | | 💡 |
| [consistent-empty-array-spread](docs/rules/consistent-empty-array-spread.md) | Prefer consistent types when spreading a ternary in an array literal. | ✅ | 🔧 | |
| [consistent-existence-index-check](docs/rules/consistent-existence-index-check.md) | Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`. | ✅ ☑️ | 🔧 | |
| [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. | ✅ | | |
| [consistent-template-literal-escape](docs/rules/consistent-template-literal-escape.md) | Enforce consistent style for escaping `${` in template literals. | ✅ | 🔧 | |
| [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | 🔧 | |
| [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. | ✅ | 🔧 | |
| [error-message](docs/rules/error-message.md) | Enforce passing a `message` value when creating a built-in error. | ✅ ☑️ | | |
| [escape-case](docs/rules/escape-case.md) | Require escape sequences to use uppercase or lowercase values. | ✅ ☑️ | 🔧 | |
| [expiring-todo-comments](docs/rules/expiring-todo-comments.md) | Add expiration conditions to TODO comments. | ✅ ☑️ | | |
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. | ✅ | 🔧 | 💡 |
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. | ✅ | | |
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ ☑️ | | |
| [isolated-functions](docs/rules/isolated-functions.md) | Prevent usage of variables from outside the scope of isolated functions. | ✅ | | |
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ ☑️ | 🔧 | 💡 |
| [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | ✅ ☑️ | | |
| [no-accessor-recursion](docs/rules/no-accessor-recursion.md) | Disallow recursive access to `this` within getters and setters. | ✅ ☑️ | | |
| [no-anonymous-default-export](docs/rules/no-anonymous-default-export.md) | Disallow anonymous functions and classes as the default export. | ✅ ☑️ | | 💡 |
| [no-array-callback-reference](docs/rules/no-array-callback-reference.md) | Prevent passing a function reference directly to iterator methods. | ✅ | | 💡 |
| [no-array-for-each](docs/rules/no-array-for-each.md) | Prefer `for…of` over the `forEach` method. | ✅ ☑️ | 🔧 | 💡 |
| [no-array-method-this-argument](docs/rules/no-array-method-this-argument.md) | Disallow using the `this` argument in array methods. | ✅ ☑️ | 🔧 | 💡 |
| [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | ✅ | | |
| [no-array-reverse](docs/rules/no-array-reverse.md) | Prefer `Array#toReversed()` over `Array#reverse()`. | ✅ ☑️ | | 💡 |
| [no-array-sort](docs/rules/no-array-sort.md) | Prefer `Array#toSorted()` over `Array#sort()`. | ✅ ☑️ | | 💡 |
| [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. | ✅ | 🔧 | |
| [no-await-in-promise-methods](docs/rules/no-await-in-promise-methods.md) | Disallow using `await` in `Promise` method parameters. | ✅ ☑️ | | 💡 |
| [no-console-spaces](docs/rules/no-console-spaces.md) | Do not use leading/trailing space between `console.log` parameters. | ✅ ☑️ | 🔧 | |
| [no-document-cookie](docs/rules/no-document-cookie.md) | Do not use `document.cookie` directly. | ✅ ☑️ | | |
| [no-empty-file](docs/rules/no-empty-file.md) | Disallow empty files. | ✅ ☑️ | | |
| [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. | ✅ | 🔧 | 💡 |
| [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. | ✅ ☑️ | 🔧 | |
| [no-immediate-mutation](docs/rules/no-immediate-mutation.md) | Disallow immediate mutation after variable assignment. | ✅ | 🔧 | 💡 |
| [no-instanceof-builtins](docs/rules/no-instanceof-builtins.md) | Disallow `instanceof` with built-in objects | ✅ ☑️ | 🔧 | 💡 |
| [no-invalid-fetch-options](docs/rules/no-invalid-fetch-options.md) | Disallow invalid options in `fetch()` and `new Request()`. | ✅ ☑️ | | |
| [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. | ✅ ☑️ | | |
| [no-keyword-prefix](docs/rules/no-keyword-prefix.md) | Disallow identifiers starting with `new` or `class`. | | | |
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | ✅ ☑️ | 🔧 | |
| [no-magic-array-flat-depth](docs/rules/no-magic-array-flat-depth.md) | Disallow a magic number as the `depth` argument in `Array#flat(…).` | ✅ ☑️ | | |
| [no-named-default](docs/rules/no-named-default.md) | Disallow named usage of default import and export. | ✅ ☑️ | 🔧 | |
| [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | ✅ ☑️ | 🔧 | |
| [no-negation-in-equality-check](docs/rules/no-negation-in-equality-check.md) | Disallow negated expression in equality check. | ✅ ☑️ | | 💡 |
| [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. | ✅ | 🔧 | |
| [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. | ✅ ☑️ | 🔧 | 💡 |
| [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. | ✅ ☑️ | 🔧 | 💡 |
| [no-null](docs/rules/no-null.md) | Disallow the use of the `null` literal. | ✅ | 🔧 | 💡 |
| [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) | Disallow the use of objects as default parameters. | ✅ ☑️ | | |
| [no-process-exit](docs/rules/no-process-exit.md) | Disallow `process.exit()`. | ✅ ☑️ | | |
| [no-single-promise-in-promise-methods](docs/rules/no-single-promise-in-promise-methods.md) | Disallow passing single-element arrays to `Promise` methods. | ✅ ☑️ | 🔧 | 💡 |
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. | ✅ ☑️ | 🔧 | |
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. | ✅ ☑️ | | |
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. | ✅ ☑️ | | |
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. | ✅ ☑️ | 🔧 | 💡 |
| [no-unnecessary-array-flat-depth](docs/rules/no-unnecessary-array-flat-depth.md) | Disallow using `1` as the `depth` argument of `Array#flat()`. | ✅ ☑️ | 🔧 | |
| [no-unnecessary-array-splice-count](docs/rules/no-unnecessary-array-splice-count.md) | Disallow using `.length` or `Infinity` as the `deleteCount` or `skipCount` argument of `Array#{splice,toSpliced}()`. | ✅ ☑️ | 🔧 | |
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. | ✅ ☑️ | 🔧 | |
| [no-unnecessary-polyfills](docs/rules/no-unnecessary-polyfills.md) | Enforce the use of built-in methods instead of unnecessary polyfills. | ✅ ☑️ | | |
| [no-unnecessary-slice-end](docs/rules/no-unnecessary-slice-end.md) | Disallow using `.length` or `Infinity` as the `end` argument of `{Array,String,TypedArray}#slice()`. | ✅ ☑️ | 🔧 | |
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. | ✅ ☑️ | 🔧 | |
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. | ✅ ☑️ | | |
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |
| [no-useless-collection-argument](docs/rules/no-useless-collection-argument.md) | Disallow useless values or fallbacks in `Set`, `Map`, `WeakSet`, or `WeakMap`. | ✅ ☑️ | 🔧 | 💡 |
| [no-useless-error-capture-stack-trace](docs/rules/no-useless-error-capture-stack-trace.md) | Disallow unnecessary `Error.captureStackTrace(…)`. | ✅ ☑️ | 🔧 | |
| [no-useless-fallback-in-spread](docs/rules/no-useless-fallback-in-spread.md) | Disallow useless fallback when spreading in object literals. | ✅ ☑️ | 🔧 | |
| [no-useless-iterator-to-array](docs/rules/no-useless-iterator-to-array.md) | Disallow unnecessary `.toArray()` on iterators. | ✅ ☑️ | 🔧 | 💡 |
| [no-useless-length-check](docs/rules/no-useless-length-check.md) | Disallow useless array length check. | ✅ ☑️ | 🔧 | |
| [no-useless-promise-resolve-reject](docs/rules/no-useless-promise-resolve-reject.md) | Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks | ✅ ☑️ | 🔧 | |
| [no-useless-spread](docs/rules/no-useless-spread.md) | Disallow unnecessary spread. | ✅ ☑️ | 🔧 | |
| [no-useless-switch-case](docs/rules/no-useless-switch-case.md) | Disallow useless case in switch statements. | ✅ ☑️ | | 💡 |
| [no-useless-undefined](docs/rules/no-useless-undefined.md) | Disallow useless `undefined`. | ✅ ☑️ | 🔧 | |
| [no-zero-fractions](docs/rules/no-zero-fractions.md) | Disallow number literals with zero fractions or dangling dots. | ✅ ☑️ | 🔧 | |
| [number-literal-case](docs/rules/number-literal-case.md) | Enforce proper case for numeric literals. | ✅ ☑️ | 🔧 | |
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the style of numeric separators by correctly grouping digits. | ✅ ☑️ | 🔧 | |
| [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) | Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. | ✅ ☑️ | 🔧 | |
| [prefer-array-find](docs/rules/prefer-array-find.md) | Prefer `.find(…)` and `.findLast(…)` over the first or last element from `.filter(…)`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-array-flat](docs/rules/prefer-array-flat.md) | Prefer `Array#flat()` over legacy techniques to flatten arrays. | ✅ ☑️ | 🔧 | |
| [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) | Prefer `.flatMap(…)` over `.map(…).flat()`. | ✅ ☑️ | 🔧 | |
| [prefer-array-index-of](docs/rules/prefer-array-index-of.md) | Prefer `Array#{indexOf,lastIndexOf}()` over `Array#{findIndex,findLastIndex}()` when looking for the index of an item. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-bigint-literals](docs/rules/prefer-bigint-literals.md) | Prefer `BigInt` literals over the constructor. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-blob-reading-methods](docs/rules/prefer-blob-reading-methods.md) | Prefer `Blob#arrayBuffer()` over `FileReader#readAsArrayBuffer(…)` and `Blob#text()` over `FileReader#readAsText(…)`. | ✅ ☑️ | | |
| [prefer-class-fields](docs/rules/prefer-class-fields.md) | Prefer class field declarations over `this` assignments in constructors. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-classlist-toggle](docs/rules/prefer-classlist-toggle.md) | Prefer using `Element#classList.toggle()` to toggle class names. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-code-point](docs/rules/prefer-code-point.md) | Prefer `String#codePointAt(…)` over `String#charCodeAt(…)` and `String.fromCodePoint(…)` over `String.fromCharCode(…)`. | ✅ ☑️ | | 💡 |
| [prefer-date-now](docs/rules/prefer-date-now.md) | Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. | ✅ ☑️ | 🔧 | |
| [prefer-default-parameters](docs/rules/prefer-default-parameters.md) | Prefer default parameters over reassignment. | ✅ ☑️ | | 💡 |
| [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) | Prefer `Node#append()` over `Node#appendChild()`. | ✅ ☑️ | 🔧 | |
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over calling attribute methods. | ✅ ☑️ | 🔧 | |
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | ✅ ☑️ | | 💡 |
| [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | ✅ ☑️ | | |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | ✅ | 🔧 | 💡 |
| [prefer-global-this](docs/rules/prefer-global-this.md) | Prefer `globalThis` over `window`, `self`, and `global`. | ✅ ☑️ | 🔧 | |
| [prefer-import-meta-properties](docs/rules/prefer-import-meta-properties.md) | Prefer `import.meta.{dirname,filename}` over legacy techniques for getting file paths. | | 🔧 | |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ ☑️ | 🔧 | |
| [prefer-logical-operator-over-ternary](docs/rules/prefer-logical-operator-over-ternary.md) | Prefer using a logical operator over a ternary. | ✅ ☑️ | | 💡 |
| [prefer-math-min-max](docs/rules/prefer-math-min-max.md) | Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons. | ✅ ☑️ | 🔧 | |
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. | ✅ ☑️ | 🔧 | |
| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. | ✅ ☑️ | 🔧 | |
| [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-native-coercion-functions](docs/rules/prefer-native-coercion-functions.md) | Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly. | ✅ ☑️ | 🔧 | |
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` when possible. | ✅ ☑️ | 🔧 | |
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ ☑️ | 🔧 | |
| [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-object-from-entries](docs/rules/prefer-object-from-entries.md) | Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object. | ✅ ☑️ | 🔧 | |
| [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ ☑️ | 🔧 | |
| [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. | ✅ ☑️ | 🔧 | |
| [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()` and `.getElementsByName()`. | ✅ | 🔧 | |
| [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ ☑️ | 🔧 | |
| [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-response-static-json](docs/rules/prefer-response-static-json.md) | Prefer `Response.json()` over `new Response(JSON.stringify())`. | ✅ ☑️ | 🔧 | |
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-set-size](docs/rules/prefer-set-size.md) | Prefer using `Set#size` instead of `Array#length`. | ✅ ☑️ | 🔧 | |
| [prefer-simple-condition-first](docs/rules/prefer-simple-condition-first.md) | Prefer simple conditions first in logical expressions. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-single-call](docs/rules/prefer-single-call.md) | Enforce combining multiple `Array#push()`, `Element#classList.{add,remove}()`, and `importScripts()` into one call. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split('')`. | ✅ | 🔧 | 💡 |
| [prefer-string-raw](docs/rules/prefer-string-raw.md) | Prefer using the `String.raw` tag to avoid escaping `\`. | ✅ ☑️ | 🔧 | |
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. | ✅ ☑️ | 🔧 | |
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. | ✅ ☑️ | 🔧 | |
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. | ✅ ☑️ | 🔧 | 💡 |
| [prefer-string-trim-start-end](docs/rules/prefer-string-trim-start-end.md) | Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. | ✅ ☑️ | 🔧 | |
| [prefer-structured-clone](docs/rules/prefer-structured-clone.md) | Prefer using `structuredClone` to create a deep clone. | ✅ ☑️ | | 💡 |
| [prefer-switch](docs/rules/prefer-switch.md) | Prefer `switch` over multiple `else-if`. | ✅ ☑️ | 🔧 | |
| [prefer-ternary](docs/rules/prefer-ternary.md) | Prefer ternary expressions over simple `if-else` statements. | ✅ ☑️ | 🔧 | |
| [prefer-top-level-await](docs/rules/prefer-top-level-await.md) | Prefer top-level await over top-level promises and async function calls. | ✅ ☑️ | | 💡 |
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. | ✅ ☑️ | 🔧 | |
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. | ✅ | 🔧 | |
| [relative-url-style](docs/rules/relative-url-style.md) | Enforce consistent relative URL style. | ✅ ☑️ | 🔧 | 💡 |
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. | ✅ ☑️ | 🔧 | |
| [require-module-attributes](docs/rules/require-module-attributes.md) | Require non-empty module attributes for imports and exports | ✅ ☑️ | 🔧 | |
| [require-module-specifiers](docs/rules/require-module-specifiers.md) | Require non-empty specifier list in import and export statements. | ✅ ☑️ | 🔧 | 💡 |
| [require-number-to-fixed-digits-argument](docs/rules/require-number-to-fixed-digits-argument.md) | Enforce using the digits argument with `Number#toFixed()`. | ✅ ☑️ | 🔧 | |
| [require-post-message-target-origin](docs/rules/require-post-message-target-origin.md) | Enforce using the `targetOrigin` argument with `window.postMessage()`. | | | 💡 |
| [string-content](docs/rules/string-content.md) | Enforce better string content. | | 🔧 | 💡 |
| [switch-case-braces](docs/rules/switch-case-braces.md) | Enforce consistent brace style for `case` clauses. | ✅ | 🔧 | |
| [switch-case-break-position](docs/rules/switch-case-break-position.md) | Enforce consistent `break`/`return`/`continue`/`throw` position in `case` clauses. | ✅ | 🔧 | |
| [template-indent](docs/rules/template-indent.md) | Fix whitespace-insensitive template indentation. | ✅ | 🔧 | |
| [text-encoding-identifier-case](docs/rules/text-encoding-identifier-case.md) | Enforce consistent case for text encoding identifiers. | ✅ ☑️ | 🔧 | 💡 |
| [throw-new-error](docs/rules/throw-new-error.md) | Require `new` when creating an error. | ✅ ☑️ | 🔧 | |
<!-- end auto-generated rules list -->
### Deleted and deprecated rules
See [the list](docs/deleted-and-deprecated-rules.md).
## Preset configs
See the [ESLint docs](https://eslint.org/docs/latest/use/configure/configuration-files) for more information about extending config files.
**Note**: Preset configs will also enable the correct [language options](https://eslint.org/docs/latest/use/configure/language-options).
### Recommended config
This plugin exports a `recommended` config that enforces good practices.
```js
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
export default [
// …
eslintPluginUnicorn.configs.recommended,
{
rules: {
'unicorn/better-regex': 'warn',
},
},
];
```
### All config
This plugin exports an `all` that makes use of all rules (except for deprecated ones).
```js
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
export default [
// …
eslintPluginUnicorn.configs.all,
{
rules: {
'unicorn/better-regex': 'warn',
},
},
];
```
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Fisker Cheung](https://github.com/fisker)
- [Bryan Mishkin](https://github.com/bmish)
- [futpib](https://github.com/futpib)
### Former
- [Jeroen Engels](https://github.com/jfmengels)
- [Sam Verschueren](https://github.com/SamVerschueren)
- [Adam Babcock](https://github.com/MrHen)
@@ -0,0 +1,118 @@
/**
@typedef {
{
name?: string,
names?: string[],
argumentsLength?: number,
minimumArguments?: number,
maximumArguments?: number,
allowSpreadElement?: boolean,
optional?: boolean,
} | string | string[]
} CallOrNewExpressionCheckOptions
*/
// eslint-disable-next-line complexity
function create(node, options, types) {
if (!types.includes(node?.type)) {
return false;
}
if (typeof options === 'string') {
options = {names: [options]};
}
if (Array.isArray(options)) {
options = {names: options};
}
let {
name,
names,
argumentsLength,
minimumArguments,
maximumArguments,
allowSpreadElement,
optional,
} = {
minimumArguments: 0,
maximumArguments: Number.POSITIVE_INFINITY,
allowSpreadElement: false,
...options,
};
if (name) {
names = [name];
}
if (
(optional === true && (node.optional !== optional))
|| (
optional === false
// `node.optional` can be `undefined` in some parsers
&& node.optional
)
) {
return false;
}
if (typeof argumentsLength === 'number' && node.arguments.length !== argumentsLength) {
return false;
}
if (minimumArguments !== 0 && node.arguments.length < minimumArguments) {
return false;
}
if (Number.isFinite(maximumArguments) && node.arguments.length > maximumArguments) {
return false;
}
if (!allowSpreadElement) {
const maximumArgumentsLength = Number.isFinite(maximumArguments) ? maximumArguments : argumentsLength;
if (
typeof maximumArgumentsLength === 'number'
&& node.arguments.some((node, index) =>
node.type === 'SpreadElement'
&& index < maximumArgumentsLength)
) {
return false;
}
}
if (
Array.isArray(names)
&& names.length > 0
&& (
node.callee.type !== 'Identifier'
|| !names.includes(node.callee.name)
)
) {
return false;
}
return true;
}
/**
@param {CallOrNewExpressionCheckOptions} [options]
@returns {boolean}
*/
export const isCallExpression = (node, options) => create(node, options, ['CallExpression']);
/**
@param {CallOrNewExpressionCheckOptions} [options]
@returns {boolean}
*/
export const isNewExpression = (node, options) => {
if (typeof options?.optional === 'boolean') {
throw new TypeError('Cannot check node.optional in `isNewExpression`.');
}
return create(node, options, ['NewExpression']);
};
/**
@param {CallOrNewExpressionCheckOptions} [options]
@returns {boolean}
*/
export const isCallOrNewExpression = (node, options) => create(node, options, ['CallExpression', 'NewExpression']);
@@ -0,0 +1,7 @@
const functionTypes = [
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
];
export default functionTypes;
+31
View File
@@ -0,0 +1,31 @@
export {
isLiteral,
isStringLiteral,
isNumericLiteral,
isBigIntLiteral,
isNullLiteral,
isRegexLiteral,
isEmptyStringLiteral,
} from './literal.js';
export {
isNewExpression,
isCallExpression,
isCallOrNewExpression,
} from './call-or-new-expression.js';
export {default as isArrowFunctionBody} from './is-arrow-function-body.js';
export {default as isDirective} from './is-directive.js';
export {default as isEmptyNode} from './is-empty-node.js';
export {default as isEmptyArrayExpression} from './is-empty-array-expression.js';
export {default as isEmptyObjectExpression} from './is-empty-object-expression.js';
export {default as isExpressionStatement} from './is-expression-statement.js';
export {default as isFunction} from './is-function.js';
export {default as isMemberExpression} from './is-member-expression.js';
export {default as isMethodCall} from './is-method-call.js';
export {default as isNegativeOne} from './is-negative-one.js';
export {default as isReferenceIdentifier} from './is-reference-identifier.js';
export {default as isStaticRequire} from './is-static-require.js';
export {default as isTaggedTemplateLiteral} from './is-tagged-template-literal.js';
export {default as isUndefined} from './is-undefined.js';
export {default as functionTypes} from './function-types.js';
@@ -0,0 +1,3 @@
export default function isArrowFunctionBody(node) {
return node.parent.type === 'ArrowFunctionExpression' && node.parent.body === node;
}
@@ -0,0 +1,4 @@
const isDirective = node => node.type === 'ExpressionStatement'
&& typeof node.directive === 'string';
export default isDirective;
@@ -0,0 +1,5 @@
const isEmptyArrayExpression = node =>
node.type === 'ArrayExpression'
&& node.elements.length === 0;
export default isEmptyArrayExpression;
+17
View File
@@ -0,0 +1,17 @@
export default function isEmptyNode(node, additionalEmpty) {
const {type} = node;
if (type === 'BlockStatement') {
return node.body.every(currentNode => isEmptyNode(currentNode, additionalEmpty));
}
if (type === 'EmptyStatement') {
return true;
}
if (additionalEmpty?.(node)) {
return true;
}
return false;
}
@@ -0,0 +1,5 @@
const isEmptyArrayExpression = node =>
node.type === 'ObjectExpression'
&& node.properties.length === 0;
export default isEmptyArrayExpression;
@@ -0,0 +1,7 @@
export default function isExpressionStatement(node) {
return node.type === 'ExpressionStatement'
|| (
node.type === 'ChainExpression'
&& node.parent.type === 'ExpressionStatement'
);
}
+5
View File
@@ -0,0 +1,5 @@
import functionTypes from './function-types.js';
export default function isFunction(node) {
return functionTypes.includes(node.type);
}
@@ -0,0 +1,98 @@
/* eslint-disable complexity */
/**
@param {
{
property?: string,
properties?: string[],
object?: string,
objects?: string[],
optional?: boolean,
computed?: boolean
} | string | string[]
} [options]
@returns {string}
*/
export default function isMemberExpression(node, options) {
if (node?.type !== 'MemberExpression') {
return false;
}
if (typeof options === 'string') {
options = {properties: [options]};
}
if (Array.isArray(options)) {
options = {properties: options};
}
let {
property,
properties,
object,
objects,
optional,
computed,
} = {
property: '',
properties: [],
object: '',
...options,
};
if (property) {
properties = [property];
}
if (object) {
objects = [object];
}
if (
(optional === true && (node.optional !== optional))
|| (
optional === false
// `node.optional` can be `undefined` in some parsers
&& node.optional
)
) {
return false;
}
if (
Array.isArray(properties)
&& properties.length > 0
) {
if (
node.property.type !== 'Identifier'
|| !properties.includes(node.property.name)
) {
return false;
}
computed ??= false;
}
if (
(computed === true && (node.computed !== computed))
|| (
computed === false
// `node.computed` can be `undefined` in some parsers
&& node.computed
)
) {
return false;
}
if (
Array.isArray(objects)
&& objects.length > 0
&& (
node.object.type !== 'Identifier'
|| !objects.includes(node.object.name)
)
) {
return false;
}
return true;
}
@@ -0,0 +1,62 @@
import isMemberExpression from './is-member-expression.js';
import {isCallExpression} from './call-or-new-expression.js';
/**
@param {
{
// `isCallExpression` options
argumentsLength?: number,
minimumArguments?: number,
maximumArguments?: number,
optionalCall?: boolean,
allowSpreadElement?: boolean,
// `isMemberExpression` options
method?: string,
methods?: string[],
object?: string,
objects?: string[],
optionalMember?: boolean,
computed?: boolean
} | string | string[]
} [options]
@returns {string}
*/
export default function isMethodCall(node, options) {
if (typeof options === 'string') {
options = {methods: [options]};
}
if (Array.isArray(options)) {
options = {methods: options};
}
const {
optionalCall,
optionalMember,
method,
methods,
} = {
method: '',
methods: [],
...options,
};
return (
isCallExpression(node, {
argumentsLength: options.argumentsLength,
minimumArguments: options.minimumArguments,
maximumArguments: options.maximumArguments,
allowSpreadElement: options.allowSpreadElement,
optional: optionalCall,
})
&& isMemberExpression(node.callee, {
object: options.object,
objects: options.objects,
computed: options.computed,
property: method,
properties: methods,
optional: optionalMember,
})
);
}
@@ -0,0 +1,8 @@
import {isNumericLiteral} from './literal.js';
export default function isNegativeOne(node) {
return node?.type === 'UnaryExpression'
&& node.operator === '-'
&& isNumericLiteral(node.argument)
&& node.argument.value === 1;
}
@@ -0,0 +1,159 @@
// eslint-disable-next-line complexity
function isNotReference(node) {
const {parent} = node;
switch (parent.type) {
// `foo.Identifier`
case 'MemberExpression': {
return !parent.computed && parent.property === node;
}
case 'FunctionDeclaration':
case 'FunctionExpression': {
return (
// `function foo(Identifier) {}`
// `const foo = function(Identifier) {}`
parent.params.includes(node)
// `function Identifier() {}`
// `const foo = function Identifier() {}`
|| parent.id === node
);
}
case 'ArrowFunctionExpression': {
// `const foo = (Identifier) => {}`
return parent.params.includes(node);
}
// `class Identifier() {}`
// `const foo = class Identifier() {}`
// `const Identifier = 1`
case 'ClassDeclaration':
case 'ClassExpression':
case 'VariableDeclarator': {
return parent.id === node;
}
// `class Foo {Identifier = 1}`
// `class Foo {Identifier() {}}`
case 'PropertyDefinition':
case 'MethodDefinition': {
return !parent.computed && parent.key === node;
}
// `const foo = {Identifier: 1}`
// `const {Identifier} = {}`
// `const {Identifier: foo} = {}`
// `const {Identifier} = {}`
// `const {foo: Identifier} = {}`
case 'Property': {
return (
(
!parent.computed
&& parent.key === node
&& (
(parent.parent.type === 'ObjectExpression' || parent.parent.type === 'ObjectPattern')
&& parent.parent.properties.includes(parent)
)
)
|| (
parent.value === node
&& parent.parent.type === 'ObjectPattern'
&& parent.parent.properties.includes(parent)
)
);
}
// `const [Identifier] = []`
case 'ArrayPattern': {
return parent.elements.includes(node);
}
/*
```
Identifier: for (const foo of bar) {
continue Identifier;
break Identifier;
}
```
*/
case 'LabeledStatement':
case 'ContinueStatement':
case 'BreakStatement': {
return parent.label === node;
}
// `import * as Identifier from 'foo'`
// `import Identifier from 'foo'`
case 'ImportDefaultSpecifier':
case 'ImportNamespaceSpecifier': {
return parent.local === node;
}
// `export * as Identifier from 'foo'`
case 'ExportAllDeclaration': {
return parent.exported === node;
}
// `import {foo as Identifier} from 'foo'`
// `import {Identifier as foo} from 'foo'`
case 'ImportSpecifier': {
return (parent.local === node || parent.imported === node);
}
// `export {foo as Identifier}`
// `export {Identifier as foo}`
case 'ExportSpecifier': {
return (parent.local === node || parent.exported === node);
}
// TypeScript
case 'TSDeclareFunction':
case 'TSEnumMember': {
return parent.id === node;
}
// `type Foo = { [Identifier: string]: string }`
case 'TSIndexSignature': {
return parent.parameters.includes(node);
}
// `@typescript-eslint/parse` v7
// `type Foo = { [Identifier in keyof string]: number; };`
case 'TSTypeParameter': {
return parent.name === node;
}
// `@typescript-eslint/parse` v8
// `type Foo = { [Identifier in keyof string]: number; };`
case 'TSMappedType': {
return parent.key === node;
}
// `type Identifier = Foo`
case 'TSTypeAliasDeclaration': {
return parent.id === node;
}
case 'TSPropertySignature': {
return parent.key === node;
}
// No default
}
return false;
}
export default function isReferenceIdentifier(node, nameOrNames = []) {
if (node.type !== 'Identifier') {
return false;
}
const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames];
if (names.length > 0 && !names.includes(node.name)) {
return false;
}
return !isNotReference(node);
}
@@ -0,0 +1,10 @@
import {isStringLiteral} from './literal.js';
import {isCallExpression} from './call-or-new-expression.js';
const isStaticRequire = node => isCallExpression(node, {
name: 'require',
argumentsLength: 1,
optional: false,
}) && isStringLiteral(node.arguments[0]);
export default isStaticRequire;
@@ -0,0 +1,24 @@
import {isNodeMatches} from '../utils/is-node-matches.js';
/**
Check if the given node is a tagged template literal.
@param {Node} node - The AST node to check.
@param {string[]} tags - The object name or key paths.
@returns {boolean}
*/
export default function isTaggedTemplateLiteral(node, tags) {
if (
node.type !== 'TemplateLiteral'
|| node.parent.type !== 'TaggedTemplateExpression'
|| node.parent.quasi !== node
) {
return false;
}
if (tags) {
return isNodeMatches(node.parent.tag, tags);
}
return true;
}
@@ -0,0 +1,3 @@
export default function isUndefined(node) {
return node?.type === 'Identifier' && node.name === 'undefined';
}
+19
View File
@@ -0,0 +1,19 @@
export function isLiteral(node, value) {
if (node?.type !== 'Literal') {
return false;
}
return node.value === value;
}
export const isStringLiteral = node => node?.type === 'Literal' && typeof node.value === 'string';
export const isNumericLiteral = node => node.type === 'Literal' && typeof node.value === 'number';
export const isRegexLiteral = node => node.type === 'Literal' && Boolean(node.regex);
export const isNullLiteral = node => node?.type === 'Literal' && node.raw === 'null';
export const isBigIntLiteral = node => node.type === 'Literal' && Boolean(node.bigint);
export const isEmptyStringLiteral = node => isLiteral(node, '');
+155
View File
@@ -0,0 +1,155 @@
import cleanRegexp from 'clean-regexp';
import regexpTree from 'regexp-tree';
import escapeString from './utils/escape-string.js';
import {isStringLiteral, isNewExpression, isRegexLiteral} from './ast/index.js';
const MESSAGE_ID = 'better-regex';
const MESSAGE_ID_PARSE_ERROR = 'better-regex/parse-error';
const messages = {
[MESSAGE_ID]: '{{original}} can be optimized to {{optimized}}.',
[MESSAGE_ID_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}',
};
// `regexp-tree` can optimize `/|/` into `//`, which is not valid JavaScript syntax.
// Normalize to an explicit empty alternative so the autofix always stays parseable.
const normalizeOptimizedRegexLiteral = optimizedRegexLiteral => (
optimizedRegexLiteral.startsWith('//')
? `/(?:)/${optimizedRegexLiteral.slice(2)}`
: optimizedRegexLiteral
);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sortCharacterClasses} = context.options[0];
const ignoreList = [];
if (sortCharacterClasses === false) {
ignoreList.push('charClassClassrangesMerge');
}
context.on('Literal', node => {
if (!isRegexLiteral(node)) {
return;
}
const {raw: original, regex} = node;
// Regular Expressions with `u` and `v` flag are not well handled by `regexp-tree`
// https://github.com/DmitrySoshnikov/regexp-tree/issues/162
if (regex.flags.includes('u') || regex.flags.includes('v')) {
return;
}
let optimized = original;
try {
optimized = regexpTree.optimize(original, undefined, {blacklist: ignoreList}).toString();
optimized = normalizeOptimizedRegexLiteral(optimized);
} catch (error) {
return {
node,
messageId: MESSAGE_ID_PARSE_ERROR,
data: {
original,
error: error.message,
},
};
}
if (original === optimized) {
return;
}
const problem = {
node,
messageId: MESSAGE_ID,
data: {
original,
optimized,
},
};
if (
node.parent.type === 'MemberExpression'
&& node.parent.object === node
&& !node.parent.optional
&& !node.parent.computed
&& node.parent.property.type === 'Identifier'
&& (
node.parent.property.name === 'toString'
|| node.parent.property.name === 'source'
)
) {
return problem;
}
return Object.assign(problem, {
fix: fixer => fixer.replaceText(node, optimized),
});
});
context.on('NewExpression', node => {
if (!isNewExpression(node, {name: 'RegExp', minimumArguments: 1})) {
return;
}
const [patternNode, flagsNode] = node.arguments;
if (!isStringLiteral(patternNode)) {
return;
}
const oldPattern = patternNode.value;
const flags = isStringLiteral(flagsNode)
? flagsNode.value
: '';
const newPattern = cleanRegexp(oldPattern, flags);
if (oldPattern !== newPattern) {
return {
node,
messageId: MESSAGE_ID,
data: {
original: oldPattern,
optimized: newPattern,
},
fix: fixer => fixer.replaceText(
patternNode,
escapeString(newPattern, patternNode.raw.charAt(0)),
),
};
}
});
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
sortCharacterClasses: {
type: 'boolean',
description: 'Whether to sort character classes.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Improve regexes by making them shorter, consistent, and safer.',
recommended: false,
},
fixable: 'code',
schema,
defaultOptions: [{sortCharacterClasses: true}],
messages,
},
};
export default config;
+132
View File
@@ -0,0 +1,132 @@
import {isRegExp} from 'node:util/types';
import {findVariable} from '@eslint-community/eslint-utils';
import {getAvailableVariableName, upperFirst} from './utils/index.js';
import {renameVariable} from './fix/index.js';
import {isMethodCall} from './ast/index.js';
const MESSAGE_ID = 'catch-error-name';
const messages = {
[MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.',
};
// - `promise.then(…, foo => {})`
// - `promise.then(…, function(foo) {})`
// - `promise.catch(foo => {})`
// - `promise.catch(function(foo) {})`
const isPromiseCatchParameter = node =>
(node.parent.type === 'FunctionExpression' || node.parent.type === 'ArrowFunctionExpression')
&& node.parent.params[0] === node
&& (
isMethodCall(node.parent.parent, {
method: 'then',
argumentsLength: 2,
optionalCall: false,
})
|| isMethodCall(node.parent.parent, {
method: 'catch',
argumentsLength: 1,
optionalCall: false,
})
)
&& node.parent.parent.arguments.at(-1) === node.parent;
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const options = context.options[0];
const {name: expectedName} = options;
const ignore = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
const isNameAllowed = name =>
name === expectedName
|| ignore.some(regexp => regexp.test(name))
|| name.endsWith(expectedName)
|| name.endsWith(upperFirst(expectedName));
context.on('Identifier', node => {
if (
!(node.parent.type === 'CatchClause' && node.parent.param === node)
&& !isPromiseCatchParameter(node)
) {
return;
}
const originalName = node.name;
if (
isNameAllowed(originalName)
|| isNameAllowed(originalName.replaceAll(/_+$/g, ''))
) {
return;
}
const scope = context.sourceCode.getScope(node);
const variable = findVariable(scope, node);
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768072967
// But can't reproduce, just ignore this case
/* c8 ignore next 3 */
if (!variable) {
return;
}
if (originalName === '_' && variable.references.length === 0) {
return;
}
const scopes = [
variable.scope,
...variable.references.map(({from}) => from),
];
const fixedName = getAvailableVariableName(expectedName, scopes);
const problem = {
node,
messageId: MESSAGE_ID,
data: {
originalName,
fixedName: fixedName || expectedName,
},
};
if (fixedName) {
problem.fix = fixer => renameVariable(variable, fixedName, context, fixer);
}
return problem;
});
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
name: {
type: 'string',
description: 'The expected name for the error variable.',
},
ignore: {
type: 'array',
uniqueItems: true,
description: 'Patterns to ignore.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce a specific parameter name in catch clauses.',
recommended: true,
},
fixable: 'code',
schema,
defaultOptions: [{name: 'error', ignore: []}],
messages,
},
};
export default config;
+100
View File
@@ -0,0 +1,100 @@
const MESSAGE_ID_ERROR = 'consistent-assert/error';
const messages = {
[MESSAGE_ID_ERROR]: 'Prefer `{{name}}.ok(…)` over `{{name}}(…)`.',
};
/**
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier | import('estree').ImportSpecifier | import('estree').ImportDeclaration} node
*/
const isValueImport = node => !node.importKind || node.importKind === 'value';
/**
Check if a specifier is `assert` function.
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier} specifier
@param {string} moduleName
*/
const isAssertFunction = (specifier, moduleName) =>
// `import assert from 'node:assert';`
// `import assert from 'node:assert/strict';`
specifier.type === 'ImportDefaultSpecifier'
// `import {default as assert} from 'node:assert';`
// `import {default as assert} from 'node:assert/strict';`
|| (
specifier.type === 'ImportSpecifier'
&& specifier.imported.type === 'Identifier'
&& specifier.imported.name === 'default'
)
// `import {strict as assert} from 'node:assert';`
|| (
moduleName === 'assert'
&& specifier.type === 'ImportSpecifier'
&& specifier.imported.type === 'Identifier'
&& specifier.imported.name === 'strict'
);
const NODE_PROTOCOL = 'node:';
/** @type {import('eslint').Rule.RuleModule['create']} */
const create = context => {
context.on('ImportDeclaration', function * (importDeclaration) {
if (!isValueImport(importDeclaration)) {
return;
}
let moduleName = importDeclaration.source.value;
if (moduleName.startsWith(NODE_PROTOCOL)) {
moduleName = moduleName.slice(NODE_PROTOCOL.length);
}
if (moduleName !== 'assert' && moduleName !== 'assert/strict') {
return;
}
for (const specifier of importDeclaration.specifiers) {
if (!isValueImport(specifier) || !isAssertFunction(specifier, moduleName)) {
continue;
}
const variables = context.sourceCode.getDeclaredVariables(specifier);
/* c8 ignore next 3 */
if (!Array.isArray(variables) && variables.length === 1) {
continue;
}
const [variable] = variables;
for (const {identifier} of variable.references) {
if (!(identifier.parent.type === 'CallExpression' && identifier.parent.callee === identifier)) {
continue;
}
yield {
node: identifier,
messageId: MESSAGE_ID_ERROR,
data: {name: identifier.name},
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => fixer.insertTextAfter(identifier, '.ok'),
};
}
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Enforce consistent assertion style with `node:assert`.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,54 @@
import {isMethodCall, isNewExpression} from './ast/index.js';
import {removeMethodCall} from './fix/index.js';
const MESSAGE_ID_ERROR = 'consistent-date-clone/error';
const messages = {
[MESSAGE_ID_ERROR]: 'Unnecessary `.getTime()` call.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('NewExpression', newExpression => {
if (!isNewExpression(newExpression, {name: 'Date', argumentsLength: 1})) {
return;
}
const [callExpression] = newExpression.arguments;
if (!isMethodCall(callExpression, {
method: 'getTime',
argumentsLength: 0,
optionalCall: false,
optionalMember: false,
})) {
return;
}
const {sourceCode} = context;
return {
node: callExpression,
loc: {
start: sourceCode.getLoc(callExpression.callee.property).start,
end: sourceCode.getLoc(callExpression).end,
},
messageId: MESSAGE_ID_ERROR,
fix: fixer => removeMethodCall(fixer, callExpression, context),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer passing `Date` directly to the constructor when cloning.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,291 @@
import {findVariable} from '@eslint-community/eslint-utils';
import {getAvailableVariableName, isLeftHandSide} from './utils/index.js';
import {isCallOrNewExpression} from './ast/index.js';
const MESSAGE_ID = 'consistentDestructuring';
const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest';
const isSimpleExpression = expression => {
while (expression) {
if (expression.computed) {
return false;
}
if (expression.type !== 'MemberExpression') {
break;
}
expression = expression.object;
}
return expression.type === 'Identifier'
|| expression.type === 'ThisExpression';
};
const isChildInParentScope = (child, parent) => {
while (child) {
if (child === parent) {
return true;
}
child = child.upper;
}
return false;
};
const getRootIdentifier = expression => {
while (expression?.type === 'MemberExpression') {
expression = expression.object;
}
return expression?.type === 'Identifier' ? expression : undefined;
};
const hasRestElement = pattern => {
switch (pattern?.type) {
case 'RestElement': {
return true;
}
case 'AssignmentPattern': {
return hasRestElement(pattern.left);
}
case 'ObjectPattern': {
return pattern.properties.some(property =>
hasRestElement(property.type === 'Property' ? property.value : property));
}
case 'ArrayPattern': {
return pattern.elements.some(element =>
hasRestElement(element));
}
default: {
return false;
}
}
};
const isIdentifierProperty = property =>
property.type === 'Property'
&& property.key.type === 'Identifier';
const hasNestedRestElement = pattern => {
switch (pattern?.type) {
case 'AssignmentPattern': {
return hasNestedRestElement(pattern.left);
}
case 'ObjectPattern': {
return pattern.properties.some(property => {
if (property.type === 'RestElement') {
return false;
}
return hasRestElement(property.value);
});
}
case 'ArrayPattern': {
return pattern.elements.some(element => {
if (!element || element.type === 'RestElement') {
return false;
}
return hasRestElement(element);
});
}
default: {
return false;
}
}
};
const isMemberDestructuredInNestedPatternWithRest = (objectPattern, memberName) =>
objectPattern.properties.some(property =>
isIdentifierProperty(property)
&& property.key.name === memberName
&& property.value.type !== 'Identifier'
&& hasNestedRestElement(property.value));
const isRootVariableReassigned = (declaration, memberExpressionNode, memberScope, sourceCode) => {
if (!declaration.rootVariable) {
return false;
}
const [, declarationEnd] = sourceCode.getRange(declaration.object);
const [memberStart] = sourceCode.getRange(memberExpressionNode);
return declaration.rootVariable.references.some(reference => {
if (!reference.isWrite()) {
return false;
}
const [referenceStart] = sourceCode.getRange(reference.identifier);
if (referenceStart < declarationEnd) {
return false;
}
// Be conservative: writes from other variable scopes may run before this read via calls/closures.
if (reference.from.variableScope !== memberScope.variableScope) {
return true;
}
return referenceStart <= memberStart;
});
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
const declarations = new Map();
context.on('VariableDeclarator', node => {
if (!(
node.id.type === 'ObjectPattern'
&& node.init
&& node.init.type !== 'Literal'
// Ignore any complex expressions (e.g. arrays, functions)
&& isSimpleExpression(node.init)
)) {
return;
}
const rootIdentifier = getRootIdentifier(node.init);
declarations.set(sourceCode.getText(node.init), {
scope: sourceCode.getScope(node),
object: node.init,
rootIdentifierName: rootIdentifier?.name,
rootVariable: rootIdentifier && findVariable(sourceCode.getScope(node), rootIdentifier),
objectPattern: node.id,
});
});
context.on('MemberExpression', node => {
if (
node.computed
|| (
isCallOrNewExpression(node.parent)
&& node.parent.callee === node
)
|| isLeftHandSide(node)
) {
return;
}
const declaration = declarations.get(sourceCode.getText(node.object));
if (!declaration) {
return;
}
const memberScope = sourceCode.getScope(node);
const memberRootIdentifier = getRootIdentifier(node.object);
const memberRootVariable = memberRootIdentifier && findVariable(memberScope, memberRootIdentifier);
if (
declaration.rootIdentifierName
&& memberRootIdentifier?.name === declaration.rootIdentifierName
&& memberRootVariable !== declaration.rootVariable
) {
return;
}
if (isRootVariableReassigned(declaration, node, memberScope, sourceCode)) {
return;
}
const {scope, objectPattern} = declaration;
// Property is destructured outside the current scope
if (!isChildInParentScope(memberScope, scope)) {
return;
}
const member = sourceCode.getText(node.property);
const memberDestructuredInNestedPattern = isMemberDestructuredInNestedPatternWithRest(objectPattern, member);
const destructuredProperties = objectPattern.properties.filter(property =>
isIdentifierProperty(property)
&& property.value.type === 'Identifier');
const lastProperty = objectPattern.properties.at(-1);
const hasRest = lastProperty?.type === 'RestElement';
const expression = sourceCode.getText(node);
// Member might already be destructured
const destructuredMember = destructuredProperties.find(property =>
property.key.name === member);
if (!destructuredMember) {
if (memberDestructuredInNestedPattern) {
return;
}
// Don't destructure additional members when rest is used
if (hasRest) {
return;
}
// Destructured member collides with an existing identifier
if (getAvailableVariableName(member, [memberScope]) !== member) {
return;
}
}
// Don't try to fix nested member expressions
if (node.parent.type === 'MemberExpression') {
return {
node,
messageId: MESSAGE_ID,
};
}
const newMember = destructuredMember ? destructuredMember.value.name : member;
return {
node,
messageId: MESSAGE_ID,
suggest: [{
messageId: MESSAGE_ID_SUGGEST,
data: {
expression,
property: newMember,
},
* fix(fixer) {
const {properties} = objectPattern;
const lastProperty = properties.at(-1);
yield fixer.replaceText(node, newMember);
if (!destructuredMember) {
yield lastProperty
? fixer.insertTextAfter(lastProperty, `, ${newMember}`)
: fixer.replaceText(objectPattern, `{${newMember}}`);
}
},
}],
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Use destructured variables over properties.',
recommended: false,
},
hasSuggestions: true,
messages: {
[MESSAGE_ID]: 'Use destructured variables over properties.',
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.',
},
},
};
export default config;
@@ -0,0 +1,123 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {
isEmptyArrayExpression,
isEmptyStringLiteral,
} from './ast/index.js';
const MESSAGE_ID = 'consistent-empty-array-spread';
const messages = {
[MESSAGE_ID]: 'Prefer using empty {{replacementDescription}} since the {{anotherNodePosition}} is {{anotherNodeDescription}}.',
};
const isString = (node, context) => {
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
return typeof staticValueResult?.value === 'string';
};
const isArray = (node, context) => {
if (node.type === 'ArrayExpression') {
return true;
}
const staticValueResult = getStaticValue(node, context.sourceCode.getScope(node));
return Array.isArray(staticValueResult?.value);
};
const cases = [
{
oneSidePredicate: isEmptyStringLiteral,
anotherSidePredicate: isArray,
anotherNodeDescription: 'an array',
replacementDescription: 'array',
replacementCode: '[]',
},
{
oneSidePredicate: isEmptyArrayExpression,
anotherSidePredicate: isString,
anotherNodeDescription: 'a string',
replacementDescription: 'string',
replacementCode: '\'\'',
},
];
function createProblem({
problemNode,
anotherNodePosition,
anotherNodeDescription,
replacementDescription,
replacementCode,
}) {
return {
node: problemNode,
messageId: MESSAGE_ID,
data: {
replacementDescription,
anotherNodePosition,
anotherNodeDescription,
},
fix: fixer => fixer.replaceText(problemNode, replacementCode),
};
}
function getProblem(conditionalExpression, context) {
const {
consequent,
alternate,
} = conditionalExpression;
for (const problemCase of cases) {
const {
oneSidePredicate,
anotherSidePredicate,
} = problemCase;
if (oneSidePredicate(consequent, context) && anotherSidePredicate(alternate, context)) {
return createProblem({
...problemCase,
problemNode: consequent,
anotherNodePosition: 'alternate',
});
}
if (oneSidePredicate(alternate, context) && anotherSidePredicate(consequent, context)) {
return createProblem({
...problemCase,
problemNode: alternate,
anotherNodePosition: 'consequent',
});
}
}
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('ArrayExpression', function * (arrayExpression) {
for (const element of arrayExpression.elements) {
if (
element?.type !== 'SpreadElement'
|| element.argument.type !== 'ConditionalExpression'
) {
continue;
}
yield getProblem(element.argument, context);
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer consistent types when spreading a ternary in an array literal.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,136 @@
import toLocation from './utils/to-location.js';
import {isMethodCall, isNegativeOne, isNumericLiteral} from './ast/index.js';
const MESSAGE_ID = 'consistent-existence-index-check';
const messages = {
[MESSAGE_ID]: 'Prefer `{{replacementOperator}} {{replacementValue}}` over `{{originalOperator}} {{originalValue}}` to check {{existenceOrNonExistence}}.',
};
const isZero = node => isNumericLiteral(node) && node.value === 0;
/**
@param {parent: import('estree').BinaryExpression} binaryExpression
@returns {{
replacementOperator: string,
replacementValue: string,
originalOperator: string,
originalValue: string,
} | undefined}
*/
function getReplacement(binaryExpression) {
const {operator, right} = binaryExpression;
if (operator === '<' && isZero(right)) {
return {
replacementOperator: '===',
replacementValue: '-1',
originalOperator: operator,
originalValue: '0',
};
}
if (operator === '>' && isNegativeOne(right)) {
return {
replacementOperator: '!==',
replacementValue: '-1',
originalOperator: operator,
originalValue: '-1',
};
}
if (operator === '>=' && isZero(right)) {
return {
replacementOperator: '!==',
replacementValue: '-1',
originalOperator: operator,
originalValue: '0',
};
}
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('VariableDeclarator', /** @param {import('estree').VariableDeclarator} variableDeclarator */ function * (variableDeclarator) {
if (!(
variableDeclarator.parent.type === 'VariableDeclaration'
&& variableDeclarator.parent.kind === 'const'
&& variableDeclarator.id.type === 'Identifier'
&& isMethodCall(variableDeclarator.init, {methods: ['indexOf', 'lastIndexOf', 'findIndex', 'findLastIndex']})
)) {
return;
}
const variableIdentifier = variableDeclarator.id;
const variables = context.sourceCode.getDeclaredVariables(variableDeclarator);
const [variable] = variables;
// Just for safety
if (
variables.length !== 1
|| variable.identifiers.length !== 1
|| variable.identifiers[0] !== variableIdentifier
) {
return;
}
for (const {identifier} of variable.references) {
/** @type {{parent: import('estree').BinaryExpression}} */
const binaryExpression = identifier.parent;
if (binaryExpression.type !== 'BinaryExpression' || binaryExpression.left !== identifier) {
continue;
}
const replacement = getReplacement(binaryExpression);
if (!replacement) {
return;
}
const {left, operator, right} = binaryExpression;
const {sourceCode} = context;
const operatorToken = sourceCode.getTokenAfter(
left,
token => token.type === 'Punctuator' && token.value === operator,
);
const [start] = sourceCode.getRange(operatorToken);
const [, end] = sourceCode.getRange(right);
yield {
node: binaryExpression,
loc: toLocation([start, end], context),
messageId: MESSAGE_ID,
data: {
...replacement,
existenceOrNonExistence: `${replacement.replacementOperator === '===' ? 'non-' : ''}existence`,
},
* fix(fixer) {
yield fixer.replaceText(operatorToken, replacement.replacementOperator);
if (replacement.replacementValue !== replacement.originalValue) {
yield fixer.replaceText(right, replacement.replacementValue);
}
},
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description:
'Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,269 @@
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
import {getReferences, isNodeContainsLexicalThis, isNodeMatches} from './utils/index.js';
import {functionTypes} from './ast/index.js';
const MESSAGE_ID = 'consistent-function-scoping';
const messages = {
[MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.',
};
const isSameScope = (scope1, scope2) =>
scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block);
function checkReferences(scope, parent, scopeManager) {
const hitReference = references => references.some(reference => {
if (isSameScope(parent, reference.from)) {
return true;
}
const {resolved} = reference;
const [definition] = resolved.defs;
// Skip recursive function name
if (definition?.type === 'FunctionName' && resolved.name === definition.name.name) {
return false;
}
return isSameScope(parent, resolved.scope);
});
const hitDefinitions = definitions => definitions.some(definition => {
const scope = scopeManager.acquire(definition.node);
return isSameScope(parent, scope);
});
// This check looks for neighboring function definitions
const hitIdentifier = identifiers => identifiers.some(identifier => {
// Only look at identifiers that live in a FunctionDeclaration
if (
!identifier.parent
|| identifier.parent.type !== 'FunctionDeclaration'
) {
return false;
}
const identifierScope = scopeManager.acquire(identifier);
// If we have a scope, the earlier checks should have worked so ignore them here
/* c8 ignore next 3 */
if (identifierScope) {
return false;
}
const identifierParentScope = scopeManager.acquire(identifier.parent);
/* c8 ignore next 3 */
if (!identifierParentScope) {
return false;
}
// Ignore identifiers from our own scope
if (isSameScope(scope, identifierParentScope)) {
return false;
}
// Look at the scope above the function definition to see if it lives
// next to the reference being checked
return isSameScope(parent, identifierParentScope.upper);
});
return getReferences(scope)
.map(({resolved}) => resolved)
.filter(Boolean)
.some(variable =>
hitReference(variable.references)
|| hitDefinitions(variable.defs)
|| hitIdentifier(variable.identifiers));
}
// https://reactjs.org/docs/hooks-reference.html
const reactHooks = [
'useState',
'useEffect',
'useContext',
'useReducer',
'useCallback',
'useMemo',
'useRef',
'useImperativeHandle',
'useLayoutEffect',
'useDebugValue',
].flatMap(hookName => [hookName, `React.${hookName}`]);
const isReactHook = scope =>
scope.block?.parent?.callee
&& isNodeMatches(scope.block.parent.callee, reactHooks);
const isArrowFunctionNodeWithThis = (node, visitorKeys) =>
node.type === 'ArrowFunctionExpression'
// We avoid `scope.thisFound` because parser scope metadata differs; AST lexical checks are consistent.
// Include both params and body, because parameter defaults can reference lexical `this`.
&& isNodeContainsLexicalThis(node, visitorKeys);
const iifeFunctionTypes = new Set([
'FunctionExpression',
'ArrowFunctionExpression',
]);
const isIife = node =>
iifeFunctionTypes.has(node.type)
&& node.parent.type === 'CallExpression'
&& node.parent.callee === node;
// Helper to walk up the chain to find the first non-arrow ancestor
function getNonArrowAncestor(node) {
let ancestor = node;
while (ancestor && ancestor.type === 'ArrowFunctionExpression') {
ancestor = ancestor.parent;
}
return ancestor;
}
// Helper to skip over a chain of ArrowFunctionExpression nodes
function skipArrowFunctionChain(node) {
let current = node;
while (current.type === 'ArrowFunctionExpression') {
current = current.parent;
}
return current;
}
function handleNestedArrowFunctions(parentNode, node) {
// Skip over arrow function expressions when they are parents and we came from a ReturnStatement
// This handles nested arrow functions: return next => action => { ... }
// But only when we're in a return statement context
if (parentNode.type === 'ArrowFunctionExpression' && node.type === 'ArrowFunctionExpression') {
const ancestor = getNonArrowAncestor(parentNode);
if (ancestor && ancestor.type === 'ReturnStatement') {
parentNode = skipArrowFunctionChain(parentNode);
if (parentNode.type === 'ReturnStatement') {
parentNode = parentNode.parent;
}
}
}
return parentNode;
}
function checkNode(node, scopeManager, sourceCode) {
const scope = scopeManager.acquire(node);
if (
!scope
|| isArrowFunctionNodeWithThis(node, sourceCode.visitorKeys)
) {
return true;
}
let parentNode = node.parent;
// Skip over junk like the block statement inside of a function declaration
// or the various pieces of an arrow function.
if (parentNode.type === 'VariableDeclarator') {
parentNode = parentNode.parent;
}
if (parentNode.type === 'VariableDeclaration') {
parentNode = parentNode.parent;
}
// Only skip ReturnStatement for arrow functions
// Regular function expressions have different semantics and shouldn't be moved
if (parentNode?.type === 'ReturnStatement' && node.type === 'ArrowFunctionExpression') {
parentNode = parentNode.parent;
}
parentNode = handleNestedArrowFunctions(parentNode, node);
if (parentNode?.type === 'BlockStatement') {
parentNode = parentNode.parent;
}
const parentScope = scopeManager.acquire(parentNode);
if (
!parentScope
|| parentScope.type === 'global'
|| isReactHook(parentScope)
|| isIife(parentNode)
) {
return true;
}
return checkReferences(scope, parentScope, scopeManager);
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {checkArrowFunctions} = context.options[0];
const {sourceCode} = context;
const {scopeManager} = sourceCode;
const functions = [];
context.on(functionTypes, () => {
functions.push(false);
});
context.on('JSXElement', () => {
// Turn off this rule if we see a JSX element because scope
// references does not include JSXElement nodes.
if (functions.length > 0) {
functions[functions.length - 1] = true;
}
});
context.onExit(functionTypes, node => {
const currentFunctionHasJsx = functions.pop();
if (currentFunctionHasJsx) {
return;
}
if (node.type === 'ArrowFunctionExpression' && !checkArrowFunctions) {
return;
}
if (checkNode(node, scopeManager, sourceCode)) {
return;
}
return {
node,
loc: getFunctionHeadLocation(node, sourceCode),
messageId: MESSAGE_ID,
data: {
functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
},
};
});
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
checkArrowFunctions: {
type: 'boolean',
description: 'Whether to check arrow functions.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Move function definitions to the highest possible scope.',
recommended: true,
},
schema,
defaultOptions: [{checkArrowFunctions: true}],
messages,
},
};
export default config;
@@ -0,0 +1,52 @@
import {replaceTemplateElement} from './fix/index.js';
import {isTaggedTemplateLiteral} from './ast/index.js';
const MESSAGE_ID = 'consistent-template-literal-escape';
const messages = {
[MESSAGE_ID]: 'Use `\\${` instead of `$\\{` to escape in template literals.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('TemplateElement', node => {
if (isTaggedTemplateLiteral(node.parent)) {
return;
}
const {raw} = node.value;
// Match `$\{` or `\$\{` and replace with `\${`.
// The `\\?` makes the leading backslash optional to handle both patterns.
// The lookbehind ensures an even number of preceding backslashes (including zero).
const fixedRaw = raw.replaceAll(
/(?<=(?:^|[^\\])(?:\\\\)*)\\?\$\\{/g,
String.raw`\${`,
);
if (raw !== fixedRaw) {
const problem = {
node,
messageId: MESSAGE_ID,
fix: fixer => replaceTemplateElement(node, fixedRaw, context, fixer),
};
return problem;
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce consistent style for escaping `${` in template literals.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,246 @@
import {
upperFirst,
getParenthesizedText,
isNodeMatchesNameOrPath,
} from './utils/index.js';
const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
const messages = {
[MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class',
};
const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
const getNameProperty = className => `
name = '${className}';
`;
const getSuperClassName = superClass => {
if (superClass?.type === 'Identifier') {
return superClass.name;
}
if (
superClass?.type === 'MemberExpression'
&& !superClass.computed
&& superClass.property.type === 'Identifier'
) {
return superClass.property.name;
}
};
const hasValidSuperClass = node => {
const superClassName = getSuperClassName(node.superClass);
return Boolean(superClassName) && nameRegexp.test(superClassName);
};
const isSuperExpression = node =>
node.type === 'ExpressionStatement'
&& node.expression.type === 'CallExpression'
&& isNodeMatchesNameOrPath(node.expression.callee, 'super');
const isAssignmentExpression = (node, name) =>
node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& isNodeMatchesNameOrPath(node.expression.left, `this.${name}`);
const createInvalidNameError = (node, name) => ({
node,
message: `The \`name\` property should be set to \`${name}\`.`,
});
const isPropertyDefinition = (node, name) =>
node.type === 'PropertyDefinition'
&& !node.static
&& !node.computed
&& node.key.type === 'Identifier'
&& node.key.name === name;
const isValidNameProperty = (nameProperty, className) =>
nameProperty?.value
&& nameProperty.value.value === className;
function * checkConstructorBody(context, constructor, name, nameProperty) {
const {sourceCode} = context;
const constructorBodyNode = constructor.value.body;
// Verify the constructor has a body (TypeScript)
if (!constructorBodyNode) {
return;
}
const constructorBody = constructorBodyNode.body;
const superExpression = constructorBody.find(bodyNode => isSuperExpression(bodyNode));
const messageExpressionIndex = constructorBody.findIndex(bodyNode => isAssignmentExpression(bodyNode, 'message'));
if (!superExpression) {
yield {
node: constructorBodyNode,
message: 'Missing call to `super()` in constructor.',
};
} else if (messageExpressionIndex !== -1) {
const expression = constructorBody[messageExpressionIndex];
yield {
node: superExpression,
message: 'Pass the error message to `super()` instead of setting `this.message`.',
* fix(fixer) {
if (superExpression.expression.arguments.length === 0) {
const rhs = expression.expression.right;
const [start] = sourceCode.getRange(superExpression);
// This part crashes on ESLint 10, but it's still not correct.
// There can be spaces, comments after `super`
yield fixer.insertTextAfterRange(
[start, start + 6],
getParenthesizedText(rhs, context),
);
}
const start = messageExpressionIndex === 0
? sourceCode.getRange(constructorBodyNode)[0]
: sourceCode.getRange(constructorBody[messageExpressionIndex - 1])[1];
const [, end] = sourceCode.getRange(expression);
yield fixer.removeRange([start, end]);
},
};
}
const nameExpression = constructorBody.find(bodyNode => isAssignmentExpression(bodyNode, 'name'));
if (!nameExpression) {
if (!isValidNameProperty(nameProperty, name)) {
yield createInvalidNameError(nameProperty?.value ?? constructorBodyNode, name);
}
return;
}
if (
nameExpression.expression.right.type !== 'Literal'
|| nameExpression.expression.right.value !== name
) {
yield createInvalidNameError(nameExpression.expression.right ?? constructorBodyNode, name);
}
}
function * customErrorDefinition(context, node) {
if (!hasValidSuperClass(node)) {
return;
}
if (node.id === null) {
return;
}
const {name} = node.id;
const className = getClassName(name);
if (name !== className) {
yield {
node: node.id,
message: `Invalid class name, use \`${className}\`.`,
};
}
const {body} = node.body;
const {sourceCode} = context;
const constructor = body.find(x => x.kind === 'constructor');
const nameProperty = body.find(classNode => isPropertyDefinition(classNode, 'name'));
if (!constructor) {
if (isValidNameProperty(nameProperty, name)) {
return;
}
const range = sourceCode.getRange(node.body);
yield {
...createInvalidNameError(nameProperty?.value ?? node, name),
fix(fixer) {
if (nameProperty?.value) {
return fixer.replaceText(nameProperty.value, `'${name}'`);
}
if (nameProperty) {
return fixer.replaceText(nameProperty, getNameProperty(name).trim());
}
return fixer.insertTextAfterRange([
range[0],
range[0] + 1,
], getNameProperty(name));
},
};
return;
}
yield * checkConstructorBody(context, constructor, name, nameProperty);
}
const customErrorExport = (context, node) => {
const exportsName = node.left.property.name;
const maybeError = node.right;
if (maybeError.type !== 'ClassExpression') {
return;
}
if (!hasValidSuperClass(maybeError)) {
return;
}
if (!maybeError.id) {
return;
}
// Assume rule has already fixed the error name
const errorName = maybeError.id.name;
if (exportsName === errorName) {
return;
}
return {
node: node.left.property,
messageId: MESSAGE_ID_INVALID_EXPORT,
fix: fixer => fixer.replaceText(node.left.property, errorName),
};
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('ClassDeclaration', node => customErrorDefinition(context, node));
context.on('AssignmentExpression', node => {
if (node.right.type === 'ClassExpression') {
return customErrorDefinition(context, node.right);
}
});
context.on('AssignmentExpression', node => {
if (
node.left.type === 'MemberExpression'
&& node.left.object.type === 'Identifier'
&& node.left.object.name === 'exports'
) {
return customErrorExport(context, node);
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Enforce correct `Error` subclassing.',
recommended: false,
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,62 @@
import {isOpeningBraceToken} from '@eslint-community/eslint-utils';
const MESSAGE_ID = 'empty-brace-spaces';
const messages = {
[MESSAGE_ID]: 'Do not add spaces between braces.',
};
const getProblem = (node, context) => {
const {sourceCode} = context;
const openingBrace = sourceCode.getFirstToken(node, {filter: isOpeningBraceToken});
const closingBrace = sourceCode.getLastToken(node);
const [, start] = sourceCode.getRange(openingBrace);
const [end] = sourceCode.getRange(closingBrace);
const textBetween = sourceCode.text.slice(start, end);
if (!/^\s+$/.test(textBetween)) {
return;
}
return {
loc: {
start: sourceCode.getLoc(openingBrace).end,
end: sourceCode.getLoc(closingBrace).start,
},
messageId: MESSAGE_ID,
fix: fixer => fixer.removeRange([start, end]),
};
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on([
'BlockStatement',
'ClassBody',
'StaticBlock',
'ObjectExpression',
], node => {
const children = node.type === 'ObjectExpression' ? node.properties : node.body;
if (children.length > 0) {
return;
}
return getProblem(node, context);
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'layout',
docs: {
description: 'Enforce no spaces between braces.',
recommended: true,
},
fixable: 'whitespace',
messages,
},
};
export default config;
+98
View File
@@ -0,0 +1,98 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {isCallOrNewExpression} from './ast/index.js';
import builtinErrors from './shared/builtin-errors.js';
const MESSAGE_ID_MISSING_MESSAGE = 'missing-message';
const MESSAGE_ID_EMPTY_MESSAGE = 'message-is-empty-string';
const MESSAGE_ID_NOT_STRING = 'message-is-not-a-string';
const messages = {
[MESSAGE_ID_MISSING_MESSAGE]: 'Pass a message to the `{{constructorName}}` constructor.',
[MESSAGE_ID_EMPTY_MESSAGE]: 'Error message should not be an empty string.',
[MESSAGE_ID_NOT_STRING]: 'Error message should be a string.',
};
const messageArgumentIndexes = new Map([
['AggregateError', 1],
['SuppressedError', 2],
]);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on(['CallExpression', 'NewExpression'], expression => {
if (!(
isCallOrNewExpression(expression, {
names: builtinErrors,
optional: false,
})
&& context.sourceCode.isGlobalReference(expression.callee)
)) {
return;
}
const constructorName = expression.callee.name;
const messageArgumentIndex = messageArgumentIndexes.has(constructorName)
? messageArgumentIndexes.get(constructorName)
: 0;
const callArguments = expression.arguments;
// If message is `SpreadElement` or there is `SpreadElement` before message
if (callArguments.some((node, index) => index <= messageArgumentIndex && node.type === 'SpreadElement')) {
return;
}
const node = callArguments[messageArgumentIndex];
if (!node) {
return {
node: expression,
messageId: MESSAGE_ID_MISSING_MESSAGE,
data: {constructorName},
};
}
// These types can't be string, and `getStaticValue` may don't know the value
// Add more types, if issue reported
if (node.type === 'ArrayExpression' || node.type === 'ObjectExpression') {
return {
node,
messageId: MESSAGE_ID_NOT_STRING,
};
}
const staticResult = getStaticValue(node, context.sourceCode.getScope(node));
// We don't know the value of `message`
if (!staticResult) {
return;
}
const {value} = staticResult;
if (typeof value !== 'string') {
return {
node,
messageId: MESSAGE_ID_NOT_STRING,
};
}
if (value === '') {
return {
node,
messageId: MESSAGE_ID_EMPTY_MESSAGE,
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Enforce passing a `message` value when creating a built-in error.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
+87
View File
@@ -0,0 +1,87 @@
import {replaceTemplateElement} from './fix/index.js';
import {isRegexLiteral, isStringLiteral, isTaggedTemplateLiteral} from './ast/index.js';
const MESSAGE_ID_UPPERCASE = 'escape-uppercase';
const MESSAGE_ID_LOWERCASE = 'escape-lowercase';
const messages = {
[MESSAGE_ID_UPPERCASE]: 'Use uppercase characters for the value of the escape sequence.',
[MESSAGE_ID_LOWERCASE]: 'Use lowercase characters for the value of the escape sequence.',
};
const escapeCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?<data>x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+})/g;
const escapePatternCase = /(?<=(?:^|[^\\])(?:\\\\)*\\)(?<data>x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+}|c[A-Za-z])/g;
const getProblem = ({node, original, regex = escapeCase, lowercase, fix}) => {
const fixed = original.replace(regex, data => data[0] + data.slice(1)[lowercase ? 'toLowerCase' : 'toUpperCase']());
if (fixed !== original) {
return {
node,
messageId: lowercase ? MESSAGE_ID_LOWERCASE : MESSAGE_ID_UPPERCASE,
fix: fixer => fix ? fix(fixer, fixed) : fixer.replaceText(node, fixed),
};
}
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const lowercase = context.options[0] === 'lowercase';
context.on('Literal', node => {
if (isStringLiteral(node)) {
return getProblem({
node,
original: node.raw,
lowercase,
});
}
});
context.on('Literal', node => {
if (isRegexLiteral(node)) {
return getProblem({
node,
original: node.raw,
regex: escapePatternCase,
lowercase,
});
}
});
context.on('TemplateElement', node => {
if (isTaggedTemplateLiteral(node.parent, ['String.raw'])) {
return;
}
return getProblem({
node,
original: node.value.raw,
lowercase,
fix: (fixer, fixed) => replaceTemplateElement(node, fixed, context, fixer),
});
});
};
const schema = [
{
enum: ['uppercase', 'lowercase'],
description: 'The case style for escape sequences.',
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Require escape sequences to use uppercase or lowercase values.',
recommended: 'unopinionated',
},
fixable: 'code',
schema,
defaultOptions: ['uppercase'],
messages,
},
};
export default config;
@@ -0,0 +1,577 @@
import path from 'node:path';
import {isRegExp} from 'node:util/types';
import semver from 'semver';
import * as ci from 'ci-info';
import {
isEslintDisableOrEnableDirective,
getBuiltinRule,
} from './utils/index.js';
import {readPackageJson} from './shared/package-json.js';
const baseRule = getBuiltinRule('no-warning-comments');
// `unicorn/` prefix is added to avoid conflicts with core rule
const MESSAGE_ID_AVOID_MULTIPLE_DATES = 'unicorn/avoidMultipleDates';
const MESSAGE_ID_EXPIRED_TODO = 'unicorn/expiredTodo';
const MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS
= 'unicorn/avoidMultiplePackageVersions';
const MESSAGE_ID_REACHED_PACKAGE_VERSION = 'unicorn/reachedPackageVersion';
const MESSAGE_ID_HAVE_PACKAGE = 'unicorn/havePackage';
const MESSAGE_ID_DONT_HAVE_PACKAGE = 'unicorn/dontHavePackage';
const MESSAGE_ID_VERSION_MATCHES = 'unicorn/versionMatches';
const MESSAGE_ID_ENGINE_MATCHES = 'unicorn/engineMatches';
const MESSAGE_ID_REMOVE_WHITESPACE = 'unicorn/removeWhitespaces';
const MESSAGE_ID_MISSING_AT_SYMBOL = 'unicorn/missingAtSymbol';
// Override of core rule message with a more specific one - no prefix
const MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT = 'unexpectedComment';
const messages = {
[MESSAGE_ID_AVOID_MULTIPLE_DATES]:
'Avoid using multiple expiration dates in TODO: {{expirationDates}}. {{message}}',
[MESSAGE_ID_EXPIRED_TODO]:
'There is a TODO that is past due date: {{expirationDate}}. {{message}}',
[MESSAGE_ID_REACHED_PACKAGE_VERSION]:
'There is a TODO that is past due package version: {{comparison}}. {{message}}',
[MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS]:
'Avoid using multiple package versions in TODO: {{versions}}. {{message}}',
[MESSAGE_ID_HAVE_PACKAGE]:
'There is a TODO that is deprecated since you installed: {{package}}. {{message}}',
[MESSAGE_ID_DONT_HAVE_PACKAGE]:
'There is a TODO that is deprecated since you uninstalled: {{package}}. {{message}}',
[MESSAGE_ID_VERSION_MATCHES]:
'There is a TODO match for package version: {{comparison}}. {{message}}',
[MESSAGE_ID_ENGINE_MATCHES]:
'There is a TODO match for Node.js version: {{comparison}}. {{message}}',
[MESSAGE_ID_REMOVE_WHITESPACE]:
'Avoid using whitespace on TODO argument. On \'{{original}}\' use \'{{fix}}\'. {{message}}',
[MESSAGE_ID_MISSING_AT_SYMBOL]:
'Missing \'@\' on TODO argument. On \'{{original}}\' use \'{{fix}}\'. {{message}}',
...baseRule.meta.messages,
[MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT]:
'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.',
};
/** @param {string} dirname */
function getPackageHelpers(dirname) {
const packageJsonResult = readPackageJson(dirname);
const packageJson = packageJsonResult?.packageJson ?? {};
const hasPackage = Boolean(packageJsonResult);
const packageDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
function parseTodoWithArguments(string, {terms}) {
const lowerCaseString = string.toLowerCase();
const lowerCaseTerms = terms.map(term => term.toLowerCase());
const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term));
if (!hasTerm) {
return false;
}
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i;
const result = TODO_ARGUMENT_RE.exec(string);
if (!result) {
return false;
}
const {rawArguments} = result.groups;
const parsedArguments = rawArguments
.split(',')
.map(argument => parseArgument(argument.trim()));
return createArgumentGroup(parsedArguments);
}
function parseArgument(argumentString, dirname) {
const {hasPackage} = getPackageHelpers(dirname);
if (ISO8601_DATE.test(argumentString)) {
return {
type: 'dates',
value: argumentString,
};
}
if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) {
const condition = argumentString[0] === '+' ? 'in' : 'out';
const name = argumentString.slice(1).trim();
return {
type: 'dependencies',
value: {
name,
condition,
},
};
}
if (hasPackage && VERSION_COMPARISON_RE.test(argumentString)) {
const {groups} = VERSION_COMPARISON_RE.exec(argumentString);
const name = groups.name.trim();
const condition = groups.condition.trim();
const version = groups.version.trim();
const hasEngineKeyword = name.indexOf('engine:') === 0;
const isNodeEngine = hasEngineKeyword && name === 'engine:node';
if (hasEngineKeyword && isNodeEngine) {
return {
type: 'engines',
value: {
condition,
version,
},
};
}
if (!hasEngineKeyword) {
return {
type: 'dependencies',
value: {
name,
condition,
version,
},
};
}
}
if (hasPackage && PKG_VERSION_RE.test(argumentString)) {
const result = PKG_VERSION_RE.exec(argumentString);
const {condition, version} = result.groups;
return {
type: 'packageVersions',
value: {
condition: condition.trim(),
version: version.trim(),
},
};
}
// Currently being ignored as integration tests pointed
// some TODO comments have `[random data like this]`
return {
type: 'unknowns',
value: argumentString,
};
}
function parseTodoMessage(todoString) {
// @example "TODO [...]: message here"
// @example "TODO [...] message here"
const argumentsEnd = todoString.indexOf(']');
const afterArguments = todoString.slice(argumentsEnd + 1).trim();
// Check if have to skip colon
// @example "TODO [...]: message here"
const dropColon = afterArguments[0] === ':';
if (dropColon) {
return afterArguments.slice(1).trim();
}
return afterArguments;
}
return {
packageResult: packageJsonResult,
hasPackage,
packageJson,
packageDependencies,
parseArgument,
parseTodoMessage,
parseTodoWithArguments,
};
}
const DEPENDENCY_INCLUSION_RE = /^[+-]\s*@?\S+\/?\S+/;
const VERSION_COMPARISON_RE = /^(?<name>@?\S\/?\S+)@(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)/i;
const PKG_VERSION_RE = /^(?<condition>>|>=)(?<version>\d+(?:\.\d+){0,2}(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)\s*$/;
const ISO8601_DATE = /\d{4}-\d{2}-\d{2}/;
function createArgumentGroup(arguments_) {
const groups = {};
for (const {value, type} of arguments_) {
groups[type] ??= [];
groups[type].push(value);
}
return groups;
}
function reachedDate(past, now) {
return Date.parse(past) < Date.parse(now);
}
function tryToCoerceVersion(rawVersion) {
// `version` in `package.json` and comment can't be empty
/* c8 ignore next 3 */
if (!rawVersion) {
return false;
}
let version = String(rawVersion);
// Remove leading things like `^1.0.0`, `>1.0.0`
const leadingNoises = [
'>=',
'<=',
'>',
'<',
'~',
'^',
];
const foundTrailingNoise = leadingNoises.find(noise => version.startsWith(noise));
if (foundTrailingNoise) {
version = version.slice(foundTrailingNoise.length);
}
// Get only the first member for cases such as `1.0.0 - 2.9999.9999`
const parts = version.split(' ');
// We don't have this `package.json` to test
/* c8 ignore next 3 */
if (parts.length > 1) {
version = parts[0];
}
// We don't have this `package.json` to test
/* c8 ignore next 3 */
if (semver.valid(version)) {
return version;
}
try {
// Try to semver.parse a perfect match while semver.coerce tries to fix errors
// But coerce can't parse pre-releases.
return semver.parse(version) || semver.coerce(version);
} catch {
// We don't have this `package.json` to test
/* c8 ignore next 3 */
return false;
}
}
function satisfiesRange(version, condition, range) {
return semver.satisfies(version, `${condition}${range}`, {includePrerelease: true});
}
const DEFAULT_OPTIONS = {
terms: ['todo', 'fixme', 'xxx'],
ignore: [],
ignoreDates: false,
ignoreDatesOnPullRequests: true,
allowWarningComments: true,
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const options = {
date: new Date().toISOString().slice(0, 10),
...context.options[0],
};
const ignoreRegexes = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
const dirname = path.dirname(context.filename);
const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname);
const {sourceCode} = context;
const comments = sourceCode.getAllComments();
const unusedComments = comments
.filter(comment => comment.type !== 'Shebang' && !isEslintDisableOrEnableDirective(context, comment))
// Block comments come as one.
// Split for situations like this:
// /*
// * TODO [2999-01-01]: Validate this
// * TODO [2999-01-01]: And this
// * TODO [2999-01-01]: Also this
// */
.flatMap(comment =>
comment.value.split('\n').map(line => ({
...comment,
value: line,
}))).filter(comment => processComment(comment));
// This is highly dependable on ESLint's `no-warning-comments` implementation.
// What we do is patch the parts we know the rule will use, `getAllComments`.
// Since we have priority, we leave only the comments that we didn't use.
const fakeContext = new Proxy(context, {
get(target, property, receiver) {
if (property === 'sourceCode') {
return {
...sourceCode,
getAllComments: () => options.allowWarningComments ? [] : unusedComments,
};
}
return Reflect.get(target, property, receiver);
},
});
const rules = baseRule.create(fakeContext);
// eslint-disable-next-line complexity
function processComment(comment) {
if (ignoreRegexes.some(ignore => ignore.test(comment.value))) {
return;
}
const parsed = parseTodoWithArguments(comment.value, options);
if (!parsed) {
return true;
}
// Count if there are valid properties.
// Otherwise, it's a useless TODO and falls back to `no-warning-comments`.
let uses = 0;
const {
packageVersions = [],
dates = [],
dependencies = [],
engines = [],
unknowns = [],
} = parsed;
if (dates.length > 1) {
uses++;
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_AVOID_MULTIPLE_DATES,
data: {
expirationDates: dates.join(', '),
message: parseTodoMessage(comment.value),
},
});
} else if (dates.length === 1) {
uses++;
const [expirationDate] = dates;
const shouldIgnore = options.ignoreDates || (options.ignoreDatesOnPullRequests && ci.isPR);
if (!shouldIgnore && reachedDate(expirationDate, options.date)) {
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_EXPIRED_TODO,
data: {
expirationDate,
message: parseTodoMessage(comment.value),
},
});
}
}
if (packageVersions.length > 1) {
uses++;
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS,
data: {
versions: packageVersions
.map(({condition, version}) => `${condition}${version}`)
.join(', '),
message: parseTodoMessage(comment.value),
},
});
} else if (packageVersions.length === 1) {
uses++;
const [{condition, version}] = packageVersions;
const packageVersion = tryToCoerceVersion(packageJson.version);
if (packageVersion && satisfiesRange(packageVersion, condition, version)) {
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_REACHED_PACKAGE_VERSION,
data: {
comparison: `${condition}${version}`,
message: parseTodoMessage(comment.value),
},
});
}
}
// Inclusion: 'in', 'out'
// Comparison: '>', '>='
for (const dependency of dependencies) {
uses++;
const targetPackageRawVersion = packageDependencies[dependency.name];
const hasTargetPackage = Boolean(targetPackageRawVersion);
const isInclusion = ['in', 'out'].includes(dependency.condition);
if (isInclusion) {
const [trigger, messageId]
= dependency.condition === 'in'
? [hasTargetPackage, MESSAGE_ID_HAVE_PACKAGE]
: [!hasTargetPackage, MESSAGE_ID_DONT_HAVE_PACKAGE];
if (trigger) {
context.report({
loc: sourceCode.getLoc(comment),
messageId,
data: {
package: dependency.name,
message: parseTodoMessage(comment.value),
},
});
}
continue;
}
const targetPackageVersion = tryToCoerceVersion(targetPackageRawVersion);
/* c8 ignore start */
if (!hasTargetPackage || !targetPackageVersion) {
// Can't compare `¯\_(ツ)_/¯`
continue;
}
/* c8 ignore end */
if (satisfiesRange(targetPackageVersion, dependency.condition, dependency.version)) {
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_VERSION_MATCHES,
data: {
comparison: `${dependency.name} ${dependency.condition} ${dependency.version}`,
message: parseTodoMessage(comment.value),
},
});
}
}
const packageEngines = packageJson.engines || {};
for (const engine of engines) {
uses++;
const targetPackageRawEngineVersion = packageEngines.node;
const hasTargetEngine = Boolean(targetPackageRawEngineVersion);
/* c8 ignore next 3 */
if (!hasTargetEngine) {
continue;
}
const targetPackageEngineVersion = tryToCoerceVersion(targetPackageRawEngineVersion);
if (targetPackageEngineVersion && satisfiesRange(targetPackageEngineVersion, engine.condition, engine.version)) {
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_ENGINE_MATCHES,
data: {
comparison: `node${engine.condition}${engine.version}`,
message: parseTodoMessage(comment.value),
},
});
}
}
for (const unknown of unknowns) {
// In this case, check if there's just an '@' missing before a '>' or '>='.
const hasAt = unknown.includes('@');
const comparisonIndex = unknown.indexOf('>');
if (!hasAt && comparisonIndex !== -1) {
const testString = `${unknown.slice(
0,
comparisonIndex,
)}@${unknown.slice(comparisonIndex)}`;
if (parseArgument(testString).type !== 'unknowns') {
uses++;
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_MISSING_AT_SYMBOL,
data: {
original: unknown,
fix: testString,
message: parseTodoMessage(comment.value),
},
});
continue;
}
}
const withoutWhitespace = unknown.replaceAll(' ', '');
if (parseArgument(withoutWhitespace).type !== 'unknowns') {
uses++;
context.report({
loc: sourceCode.getLoc(comment),
messageId: MESSAGE_ID_REMOVE_WHITESPACE,
data: {
original: unknown,
fix: withoutWhitespace,
message: parseTodoMessage(comment.value),
},
});
continue;
}
}
return uses === 0;
}
context.on('Program', () => {
rules.Program(); // eslint-disable-line new-cap
});
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
terms: {
type: 'array',
items: {
type: 'string',
},
description: 'Comment terms to check.',
},
ignore: {
type: 'array',
uniqueItems: true,
description: 'Patterns to ignore.',
},
ignoreDates: {
type: 'boolean',
description: 'Whether to ignore expiration dates.',
},
ignoreDatesOnPullRequests: {
type: 'boolean',
description: 'Whether to ignore expiration dates on pull requests.',
},
allowWarningComments: {
type: 'boolean',
description: 'Whether to allow warning comments.',
},
date: {
type: 'string',
format: 'date',
description: 'The reference date.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Add expiration conditions to TODO comments.',
recommended: 'unopinionated',
},
schema,
defaultOptions: [{...DEFAULT_OPTIONS}],
messages,
},
};
export default config;
@@ -0,0 +1,228 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {
isParenthesized,
checkVueTemplate,
isLogicalExpression,
isBooleanExpression,
isControlFlowTest,
getBooleanAncestor,
} from './utils/index.js';
import {fixSpaceAroundKeyword} from './fix/index.js';
import {isLiteral, isMemberExpression} from './ast/index.js';
const TYPE_NON_ZERO = 'non-zero';
const TYPE_ZERO = 'zero';
const MESSAGE_ID_SUGGESTION = 'suggestion';
const messages = {
[TYPE_NON_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is not zero.',
[TYPE_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is zero.',
[MESSAGE_ID_SUGGESTION]: 'Replace `.{{property}}` with `.{{property}} {{code}}`.',
};
const isCompareRight = (node, operator, value) =>
node.type === 'BinaryExpression'
&& node.operator === operator
&& isLiteral(node.right, value);
const isCompareLeft = (node, operator, value) =>
node.type === 'BinaryExpression'
&& node.operator === operator
&& isLiteral(node.left, value);
const nonZeroStyles = new Map([
[
'greater-than',
{
code: '> 0',
test: node => isCompareRight(node, '>', 0),
},
],
[
'not-equal',
{
code: '!== 0',
test: node => isCompareRight(node, '!==', 0),
},
],
]);
const zeroStyle = {
code: '=== 0',
test: node => isCompareRight(node, '===', 0),
};
function getLengthCheckNode(node) {
node = node.parent;
// Zero length check
if (
// `foo.length === 0`
isCompareRight(node, '===', 0)
// `foo.length == 0`
|| isCompareRight(node, '==', 0)
// `foo.length < 1`
|| isCompareRight(node, '<', 1)
// `0 === foo.length`
|| isCompareLeft(node, '===', 0)
// `0 == foo.length`
|| isCompareLeft(node, '==', 0)
// `1 > foo.length`
|| isCompareLeft(node, '>', 1)
) {
return {isZeroLengthCheck: true, node};
}
// Non-Zero length check
if (
// `foo.length !== 0`
isCompareRight(node, '!==', 0)
// `foo.length != 0`
|| isCompareRight(node, '!=', 0)
// `foo.length > 0`
|| isCompareRight(node, '>', 0)
// `foo.length >= 1`
|| isCompareRight(node, '>=', 1)
// `0 !== foo.length`
|| isCompareLeft(node, '!==', 0)
// `0 !== foo.length`
|| isCompareLeft(node, '!=', 0)
// `0 < foo.length`
|| isCompareLeft(node, '<', 0)
// `1 <= foo.length`
|| isCompareLeft(node, '<=', 1)
) {
return {isZeroLengthCheck: false, node};
}
return {};
}
function create(context) {
const options = context.options[0];
const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
const {sourceCode} = context;
function getProblem({node, isZeroLengthCheck, lengthNode, autoFix, shouldSuggest = true}) {
const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
if (test(node)) {
return;
}
let fixed = `${sourceCode.getText(lengthNode)} ${code}`;
if (
!isParenthesized(node, context)
&& node.type === 'UnaryExpression'
&& (node.parent.type === 'UnaryExpression' || node.parent.type === 'AwaitExpression')
) {
fixed = `(${fixed})`;
}
const fix = function * (fixer) {
yield fixer.replaceText(node, fixed);
yield fixSpaceAroundKeyword(fixer, node, context);
};
const problem = {
node,
messageId: isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO,
data: {code, property: lengthNode.property.name},
};
if (autoFix) {
problem.fix = fix;
} else if (shouldSuggest) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
fix,
},
];
}
return problem;
}
context.on('MemberExpression', memberExpression => {
if (
!isMemberExpression(memberExpression, {
properties: ['length', 'size'],
optional: false,
})
|| memberExpression.object.type === 'ThisExpression'
) {
return;
}
const lengthNode = memberExpression;
const staticValue = getStaticValue(lengthNode, sourceCode.getScope(lengthNode));
if (staticValue && (!Number.isInteger(staticValue.value) || staticValue.value < 0)) {
// Ignore known, non-positive-integer length properties.
return;
}
let node;
let autoFix = true;
let {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(lengthNode);
if (lengthCheckNode) {
const {isNegative, node: ancestor} = getBooleanAncestor(lengthCheckNode);
node = ancestor;
if (isNegative) {
isZeroLengthCheck = !isZeroLengthCheck;
}
} else {
const {isNegative, node: ancestor} = getBooleanAncestor(lengthNode);
if (isBooleanExpression(ancestor) || isControlFlowTest(ancestor)) {
isZeroLengthCheck = isNegative;
node = ancestor;
} else if (isLogicalExpression(lengthNode.parent) && lengthNode.parent.operator === '&&') {
isZeroLengthCheck = isNegative;
node = lengthNode;
autoFix = false;
}
}
if (node) {
const isUnsafeNegationInBinaryExpression = node.type === 'UnaryExpression'
&& node.operator === '!'
&& node.parent.type === 'BinaryExpression'
&& node.parent.left === node;
return getProblem({
node,
isZeroLengthCheck,
lengthNode,
autoFix: autoFix && !isUnsafeNegationInBinaryExpression,
shouldSuggest: !isUnsafeNegationInBinaryExpression,
});
}
});
}
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
'non-zero': {
enum: [...nonZeroStyles.keys()],
default: 'greater-than',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create: checkVueTemplate(create),
meta: {
type: 'problem',
docs: {
description: 'Enforce explicitly comparing the `length` or `size` property of a value.',
recommended: true,
},
fixable: 'code',
schema,
defaultOptions: [{'non-zero': 'greater-than'}],
messages,
hasSuggestions: true,
},
};
export default config;
+284
View File
@@ -0,0 +1,284 @@
import path from 'node:path';
import {isRegExp} from 'node:util/types';
import {
camelCase,
kebabCase,
snakeCase,
pascalCase,
} from 'change-case';
import cartesianProductSamples from './utils/cartesian-product-samples.js';
const MESSAGE_ID = 'filename-case';
const MESSAGE_ID_EXTENSION = 'filename-extension';
const messages = {
[MESSAGE_ID]: 'Filename is not in {{chosenCases}}. Rename it to {{renamedFilenames}}.',
[MESSAGE_ID_EXTENSION]: 'File extension `{{extension}}` is not in lowercase. Rename it to `{{filename}}`.',
};
const isIgnoredChar = char => !/^[a-z\d-_]$/i.test(char);
const ignoredByDefault = new Set(['index.js', 'index.mjs', 'index.cjs', 'index.ts', 'index.tsx', 'index.vue']);
const isLowerCase = string => string === string.toLowerCase();
const cases = {
camelCase: {
fn: camelCase,
name: 'camel case',
},
kebabCase: {
fn: kebabCase,
name: 'kebab case',
},
snakeCase: {
fn: snakeCase,
name: 'snake case',
},
pascalCase: {
fn: pascalCase,
name: 'pascal case',
},
};
/**
Get the cases specified by the option.
@param {object} options
@returns {string[]} The chosen cases.
*/
function getChosenCases(options) {
if (options.case) {
return [options.case];
}
if (options.cases) {
const cases = Object.keys(options.cases)
.filter(cases => options.cases[cases]);
return cases.length > 0 ? cases : ['kebabCase'];
}
return ['kebabCase'];
}
function validateFilename(words, caseFunctions) {
return words
.filter(({ignored}) => !ignored)
.every(({word}) => caseFunctions.some(caseFunction => caseFunction(word) === word));
}
function fixFilename(words, caseFunctions, {leading, trailing}) {
const replacements = words
.map(({word, ignored}) => ignored ? [word] : caseFunctions.map(caseFunction => caseFunction(word)));
const {
samples: combinations,
} = cartesianProductSamples(replacements);
return [...new Set(combinations.map(parts => `${leading}${parts.join('')}${trailing}`))];
}
function getFilenameParts(filenameWithExtension, {multipleFileExtensions}) {
const extension = path.extname(filenameWithExtension);
const filename = path.basename(filenameWithExtension, extension);
const basename = filename + extension;
const parts = {
basename,
filename,
middle: '',
extension,
};
if (multipleFileExtensions) {
const [firstPart] = filename.split('.');
Object.assign(parts, {
filename: firstPart,
middle: filename.slice(firstPart.length),
});
}
return parts;
}
const leadingUnderscoresRegex = /^(?<leading>_+)(?<tailing>.*)$/;
function splitFilename(filename) {
const result = leadingUnderscoresRegex.exec(filename) || {groups: {}};
const {leading = '', tailing = filename} = result.groups;
const words = [];
let lastWord;
for (const char of tailing) {
const isIgnored = isIgnoredChar(char);
if (lastWord?.ignored === isIgnored) {
lastWord.word += char;
} else {
lastWord = {
word: char,
ignored: isIgnored,
};
words.push(lastWord);
}
}
return {
leading,
words,
};
}
/**
Turns `[a, b, c]` into `a, b, or c`.
@param {string[]} words
@returns {string}
*/
const englishishJoinWords = words => new Intl.ListFormat('en-US', {type: 'disjunction'}).format(words);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const options = context.options[0] || {};
const chosenCases = getChosenCases(options);
const ignore = (options.ignore || []).map(item => {
if (isRegExp(item)) {
return item;
}
return new RegExp(item, 'u');
});
const multipleFileExtensions = options.multipleFileExtensions !== false;
const chosenCasesFunctions = chosenCases.map(case_ => cases[case_].fn);
const filenameWithExtension = context.physicalFilename;
if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') {
return;
}
context.on('Program', () => {
const {
basename,
filename,
middle,
extension,
} = getFilenameParts(filenameWithExtension, {multipleFileExtensions});
if (ignoredByDefault.has(basename) || ignore.some(regexp => regexp.test(basename))) {
return;
}
const {leading, words} = splitFilename(filename);
const isValid = validateFilename(words, chosenCasesFunctions);
if (isValid) {
if (!isLowerCase(extension)) {
return {
loc: {column: 0, line: 1},
messageId: MESSAGE_ID_EXTENSION,
data: {filename: filename + middle + extension.toLowerCase(), extension},
};
}
return;
}
const renamedFilenames = fixFilename(words, chosenCasesFunctions, {
leading,
trailing: middle + extension.toLowerCase(),
});
return {
// Report on first character like `unicode-bom` rule
// https://github.com/eslint/eslint/blob/8a77b661bc921c3408bae01b3aa41579edfc6e58/lib/rules/unicode-bom.js#L46
loc: {column: 0, line: 1},
messageId: MESSAGE_ID,
data: {
chosenCases: englishishJoinWords(chosenCases.map(x => cases[x].name)),
renamedFilenames: englishishJoinWords(renamedFilenames.map(x => `\`${x}\``)),
},
};
});
};
const schema = [
{
oneOf: [
{
properties: {
case: {
enum: [
'camelCase',
'snakeCase',
'kebabCase',
'pascalCase',
],
description: 'The filename case style.',
},
ignore: {
type: 'array',
uniqueItems: true,
description: 'Patterns to ignore.',
},
multipleFileExtensions: {
type: 'boolean',
description: 'Whether to treat additional, dot-separated parts of a filename as file extensions.',
},
},
additionalProperties: false,
},
{
properties: {
cases: {
properties: {
camelCase: {
type: 'boolean',
description: 'Whether to allow camelCase filenames.',
},
snakeCase: {
type: 'boolean',
description: 'Whether to allow snake_case filenames.',
},
kebabCase: {
type: 'boolean',
description: 'Whether to allow kebab-case filenames.',
},
pascalCase: {
type: 'boolean',
description: 'Whether to allow PascalCase filenames.',
},
},
additionalProperties: false,
description: 'The allowed filename case styles.',
},
ignore: {
type: 'array',
uniqueItems: true,
description: 'Patterns to ignore.',
},
multipleFileExtensions: {
type: 'boolean',
description: 'Whether to treat additional, dot-separated parts of a filename as file extensions.',
},
},
additionalProperties: false,
},
],
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce a case style for filenames.',
recommended: true,
},
schema,
// eslint-disable-next-line eslint-plugin/require-meta-default-options
defaultOptions: [],
messages,
},
};
export default config;
@@ -0,0 +1,29 @@
import {isSemicolonToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.ReturnStatement | ESTree.ThrowStatement} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function * addParenthesizesToReturnOrThrowExpression(fixer, node, context) {
if (node.type !== 'ReturnStatement' && node.type !== 'ThrowStatement') {
return;
}
const {sourceCode} = context;
const returnOrThrowToken = sourceCode.getFirstToken(node);
yield fixer.insertTextAfter(returnOrThrowToken, ' (');
const lastToken = sourceCode.getLastToken(node);
if (!isSemicolonToken(lastToken)) {
yield fixer.insertTextAfter(node, ')');
return;
}
yield fixer.insertTextBefore(lastToken, ')');
}
@@ -0,0 +1,30 @@
import {isCommaToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.CallExpression} node
@param {string} text
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function appendArgument(fixer, node, text, context) {
// This function should also work for `NewExpression`
// But parentheses of `NewExpression` could be omitted, add this check to prevent accidental use on it
/* c8 ignore next 3 */
if (node.type !== 'CallExpression') {
throw new Error(`Unexpected node "${node.type}".`);
}
const {sourceCode} = context;
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2);
if (node.arguments.length > 0) {
text = isCommaToken(penultimateToken) ? ` ${text},` : `, ${text}`;
}
return fixer.insertTextBefore(lastToken, text);
}
@@ -0,0 +1,11 @@
/**
Extend fix range to prevent changes from other rules.
https://github.com/eslint/eslint/pull/13748/files#diff-c692f3fde09eda7c89f1802c908511a3fb59f5d207fe95eb009cb52e46a99e84R348
@param {ruleFixer} fixer - The fixer to fix.
@param {int[]} range - The extended range node.
*/
export default function * extendFixRange(fixer, range) {
yield fixer.insertTextBeforeRange(range, '');
yield fixer.insertTextAfterRange(range, '');
}
@@ -0,0 +1,43 @@
import {getParenthesizedRange} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
const isProblematicToken = ({type, value}) => (
(type === 'Keyword' && /^[a-z]*$/.test(value))
// ForOfStatement
|| (type === 'Identifier' && value === 'of')
// AwaitExpression
|| (type === 'Identifier' && value === 'await')
);
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
*/
export default function * fixSpaceAroundKeyword(fixer, node, context) {
const {sourceCode} = context;
const range = getParenthesizedRange(node, context);
const tokenBefore = sourceCode.getTokenBefore({range}, {includeComments: true});
if (
tokenBefore
&& range[0] === sourceCode.getRange(tokenBefore)[1]
&& isProblematicToken(tokenBefore)
) {
yield fixer.insertTextAfter(tokenBefore, ' ');
}
const tokenAfter = sourceCode.getTokenAfter({range}, {includeComments: true});
if (
tokenAfter
&& range[1] === sourceCode.getRange(tokenAfter)[0]
&& isProblematicToken(tokenAfter)
) {
yield fixer.insertTextBefore(tokenAfter, ' ');
}
}
+23
View File
@@ -0,0 +1,23 @@
export {default as extendFixRange} from './extend-fix-range.js';
export {default as removeParentheses} from './remove-parentheses.js';
export {default as appendArgument} from './append-argument.js';
export {default as removeArgument} from './remove-argument.js';
export {default as replaceArgument} from './replace-argument.js';
export {default as switchNewExpressionToCallExpression} from './switch-new-expression-to-call-expression.js';
export {default as switchCallExpressionToNewExpression} from './switch-call-expression-to-new-expression.js';
export {
replaceMemberExpressionProperty,
removeMemberExpressionProperty,
} from './replace-member-expression-property.js';
export {default as removeMethodCall} from './remove-method-call.js';
export {default as removeExpressionStatement} from './remove-expression-statement.js';
export {default as removeSpacesAfter} from './remove-spaces-after.js';
export {default as removeSpecifier} from './remove-specifier.js';
export {default as removeObjectProperty} from './remove-object-property.js';
export {default as renameVariable} from './rename-variable.js';
export {default as replaceTemplateElement} from './replace-template-element.js';
export {default as replaceReferenceIdentifier} from './replace-reference-identifier.js';
export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
export {default as fixSpaceAroundKeyword} from './fix-space-around-keywords.js';
export {default as replaceStringRaw} from './replace-string-raw.js';
export {default as addParenthesizesToReturnOrThrowExpression} from './add-parenthesizes-to-return-or-throw-expression.js';
@@ -0,0 +1,40 @@
import {isCommaToken} from '@eslint-community/eslint-utils';
import {getParentheses} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.NewExpression | ESTree.CallExpression} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function removeArgument(fixer, node, context) {
const callOrNewExpression = node.parent;
const index = callOrNewExpression.arguments.indexOf(node);
const parentheses = getParentheses(node, context);
const firstToken = parentheses[0] || node;
const lastToken = parentheses.at(-1) || node;
const {sourceCode} = context;
let [start] = sourceCode.getRange(firstToken);
let [, end] = sourceCode.getRange(lastToken);
if (index !== 0) {
const commaToken = sourceCode.getTokenBefore(firstToken);
[start] = sourceCode.getRange(commaToken);
}
// If the removed argument is the only argument, the trailing comma must be removed too
if (callOrNewExpression.arguments.length === 1) {
const tokenAfter = sourceCode.getTokenAfter(lastToken);
if (isCommaToken(tokenAfter)) {
[, end] = sourceCode.getRange(tokenAfter);
}
}
return fixer.removeRange([start, end]);
}
@@ -0,0 +1,35 @@
import {isSemicolonToken} from '@eslint-community/eslint-utils';
const isWhitespaceOnly = text => /^\s*$/.test(text);
function removeExpressionStatement(expressionStatement, context, fixer, preserveSemiColon = false) {
const {sourceCode} = context;
const {lines} = sourceCode;
let endToken = expressionStatement;
if (preserveSemiColon) {
const [penultimateToken, lastToken] = sourceCode.getLastTokens(expressionStatement, 2);
if (isSemicolonToken(lastToken)) {
endToken = penultimateToken;
}
}
const startLocation = sourceCode.getLoc(expressionStatement).start;
const endLocation = sourceCode.getLoc(endToken).end;
const textBefore = lines[startLocation.line - 1].slice(0, startLocation.column);
const textAfter = lines[endLocation.line - 1].slice(endLocation.column);
let [start] = sourceCode.getRange(expressionStatement);
let [, end] = sourceCode.getRange(endToken);
if (isWhitespaceOnly(textBefore) && isWhitespaceOnly(textAfter)) {
start = Math.max(0, start - textBefore.length - 1);
end += textAfter.length;
}
return fixer.removeRange([start, end]);
}
export default removeExpressionStatement;
@@ -0,0 +1,28 @@
import {getParenthesizedRange} from '../utils/index.js';
import {removeMemberExpressionProperty} from './replace-member-expression-property.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.CallExpression} callExpression
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function * removeMethodCall(fixer, callExpression, context) {
const memberExpression = callExpression.callee;
// `(( (( foo )).bar ))()`
// ^^^^
yield removeMemberExpressionProperty(fixer, memberExpression, context);
// `(( (( foo )).bar ))()`
// ^^
const [, start] = getParenthesizedRange(memberExpression, context);
const [, end] = context.sourceCode.getRange(callExpression);
yield fixer.removeRange([start, end]);
}
@@ -0,0 +1,21 @@
import {isCommaToken} from '@eslint-community/eslint-utils';
export default function * removeObjectProperty(fixer, property, context) {
const {sourceCode} = context;
for (const token of sourceCode.getTokens(property)) {
yield fixer.remove(token);
}
const tokenAfter = sourceCode.getTokenAfter(property);
if (isCommaToken(tokenAfter)) {
yield fixer.remove(tokenAfter);
} else {
// If the property is the last one and there is no trailing comma
// remove the previous comma
const {properties} = property.parent;
if (properties.length > 1 && properties.at(-1) === property) {
const commaTokenBefore = sourceCode.getTokenBefore(property);
yield fixer.remove(commaTokenBefore);
}
}
}
@@ -0,0 +1,19 @@
import {getParentheses} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.Node} node
@param {ESLint.Rule.RuleFixer} fixer
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function * removeParentheses(node, fixer, context) {
const parentheses = getParentheses(node, context);
for (const token of parentheses) {
yield fixer.remove(token);
}
}
@@ -0,0 +1,22 @@
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.Node | ESTree.Token | number} indexOrNodeOrToken
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {ESLint.Rule.RuleFixer} fixer
@returns {ESLint.Rule.ReportFixer}
*/
export default function removeSpacesAfter(indexOrNodeOrToken, context, fixer) {
let index = indexOrNodeOrToken;
if (typeof indexOrNodeOrToken === 'object') {
index = context.sourceCode.getRange(indexOrNodeOrToken)[1];
}
const textAfter = context.sourceCode.text.slice(index);
const [leadingSpaces] = textAfter.match(/^\s*/);
return fixer.removeRange([index, index + leadingSpaces.length]);
}
@@ -0,0 +1,59 @@
import {isCommaToken, isOpeningBraceToken} from '@eslint-community/eslint-utils';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.ImportSpecifier | ESTree.ExportSpecifier | ESTree.ImportDefaultSpecifier | ESTree.ImportNamespaceSpecifier} specifier
@param {ESLint.Rule.RuleFixer} fixer
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {boolean} [keepDeclaration = false]
@returns {ESLint.Rule.ReportFixer}
*/
export default function * removeSpecifier(specifier, fixer, context, keepDeclaration = false) {
const declaration = specifier.parent;
const {specifiers} = declaration;
if (specifiers.length === 1 && !keepDeclaration) {
yield fixer.remove(declaration);
return;
}
const {sourceCode} = context;
switch (specifier.type) {
case 'ImportSpecifier': {
const isTheOnlyNamedImport = specifiers.every(node => specifier === node || specifier.type !== node.type);
if (isTheOnlyNamedImport) {
const fromToken = sourceCode.getTokenAfter(specifier, token => token.type === 'Identifier' && token.value === 'from');
const hasDefaultImport = specifiers.some(node => node.type === 'ImportDefaultSpecifier');
const startToken = sourceCode.getTokenBefore(specifier, hasDefaultImport ? isCommaToken : isOpeningBraceToken);
const [start] = sourceCode.getRange(startToken);
const [end] = sourceCode.getRange(fromToken);
const tokenBefore = sourceCode.getTokenBefore(startToken);
const shouldInsertSpace = sourceCode.getRange(tokenBefore)[1] === start;
yield fixer.replaceTextRange([start, end], shouldInsertSpace ? ' ' : '');
return;
}
// Fallthrough
}
case 'ExportSpecifier':
case 'ImportNamespaceSpecifier':
case 'ImportDefaultSpecifier': {
yield fixer.remove(specifier);
const tokenAfter = sourceCode.getTokenAfter(specifier);
if (isCommaToken(tokenAfter)) {
yield fixer.remove(tokenAfter);
}
break;
}
// No default
}
}
@@ -0,0 +1,8 @@
import getVariableIdentifiers from '../utils/get-variable-identifiers.js';
import replaceReferenceIdentifier from './replace-reference-identifier.js';
const renameVariable = (variable, name, context, fixer) =>
getVariableIdentifiers(variable)
.map(identifier => replaceReferenceIdentifier(identifier, name, context, fixer));
export default renameVariable;
@@ -0,0 +1,17 @@
import {getParenthesizedRange} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESLint.Rule.RuleFixer} fixer
@param {ESTree.CallExpression | ESTree.NewExpression} node
@param {string} text
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export default function replaceArgument(fixer, node, text, context) {
return fixer.replaceTextRange(getParenthesizedRange(node, context), text);
}
@@ -0,0 +1,25 @@
import {getParenthesizedRange} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {string} text
@returns {ESLint.Rule.ReportFixer}
*/
export function replaceMemberExpressionProperty(fixer, memberExpression, context, text) {
const [, start] = getParenthesizedRange(memberExpression.object, context);
const [, end] = context.sourceCode.getRange(memberExpression);
return fixer.replaceTextRange([start, end], text);
}
/**
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export const removeMemberExpressionProperty = (fixer, memberExpression, context) => replaceMemberExpressionProperty(fixer, memberExpression, context, '');
@@ -0,0 +1,31 @@
import {getParentheses} from '../utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.Node | ESTree.Token} nodeOrToken
@param {string} replacement
@param {ESLint.Rule.RuleFixer} fixer
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {ESLint.SourceCode} [tokenStore]
@returns {ESLint.Rule.ReportFixer}
*/
export default function * replaceNodeOrTokenAndSpacesBefore(nodeOrToken, replacement, fixer, context, tokenStore) {
const tokens = getParentheses(nodeOrToken, tokenStore ? {sourceCode: tokenStore} : context);
for (const token of tokens) {
yield replaceNodeOrTokenAndSpacesBefore(token, '', fixer, context, tokenStore);
}
const {sourceCode} = context;
let [start, end] = sourceCode.getRange(nodeOrToken);
const textBefore = sourceCode.text.slice(0, start);
const [trailingSpaces] = textBefore.match(/\s*$/);
const [lineBreak] = trailingSpaces.match(/(?:\r?\n|\r){0,1}/);
start -= trailingSpaces.length;
yield fixer.replaceTextRange([start, end], `${lineBreak}${replacement}`);
}
@@ -0,0 +1,32 @@
import isShorthandPropertyValue from '../utils/is-shorthand-property-value.js';
import isShorthandPropertyAssignmentPatternLeft from '../utils/is-shorthand-property-assignment-pattern-left.js';
import isShorthandImportLocal from '../utils/is-shorthand-import-local.js';
import isShorthandExportLocal from '../utils/is-shorthand-export-local.js';
export default function replaceReferenceIdentifier(identifier, replacement, context, fixer) {
if (
isShorthandPropertyValue(identifier)
|| isShorthandPropertyAssignmentPatternLeft(identifier)
) {
return fixer.replaceText(identifier, `${identifier.name}: ${replacement}`);
}
if (isShorthandImportLocal(identifier, context)) {
return fixer.replaceText(identifier, `${identifier.name} as ${replacement}`);
}
if (isShorthandExportLocal(identifier, context)) {
return fixer.replaceText(identifier, `${replacement} as ${identifier.name}`);
}
// `typeAnnotation`
if (identifier.typeAnnotation) {
const {sourceCode} = context;
return fixer.replaceTextRange(
[sourceCode.getRange(identifier)[0], sourceCode.getRange(identifier.typeAnnotation)[0]],
`${replacement}${identifier.optional ? '?' : ''}`,
);
}
return fixer.replaceText(identifier, replacement);
}
@@ -0,0 +1,12 @@
// Replace `StringLiteral` or `TemplateLiteral` node with raw text
const replaceStringRaw = (node, raw, context, fixer) =>
fixer.replaceTextRange(
// Ignore quotes and backticks
[
context.sourceCode.getRange(node)[0] + 1,
context.sourceCode.getRange(node)[1] - 1,
],
raw,
);
export default replaceStringRaw;
@@ -0,0 +1,10 @@
const replaceTemplateElement = (node, replacement, context, fixer) => {
const {tail} = node;
const [start, end] = context.sourceCode.getRange(node);
return fixer.replaceTextRange(
[start + 1, end - (tail ? 1 : 2)],
replacement,
);
};
export default replaceTemplateElement;
@@ -0,0 +1,31 @@
import {
isParenthesized,
shouldAddParenthesesToNewExpressionCallee,
} from '../utils/index.js';
import fixSpaceAroundKeyword from './fix-space-around-keywords.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.CallExpression} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {ESLint.Rule.RuleFixer} fixer
@returns {ESLint.Rule.ReportFixer}
*/
export default function * switchCallExpressionToNewExpression(node, context, fixer) {
yield fixSpaceAroundKeyword(fixer, node, context);
yield fixer.insertTextBefore(node, 'new ');
const {callee} = node;
if (
!isParenthesized(callee, context)
&& shouldAddParenthesesToNewExpressionCallee(callee)
) {
yield fixer.insertTextBefore(callee, '(');
yield fixer.insertTextAfter(callee, ')');
}
}
@@ -0,0 +1,44 @@
import {
isNewExpressionWithParentheses,
isParenthesized,
isOnSameLine,
} from '../utils/index.js';
import addParenthesizesToReturnOrThrowExpression from './add-parenthesizes-to-return-or-throw-expression.js';
import removeSpaceAfter from './remove-spaces-after.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
/**
@param {ESTree.NewExpression} newExpression
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@param {ESLint.Rule.RuleFixer} fixer
@returns {ESLint.Rule.ReportFixer}
*/
export default function * switchNewExpressionToCallExpression(newExpression, context, fixer) {
const newToken = context.sourceCode.getFirstToken(newExpression);
yield fixer.remove(newToken);
yield removeSpaceAfter(newToken, context, fixer);
if (!isNewExpressionWithParentheses(newExpression, context)) {
yield fixer.insertTextAfter(newExpression, '()');
}
/*
Remove `new` from this code will makes the function return `undefined`
```js
() => {
return new // comment
Foo()
}
```
*/
if (!isOnSameLine(newToken, newExpression.callee, context) && !isParenthesized(newExpression, context)) {
// Ideally, we should use first parenthesis of the `callee`, and should check spaces after the `new` token
// But adding extra parentheses is harmless, no need to be too complicated
yield addParenthesizesToReturnOrThrowExpression(fixer, newExpression.parent, context);
}
}
+379
View File
@@ -0,0 +1,379 @@
import {getStringIfConstant} from '@eslint-community/eslint-utils';
import {isCallExpression} from './ast/index.js';
const MESSAGE_ID = 'importStyle';
const messages = {
[MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.',
};
const getActualImportDeclarationStyles = importDeclaration => {
const {specifiers} = importDeclaration;
if (specifiers.length === 0) {
return ['unassigned'];
}
const styles = new Set();
for (const specifier of specifiers) {
if (specifier.type === 'ImportDefaultSpecifier') {
styles.add('default');
continue;
}
if (specifier.type === 'ImportNamespaceSpecifier') {
styles.add('namespace');
continue;
}
if (specifier.type === 'ImportSpecifier') {
if (specifier.imported.type === 'Identifier' && specifier.imported.name === 'default') {
styles.add('default');
continue;
}
styles.add('named');
continue;
}
}
return [...styles];
};
const getActualExportDeclarationStyles = exportDeclaration => {
const {specifiers} = exportDeclaration;
if (specifiers.length === 0) {
return ['unassigned'];
}
const styles = new Set();
for (const specifier of specifiers) {
if (specifier.type === 'ExportSpecifier') {
if (specifier.exported.type === 'Identifier' && specifier.exported.name === 'default') {
styles.add('default');
continue;
}
styles.add('named');
continue;
}
}
return [...styles];
};
const getActualAssignmentTargetImportStyles = assignmentTarget => {
if (assignmentTarget.type === 'Identifier' || assignmentTarget.type === 'ArrayPattern') {
return ['namespace'];
}
if (assignmentTarget.type === 'ObjectPattern') {
if (assignmentTarget.properties.length === 0) {
return ['unassigned'];
}
const styles = new Set();
for (const property of assignmentTarget.properties) {
if (property.type === 'RestElement') {
styles.add('named');
continue;
}
if (property.key.type === 'Identifier') {
if (property.key.name === 'default') {
styles.add('default');
} else {
styles.add('named');
}
}
}
return [...styles];
}
// Next line is not test-coverable until unforceable changes to the language
// like an addition of new AST node types usable in `const __HERE__ = foo;`.
// An exotic custom parser or a bug in one could cover it too.
/* c8 ignore next */
return [];
};
const isAssignedDynamicImport = node =>
node.parent.type === 'AwaitExpression'
&& node.parent.argument === node
&& node.parent.parent.type === 'VariableDeclarator'
&& node.parent.parent.init === node.parent;
// Keep this alphabetically sorted for easier maintenance
const defaultStyles = {
chalk: {
default: true,
},
path: {
default: true,
},
'node:path': {
default: true,
},
util: {
named: true,
},
'node:util': {
named: true,
},
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
let [
{
styles = {},
extendDefaultStyles = true,
checkImport = true,
checkDynamicImport = true,
checkExportFrom = false,
checkRequire = true,
} = {},
] = context.options;
styles = extendDefaultStyles
? Object.fromEntries([...Object.keys(defaultStyles), ...Object.keys(styles)]
.map(name => [name, styles[name] === false ? {} : {...defaultStyles[name], ...styles[name]}]))
: styles;
styles = new Map(Object.entries(styles).map(([moduleName, styles]) =>
[moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))]));
const {sourceCode} = context;
// eslint-disable-next-line max-params
const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => {
if (!allowedImportStyles || allowedImportStyles.size === 0) {
return;
}
let effectiveAllowedImportStyles = allowedImportStyles;
// For `require`, `'default'` style allows both `x = require('x')` (`'namespace'` style) and
// `{default: x} = require('x')` (`'default'` style) since we don't know in advance
// whether `'x'` is a compiled ES6 module (with `default` key) or a CommonJS module and `require`
// does not provide any automatic interop for this, so the user may have to use either of these.
if (isRequire && allowedImportStyles.has('default') && !allowedImportStyles.has('namespace')) {
effectiveAllowedImportStyles = new Set(allowedImportStyles);
effectiveAllowedImportStyles.add('namespace');
}
if (actualImportStyles.every(style => effectiveAllowedImportStyles.has(style))) {
return;
}
const data = {
allowedStyles: new Intl.ListFormat('en-US', {type: 'disjunction'}).format([...allowedImportStyles.keys()]),
moduleName,
};
context.report({
node,
messageId: MESSAGE_ID,
data,
});
};
if (checkImport) {
context.on('ImportDeclaration', node => {
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = getActualImportDeclarationStyles(node);
report(node, moduleName, actualImportStyles, allowedImportStyles);
});
}
if (checkDynamicImport) {
context.on('ImportExpression', node => {
if (isAssignedDynamicImport(node)) {
return;
}
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = ['unassigned'];
report(node, moduleName, actualImportStyles, allowedImportStyles);
});
context.on('VariableDeclarator', node => {
if (!(
node.init?.type === 'AwaitExpression'
&& node.init.argument.type === 'ImportExpression'
)) {
return;
}
const assignmentTargetNode = node.id;
const moduleNameNode = node.init.argument.source;
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode));
if (!moduleName) {
return;
}
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
report(node, moduleName, actualImportStyles, allowedImportStyles);
});
}
if (checkExportFrom) {
context.on('ExportAllDeclaration', node => {
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = ['namespace'];
report(node, moduleName, actualImportStyles, allowedImportStyles);
});
context.on('ExportNamedDeclaration', node => {
if (!node.source) {
return;
}
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source));
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = getActualExportDeclarationStyles(node);
report(node, moduleName, actualImportStyles, allowedImportStyles);
});
}
if (checkRequire) {
context.on('CallExpression', node => {
if (!(
isCallExpression(node, {
name: 'require',
argumentsLength: 1,
optional: false,
})
&& (node.parent.type === 'ExpressionStatement' && node.parent.expression === node)
)) {
return;
}
const moduleName = getStringIfConstant(node.arguments[0], sourceCode.getScope(node.arguments[0]));
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = ['unassigned'];
report(node, moduleName, actualImportStyles, allowedImportStyles, true);
});
context.on('VariableDeclarator', node => {
if (!(
node.init?.type === 'CallExpression'
&& node.init.callee.type === 'Identifier'
&& node.init.callee.name === 'require'
)) {
return;
}
const assignmentTargetNode = node.id;
const moduleNameNode = node.init.arguments[0];
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode));
if (!moduleName) {
return;
}
const allowedImportStyles = styles.get(moduleName);
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
report(node, moduleName, actualImportStyles, allowedImportStyles, true);
});
}
};
const schema = {
type: 'array',
additionalItems: false,
items: [
{
type: 'object',
additionalProperties: false,
properties: {
checkImport: {
type: 'boolean',
description: 'Whether to check `import` statements.',
},
checkDynamicImport: {
type: 'boolean',
description: 'Whether to check dynamic `import()` expressions.',
},
checkExportFrom: {
type: 'boolean',
description: 'Whether to check `export … from` statements.',
},
checkRequire: {
type: 'boolean',
description: 'Whether to check `require()` calls.',
},
extendDefaultStyles: {
type: 'boolean',
description: 'Whether to extend the default styles.',
},
styles: {
$ref: '#/definitions/moduleStyles',
description: 'Module import styles.',
},
},
},
],
definitions: {
moduleStyles: {
type: 'object',
additionalProperties: {
$ref: '#/definitions/styles',
},
},
styles: {
anyOf: [
{
enum: [
false,
],
},
{
$ref: '#/definitions/booleanObject',
},
],
},
booleanObject: {
type: 'object',
additionalProperties: {
type: 'boolean',
},
},
},
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Enforce specific import styles per module.',
recommended: 'unopinionated',
},
schema,
defaultOptions: [{}],
messages,
},
};
export default config;
+148
View File
@@ -0,0 +1,148 @@
// Generated file, DO NOT edit
export {default as 'better-regex'} from './better-regex.js';
export {default as 'catch-error-name'} from './catch-error-name.js';
export {default as 'consistent-assert'} from './consistent-assert.js';
export {default as 'consistent-date-clone'} from './consistent-date-clone.js';
export {default as 'consistent-destructuring'} from './consistent-destructuring.js';
export {default as 'consistent-empty-array-spread'} from './consistent-empty-array-spread.js';
export {default as 'consistent-existence-index-check'} from './consistent-existence-index-check.js';
export {default as 'consistent-function-scoping'} from './consistent-function-scoping.js';
export {default as 'consistent-template-literal-escape'} from './consistent-template-literal-escape.js';
export {default as 'custom-error-definition'} from './custom-error-definition.js';
export {default as 'empty-brace-spaces'} from './empty-brace-spaces.js';
export {default as 'error-message'} from './error-message.js';
export {default as 'escape-case'} from './escape-case.js';
export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js';
export {default as 'explicit-length-check'} from './explicit-length-check.js';
export {default as 'filename-case'} from './filename-case.js';
export {default as 'import-style'} from './import-style.js';
export {default as 'isolated-functions'} from './isolated-functions.js';
export {default as 'new-for-builtins'} from './new-for-builtins.js';
export {default as 'no-abusive-eslint-disable'} from './no-abusive-eslint-disable.js';
export {default as 'no-accessor-recursion'} from './no-accessor-recursion.js';
export {default as 'no-anonymous-default-export'} from './no-anonymous-default-export.js';
export {default as 'no-array-callback-reference'} from './no-array-callback-reference.js';
export {default as 'no-array-for-each'} from './no-array-for-each.js';
export {default as 'no-array-method-this-argument'} from './no-array-method-this-argument.js';
export {default as 'no-array-reduce'} from './no-array-reduce.js';
export {default as 'no-array-reverse'} from './no-array-reverse.js';
export {default as 'no-array-sort'} from './no-array-sort.js';
export {default as 'no-await-expression-member'} from './no-await-expression-member.js';
export {default as 'no-await-in-promise-methods'} from './no-await-in-promise-methods.js';
export {default as 'no-console-spaces'} from './no-console-spaces.js';
export {default as 'no-document-cookie'} from './no-document-cookie.js';
export {default as 'no-empty-file'} from './no-empty-file.js';
export {default as 'no-for-loop'} from './no-for-loop.js';
export {default as 'no-hex-escape'} from './no-hex-escape.js';
export {default as 'no-immediate-mutation'} from './no-immediate-mutation.js';
export {default as 'no-instanceof-builtins'} from './no-instanceof-builtins.js';
export {default as 'no-invalid-fetch-options'} from './no-invalid-fetch-options.js';
export {default as 'no-invalid-remove-event-listener'} from './no-invalid-remove-event-listener.js';
export {default as 'no-keyword-prefix'} from './no-keyword-prefix.js';
export {default as 'no-lonely-if'} from './no-lonely-if.js';
export {default as 'no-magic-array-flat-depth'} from './no-magic-array-flat-depth.js';
export {default as 'no-named-default'} from './no-named-default.js';
export {default as 'no-negated-condition'} from './no-negated-condition.js';
export {default as 'no-negation-in-equality-check'} from './no-negation-in-equality-check.js';
export {default as 'no-nested-ternary'} from './no-nested-ternary.js';
export {default as 'no-new-array'} from './no-new-array.js';
export {default as 'no-new-buffer'} from './no-new-buffer.js';
export {default as 'no-null'} from './no-null.js';
export {default as 'no-object-as-default-parameter'} from './no-object-as-default-parameter.js';
export {default as 'no-process-exit'} from './no-process-exit.js';
export {default as 'no-single-promise-in-promise-methods'} from './no-single-promise-in-promise-methods.js';
export {default as 'no-static-only-class'} from './no-static-only-class.js';
export {default as 'no-thenable'} from './no-thenable.js';
export {default as 'no-this-assignment'} from './no-this-assignment.js';
export {default as 'no-typeof-undefined'} from './no-typeof-undefined.js';
export {default as 'no-unnecessary-array-flat-depth'} from './no-unnecessary-array-flat-depth.js';
export {default as 'no-unnecessary-array-splice-count'} from './no-unnecessary-array-splice-count.js';
export {default as 'no-unnecessary-await'} from './no-unnecessary-await.js';
export {default as 'no-unnecessary-polyfills'} from './no-unnecessary-polyfills.js';
export {default as 'no-unnecessary-slice-end'} from './no-unnecessary-slice-end.js';
export {default as 'no-unreadable-array-destructuring'} from './no-unreadable-array-destructuring.js';
export {default as 'no-unreadable-iife'} from './no-unreadable-iife.js';
export {default as 'no-unused-properties'} from './no-unused-properties.js';
export {default as 'no-useless-collection-argument'} from './no-useless-collection-argument.js';
export {default as 'no-useless-error-capture-stack-trace'} from './no-useless-error-capture-stack-trace.js';
export {default as 'no-useless-fallback-in-spread'} from './no-useless-fallback-in-spread.js';
export {default as 'no-useless-iterator-to-array'} from './no-useless-iterator-to-array.js';
export {default as 'no-useless-length-check'} from './no-useless-length-check.js';
export {default as 'no-useless-promise-resolve-reject'} from './no-useless-promise-resolve-reject.js';
export {default as 'no-useless-spread'} from './no-useless-spread.js';
export {default as 'no-useless-switch-case'} from './no-useless-switch-case.js';
export {default as 'no-useless-undefined'} from './no-useless-undefined.js';
export {default as 'no-zero-fractions'} from './no-zero-fractions.js';
export {default as 'number-literal-case'} from './number-literal-case.js';
export {default as 'numeric-separators-style'} from './numeric-separators-style.js';
export {default as 'prefer-add-event-listener'} from './prefer-add-event-listener.js';
export {default as 'prefer-array-find'} from './prefer-array-find.js';
export {default as 'prefer-array-flat-map'} from './prefer-array-flat-map.js';
export {default as 'prefer-array-flat'} from './prefer-array-flat.js';
export {default as 'prefer-array-index-of'} from './prefer-array-index-of.js';
export {default as 'prefer-array-some'} from './prefer-array-some.js';
export {default as 'prefer-at'} from './prefer-at.js';
export {default as 'prefer-bigint-literals'} from './prefer-bigint-literals.js';
export {default as 'prefer-blob-reading-methods'} from './prefer-blob-reading-methods.js';
export {default as 'prefer-class-fields'} from './prefer-class-fields.js';
export {default as 'prefer-classlist-toggle'} from './prefer-classlist-toggle.js';
export {default as 'prefer-code-point'} from './prefer-code-point.js';
export {default as 'prefer-date-now'} from './prefer-date-now.js';
export {default as 'prefer-default-parameters'} from './prefer-default-parameters.js';
export {default as 'prefer-dom-node-append'} from './prefer-dom-node-append.js';
export {default as 'prefer-dom-node-dataset'} from './prefer-dom-node-dataset.js';
export {default as 'prefer-dom-node-remove'} from './prefer-dom-node-remove.js';
export {default as 'prefer-dom-node-text-content'} from './prefer-dom-node-text-content.js';
export {default as 'prefer-event-target'} from './prefer-event-target.js';
export {default as 'prefer-export-from'} from './prefer-export-from.js';
export {default as 'prefer-global-this'} from './prefer-global-this.js';
export {default as 'prefer-import-meta-properties'} from './prefer-import-meta-properties.js';
export {default as 'prefer-includes'} from './prefer-includes.js';
export {default as 'prefer-json-parse-buffer'} from './prefer-json-parse-buffer.js';
export {default as 'prefer-keyboard-event-key'} from './prefer-keyboard-event-key.js';
export {default as 'prefer-logical-operator-over-ternary'} from './prefer-logical-operator-over-ternary.js';
export {default as 'prefer-math-min-max'} from './prefer-math-min-max.js';
export {default as 'prefer-math-trunc'} from './prefer-math-trunc.js';
export {default as 'prefer-modern-dom-apis'} from './prefer-modern-dom-apis.js';
export {default as 'prefer-modern-math-apis'} from './prefer-modern-math-apis.js';
export {default as 'prefer-module'} from './prefer-module.js';
export {default as 'prefer-native-coercion-functions'} from './prefer-native-coercion-functions.js';
export {default as 'prefer-negative-index'} from './prefer-negative-index.js';
export {default as 'prefer-node-protocol'} from './prefer-node-protocol.js';
export {default as 'prefer-number-properties'} from './prefer-number-properties.js';
export {default as 'prefer-object-from-entries'} from './prefer-object-from-entries.js';
export {default as 'prefer-optional-catch-binding'} from './prefer-optional-catch-binding.js';
export {default as 'prefer-prototype-methods'} from './prefer-prototype-methods.js';
export {default as 'prefer-query-selector'} from './prefer-query-selector.js';
export {default as 'prefer-reflect-apply'} from './prefer-reflect-apply.js';
export {default as 'prefer-regexp-test'} from './prefer-regexp-test.js';
export {default as 'prefer-response-static-json'} from './prefer-response-static-json.js';
export {default as 'prefer-set-has'} from './prefer-set-has.js';
export {default as 'prefer-set-size'} from './prefer-set-size.js';
export {default as 'prefer-simple-condition-first'} from './prefer-simple-condition-first.js';
export {default as 'prefer-single-call'} from './prefer-single-call.js';
export {default as 'prefer-spread'} from './prefer-spread.js';
export {default as 'prefer-string-raw'} from './prefer-string-raw.js';
export {default as 'prefer-string-replace-all'} from './prefer-string-replace-all.js';
export {default as 'prefer-string-slice'} from './prefer-string-slice.js';
export {default as 'prefer-string-starts-ends-with'} from './prefer-string-starts-ends-with.js';
export {default as 'prefer-string-trim-start-end'} from './prefer-string-trim-start-end.js';
export {default as 'prefer-structured-clone'} from './prefer-structured-clone.js';
export {default as 'prefer-switch'} from './prefer-switch.js';
export {default as 'prefer-ternary'} from './prefer-ternary.js';
export {default as 'prefer-top-level-await'} from './prefer-top-level-await.js';
export {default as 'prefer-type-error'} from './prefer-type-error.js';
export {default as 'prevent-abbreviations'} from './prevent-abbreviations.js';
export {default as 'relative-url-style'} from './relative-url-style.js';
export {default as 'require-array-join-separator'} from './require-array-join-separator.js';
export {default as 'require-module-attributes'} from './require-module-attributes.js';
export {default as 'require-module-specifiers'} from './require-module-specifiers.js';
export {default as 'require-number-to-fixed-digits-argument'} from './require-number-to-fixed-digits-argument.js';
export {default as 'require-post-message-target-origin'} from './require-post-message-target-origin.js';
export {default as 'string-content'} from './string-content.js';
export {default as 'switch-case-braces'} from './switch-case-braces.js';
export {default as 'switch-case-break-position'} from './switch-case-break-position.js';
export {default as 'template-indent'} from './template-indent.js';
export {default as 'text-encoding-identifier-case'} from './text-encoding-identifier-case.js';
export {default as 'throw-new-error'} from './throw-new-error.js';
+206
View File
@@ -0,0 +1,206 @@
import globals from 'globals';
import {functionTypes} from './ast/index.js';
const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable';
const messages = {
[MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE]: 'Variable {{name}} not defined in scope of isolated function. Function is isolated because: {{reason}}.',
};
/** @type {{functions: string[], selectors: string[], comments: string[], overrideGlobals?: import('eslint').Linter.Globals}} */
const defaultOptions = {
functions: ['makeSynchronous'],
selectors: [],
comments: ['@isolated'],
overrideGlobals: {},
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
/** @type {typeof defaultOptions} */
const options = {...context.options[0]};
options.comments = options.comments.map(comment => comment.toLowerCase());
const allowedGlobals = {
...(globals[`es${context.languageOptions.ecmaVersion}`] ?? globals.builtins),
...context.languageOptions.globals,
...options.overrideGlobals,
};
const checked = new WeakSet();
/** @param {import('estree').Node} node */
const checkForExternallyScopedVariables = (node, reason) => {
if (checked.has(node) || !functionTypes.includes(node.type)) {
return;
}
checked.add(node);
const nodeScope = sourceCode.getScope(node);
// `through`: "The array of references which could not be resolved in this scope" https://eslint.org/docs/latest/extend/scope-manager-interface#scope-interface
for (const reference of nodeScope.through) {
const {identifier} = reference;
if (identifier.parent.type === 'TSTypeReference' || identifier.parent.type === 'TSTypeQuery') {
continue;
}
if (identifier.name in allowedGlobals && allowedGlobals[identifier.name] !== 'off') {
if (reference.isReadOnly()) {
continue;
}
const globalsValue = allowedGlobals[identifier.name];
const isGlobalWritable = globalsValue === true || globalsValue === 'writable' || globalsValue === 'writeable';
if (isGlobalWritable) {
continue;
}
reason += ' (global variable is not writable)';
}
// Could consider checking for typeof operator here, like in no-undef?
context.report({
node: identifier,
messageId: MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE,
data: {name: identifier.name, reason},
});
}
};
const isComment = token => token?.type === 'Block' || token?.type === 'Line';
/**
Find a comment on this node or its parent, in cases where the node passed is part of a variable or export declaration.
@param {import('estree').Node} node
*/
const findComment = node => {
let previousToken = sourceCode.getTokenBefore(node, {includeComments: true});
let commentableNode = node;
while (
!isComment(previousToken)
&& (commentableNode.parent.type === 'VariableDeclarator'
|| commentableNode.parent.type === 'VariableDeclaration'
|| commentableNode.parent.type === 'ExportNamedDeclaration'
|| commentableNode.parent.type === 'ExportDefaultDeclaration')
) {
commentableNode = commentableNode.parent;
previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true});
}
if (isComment(previousToken)) {
return previousToken.value;
}
};
/**
Find the string "reason" that a function (node) should be considered isolated. For passing in to `context.report(...)` when out-of-scope variables are found. Returns undefined if the function should not be considered isolated.
@param {import('estree').Node & {parent?: import('estree').Node}} node
*/
const reasonForBeingIsolatedFunction = node => {
if (options.comments.length > 0) {
let previousComment = findComment(node);
if (previousComment) {
previousComment = previousComment
.replace(/(?:\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated`
.trim()
.toLowerCase();
const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `) || previousComment.startsWith(`${comment} -- `));
if (match) {
return `follows comment ${JSON.stringify(match)}`;
}
}
}
if (
options.functions.length > 0
&& node.parent.type === 'CallExpression'
&& node.parent.arguments.includes(node)
&& node.parent.callee.type === 'Identifier'
&& options.functions.includes(node.parent.callee.name)
) {
return `callee of function named ${JSON.stringify(node.parent.callee.name)}`;
}
};
context.onExit(
functionTypes,
node => {
const reason = reasonForBeingIsolatedFunction(node);
if (!reason) {
return;
}
return checkForExternallyScopedVariables(node, reason);
},
);
for (const selector of options.selectors) {
context.onExit(
selector,
node => {
const reason = `matches selector ${JSON.stringify(selector)}`;
return checkForExternallyScopedVariables(node, reason);
},
);
}
};
/** @type {import('json-schema').JSONSchema7[]} */
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
overrideGlobals: {
additionalProperties: {
anyOf: [{type: 'boolean'}, {type: 'string', enum: ['readonly', 'writable', 'writeable', 'off']}],
},
description: 'Override which global variables are allowed inside isolated scopes.',
},
functions: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
description: 'Function names that mark a scope as isolated.',
},
selectors: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
description: 'AST selectors that mark a scope as isolated.',
},
comments: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
description: 'Comment patterns that mark a scope as isolated.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
export default {
create,
meta: {
type: 'problem',
docs: {
description: 'Prevent usage of variables from outside the scope of isolated functions.',
recommended: true,
},
schema,
defaultOptions: [defaultOptions],
messages,
},
};
+112
View File
@@ -0,0 +1,112 @@
import {GlobalReferenceTracker} from './utils/global-reference-tracker.js';
import * as builtins from './utils/builtins.js';
import {
switchCallExpressionToNewExpression,
switchNewExpressionToCallExpression,
fixSpaceAroundKeyword,
} from './fix/index.js';
const MESSAGE_ID_ERROR_DATE = 'error-date';
const MESSAGE_ID_SUGGESTION_DATE = 'suggestion-date';
const messages = {
enforce: 'Use `new {{name}}()` instead of `{{name}}()`.',
disallow: 'Use `{{name}}()` instead of `new {{name}}()`.',
[MESSAGE_ID_ERROR_DATE]: 'Use `String(new Date())` instead of `Date()`.',
[MESSAGE_ID_SUGGESTION_DATE]: 'Switch to `String(new Date())`.',
};
function enforceNewExpression({node, path: [name]}, context) {
if (name === 'Object') {
const {parent} = node;
if (
parent.type === 'BinaryExpression'
&& (parent.operator === '===' || parent.operator === '!==')
&& (parent.left === node || parent.right === node)
) {
return;
}
}
// `Date()` returns a string representation of the current date and time, exactly as `new Date().toString()` does.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#return_value
if (name === 'Date') {
function * fix(fixer) {
yield fixer.replaceText(node, 'String(new Date())');
yield fixSpaceAroundKeyword(fixer, node, context);
}
const problem = {
node,
messageId: MESSAGE_ID_ERROR_DATE,
};
if (context.sourceCode.getCommentsInside(node).length === 0 && node.arguments.length === 0) {
problem.fix = fix;
} else {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_DATE,
fix,
},
];
}
return problem;
}
return {
node,
messageId: 'enforce',
data: {name},
fix: fixer => switchCallExpressionToNewExpression(node, context, fixer),
};
}
function enforceCallExpression({node, path: [name]}, context) {
const problem = {
node,
messageId: 'disallow',
data: {name},
};
if (name !== 'String' && name !== 'Boolean' && name !== 'Number') {
problem.fix = fixer => switchNewExpressionToCallExpression(node, context, fixer);
}
return problem;
}
const newExpressionTracker = new GlobalReferenceTracker({
objects: builtins.disallowNew,
type: GlobalReferenceTracker.CONSTRUCT,
handle: enforceCallExpression,
});
const callExpressionTracker = new GlobalReferenceTracker({
objects: builtins.enforceNew,
type: GlobalReferenceTracker.CALL,
handle: enforceNewExpression,
});
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
newExpressionTracker.listen({context});
callExpressionTracker.listen({context});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
@@ -0,0 +1,49 @@
import {
getEslintDisableDirectives,
} from './utils/index.js';
const MESSAGE_ID = 'no-abusive-eslint-disable';
const messages = {
[MESSAGE_ID]: 'Specify the rules you want to disable.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('Program', function * () {
for (const directive of getEslintDisableDirectives(context)) {
if (directive.value) {
continue;
}
const {start, end} = context.sourceCode.getLoc(directive.node);
yield {
// Can't set it at the given location as the warning
// will be ignored due to the disable comment
loc: {
start: {
...start,
column: -1,
},
end,
},
messageId: MESSAGE_ID,
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce specifying rules to disable in `eslint-disable` comments.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
@@ -0,0 +1,156 @@
const MESSAGE_ID_ERROR = 'no-accessor-recursion/error';
const messages = {
[MESSAGE_ID_ERROR]: 'Disallow recursive access to `this` within {{kind}}ters.',
};
/**
Get the closest non-arrow function scope.
@param {import('eslint').SourceCode} sourceCode
@param {import('estree').Node} node
@return {import('eslint').Scope.Scope | undefined}
*/
const getClosestFunctionScope = (sourceCode, node) => {
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
if (scope.type === 'class') {
return;
}
if (scope.type === 'function' && scope.block.type !== 'ArrowFunctionExpression') {
return scope;
}
}
};
/** @param {import('estree').Identifier | import('estree').PrivateIdentifier} node */
const isIdentifier = node => node.type === 'Identifier' || node.type === 'PrivateIdentifier';
/** @param {import('estree').ThisExpression} node */
const isDotNotationAccess = node =>
node.parent.type === 'MemberExpression'
&& node.parent.object === node
&& !node.parent.computed
&& isIdentifier(node.parent.property);
/**
Check if a property is a valid getter or setter.
@param {import('estree').Property | import('estree').MethodDefinition} property
*/
const isValidProperty = property =>
['Property', 'MethodDefinition'].includes(property?.type)
&& !property.computed
&& ['set', 'get'].includes(property.kind)
&& isIdentifier(property.key);
/**
Check if two property keys are the same.
@param {import('estree').Property['key']} keyLeft
@param {import('estree').Property['key']} keyRight
*/
const isSameKey = (keyLeft, keyRight) => ['type', 'name'].every(key => keyLeft[key] === keyRight[key]);
/**
Check if `this` is accessed recursively within a getter or setter.
@param {import('estree').ThisExpression} node
@param {import('estree').Property | import('estree').MethodDefinition} property
*/
const isMemberAccess = (node, property) =>
isDotNotationAccess(node)
&& isSameKey(node.parent.property, property.key);
/**
Check if `this` is accessed recursively within a destructuring assignment.
@param {import('estree').ThisExpression} node
@param {import('estree').Property | import('estree').MethodDefinition} property
*/
const isRecursiveDestructuringAccess = (node, property) =>
node.parent.type === 'VariableDeclarator'
&& node.parent.init === node
&& node.parent.id.type === 'ObjectPattern'
&& node.parent.id.properties.some(declaratorProperty =>
declaratorProperty.type === 'Property'
&& !declaratorProperty.computed
&& isSameKey(declaratorProperty.key, property.key));
const isPropertyRead = (thisExpression, property) =>
isMemberAccess(thisExpression, property)
|| isRecursiveDestructuringAccess(thisExpression, property);
const isPropertyWrite = (thisExpression, property) => {
if (!isMemberAccess(thisExpression, property)) {
return false;
}
const memberExpression = thisExpression.parent;
const {parent} = memberExpression;
// This part is similar to `isLeftHandSide`, try to DRY in future
return (
// `this.foo = …`
// `[this.foo = …] = …`
// `({property: this.foo = …] = …)`
(
(parent.type === 'AssignmentExpression' || parent.type === 'AssignmentPattern')
&& parent.left === memberExpression
)
// `++ this.foo`
|| (parent.type === 'UpdateExpression' && parent.argument === memberExpression)
// `[this.foo] = …`
|| (parent.type === 'ArrayPattern' && parent.elements.includes(memberExpression))
// `({property: this.foo} = …)`
|| (
parent.type === 'Property'
&& parent.value === memberExpression
&& parent.parent.type === 'ObjectPattern'
&& parent.parent.properties.includes(memberExpression.parent)
)
);
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
context.on('ThisExpression', /** @param {import('estree').ThisExpression} thisExpression */ thisExpression => {
const scope = getClosestFunctionScope(sourceCode, thisExpression);
if (!scope) {
return;
}
/** @type {import('estree').Property | import('estree').MethodDefinition} */
const property = scope.block.parent;
if (!isValidProperty(property)) {
return;
}
if (property.kind === 'get' && isPropertyRead(thisExpression, property)) {
return {node: thisExpression.parent, messageId: MESSAGE_ID_ERROR, data: {kind: property.kind}};
}
if (property.kind === 'set' && isPropertyWrite(thisExpression, property)) {
return {node: thisExpression.parent, messageId: MESSAGE_ID_ERROR, data: {kind: property.kind}};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow recursive access to `this` within getters and setters.',
recommended: 'unopinionated',
},
defaultOptions: [],
messages,
},
};
export default config;
@@ -0,0 +1,212 @@
import path from 'node:path';
import {getFunctionHeadLocation, getFunctionNameWithKind, isOpeningParenToken} from '@eslint-community/eslint-utils';
import helperValidatorIdentifier from '@babel/helper-validator-identifier';
import {camelCase} from 'change-case';
import {
getClassHeadLocation,
getParenthesizedRange,
getScopes,
getAvailableVariableName,
upperFirst,
} from './utils/index.js';
import {isMemberExpression} from './ast/index.js';
const {isIdentifierName} = helperValidatorIdentifier;
const MESSAGE_ID_ERROR = 'no-anonymous-default-export/error';
const MESSAGE_ID_SUGGESTION = 'no-anonymous-default-export/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'The {{description}} should be named.',
[MESSAGE_ID_SUGGESTION]: 'Name it as `{{name}}`.',
};
const isClassKeywordToken = token => token.type === 'Keyword' && token.value === 'class';
const isAnonymousClassOrFunction = node =>
(
(
node.type === 'FunctionDeclaration'
|| node.type === 'FunctionExpression'
|| node.type === 'ClassDeclaration'
|| node.type === 'ClassExpression'
)
&& !node.id
)
|| node.type === 'ArrowFunctionExpression';
function getSuggestionName(node, filename, sourceCode) {
if (filename === '<input>' || filename === '<text>') {
return;
}
let [name] = path.basename(filename).split('.');
name = camelCase(name);
if (!isIdentifierName(name)) {
return;
}
name = node.type === 'ClassDeclaration' || node.type === 'ClassExpression' ? upperFirst(name) : name;
name = getAvailableVariableName(name, getScopes(sourceCode.getScope(node)));
return name;
}
function addName(fixer, node, name, context) {
const {sourceCode} = context;
switch (node.type) {
case 'ClassDeclaration':
case 'ClassExpression': {
const lastDecorator = node.decorators?.at(-1);
const classToken = lastDecorator
? sourceCode.getTokenAfter(lastDecorator, isClassKeywordToken)
: sourceCode.getFirstToken(node, isClassKeywordToken);
return fixer.insertTextAfter(classToken, ` ${name}`);
}
case 'FunctionDeclaration':
case 'FunctionExpression': {
const openingParenthesisToken = sourceCode.getFirstToken(
node,
isOpeningParenToken,
);
const characterBefore = sourceCode.text.charAt(sourceCode.getRange(openingParenthesisToken)[0] - 1);
return fixer.insertTextBefore(
openingParenthesisToken,
`${characterBefore === ' ' ? '' : ' '}${name} `,
);
}
case 'ArrowFunctionExpression': {
const [exportDeclarationStart, exportDeclarationEnd]
= sourceCode.getRange(node.parent.type === 'ExportDefaultDeclaration'
? node.parent
: node.parent.parent);
const [arrowFunctionStart, arrowFunctionEnd] = getParenthesizedRange(node, context);
let textBefore = sourceCode.text.slice(exportDeclarationStart, arrowFunctionStart);
let textAfter = sourceCode.text.slice(arrowFunctionEnd, exportDeclarationEnd);
textBefore = `\n${textBefore}`;
if (!/\s$/.test(textBefore)) {
textBefore = `${textBefore} `;
}
if (!textAfter.endsWith(';')) {
textAfter = `${textAfter};`;
}
return [
fixer.replaceTextRange(
[exportDeclarationStart, arrowFunctionStart],
`const ${name} = `,
),
fixer.replaceTextRange(
[arrowFunctionEnd, exportDeclarationEnd],
';',
),
fixer.insertTextAfterRange(
[exportDeclarationEnd, exportDeclarationEnd],
`${textBefore}${name}${textAfter}`,
),
];
}
// No default
}
}
function getProblem(node, context) {
const {sourceCode, physicalFilename} = context;
const suggestionName = getSuggestionName(node, physicalFilename, sourceCode);
let loc;
let description;
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
loc = getClassHeadLocation(node, context);
description = 'class';
} else {
loc = getFunctionHeadLocation(node, sourceCode);
// [TODO: @fisker]: Ask `@eslint-community/eslint-utils` to expose `getFunctionKind`
const nameWithKind = getFunctionNameWithKind(node);
description = nameWithKind.replace(/ '.*?'$/, '');
}
const problem = {
node,
loc,
messageId: MESSAGE_ID_ERROR,
data: {
description,
},
};
if (!suggestionName) {
return problem;
}
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {
name: suggestionName,
},
fix: fixer => addName(fixer, node, suggestionName, context),
},
];
return problem;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('ExportDefaultDeclaration', node => {
if (!isAnonymousClassOrFunction(node.declaration)) {
return;
}
return getProblem(node.declaration, context);
});
context.on('AssignmentExpression', node => {
if (
!isAnonymousClassOrFunction(node.right)
|| !(
node.parent.type === 'ExpressionStatement'
&& node.parent.expression === node
)
|| !(
isMemberExpression(node.left, {
object: 'module',
property: 'exports',
computed: false,
optional: false,
})
|| (
node.left.type === 'Identifier'
&& node.left.name === 'exports'
)
)
) {
return;
}
return getProblem(node.right, context);
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow anonymous functions and classes as the default export.',
recommended: 'unopinionated',
},
hasSuggestions: true,
messages,
},
};
export default config;
@@ -0,0 +1,345 @@
import {findVariable} from '@eslint-community/eslint-utils';
import {isMethodCall} from './ast/index.js';
import {
isNodeMatches,
isNodeValueNotFunction,
isParenthesized,
getParenthesizedRange,
getParenthesizedText,
shouldAddParenthesesToCallExpressionCallee,
} from './utils/index.js';
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
const messages = {
[ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
[ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.',
};
const isAwaitExpressionArgument = node => node.parent.type === 'AwaitExpression' && node.parent.argument === node;
const iteratorMethods = new Map([
{
method: 'every',
ignore: [
'Boolean',
],
},
{
method: 'filter',
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
ignore: [
'Boolean',
],
},
{
method: 'find',
ignore: [
'Boolean',
],
},
{
method: 'findLast',
ignore: [
'Boolean',
],
},
{
method: 'findIndex',
ignore: [
'Boolean',
],
},
{
method: 'findLastIndex',
ignore: [
'Boolean',
],
},
{
method: 'flatMap',
},
{
method: 'forEach',
returnsUndefined: true,
},
{
method: 'map',
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
ignore: [
'String',
'Number',
'BigInt',
'Boolean',
'Symbol',
],
},
{
method: 'reduce',
parameters: [
'accumulator',
'element',
'index',
'array',
],
minParameters: 2,
},
{
method: 'reduceRight',
parameters: [
'accumulator',
'element',
'index',
'array',
],
minParameters: 2,
},
{
method: 'some',
ignore: [
'Boolean',
],
},
].map(({
method,
parameters = ['element', 'index', 'array'],
ignore = [],
minParameters = 1,
returnsUndefined = false,
shouldIgnoreCallExpression,
}) => [method, {
minParameters,
parameters,
returnsUndefined,
shouldIgnoreCallExpression(callExpression) {
if (
method !== 'reduce'
&& method !== 'reduceRight'
&& isAwaitExpressionArgument(callExpression)
) {
return true;
}
if (isNodeMatches(callExpression.callee.object, ignoredCallee)) {
return true;
}
if (
callExpression.callee.object.type === 'CallExpression'
&& isNodeMatches(callExpression.callee.object.callee, ignoredCallee)
) {
return true;
}
return shouldIgnoreCallExpression?.(callExpression) ?? false;
},
shouldIgnoreCallback(callback) {
if (callback.type === 'Identifier' && ignore.includes(callback.name)) {
return true;
}
return false;
},
}]));
const ignoredCallee = [
// http://bluebirdjs.com/docs/api/promise.map.html
'Promise',
'React.Children',
'Children',
'lodash',
'underscore',
'_',
'Async',
'async',
'this',
'$',
'jQuery',
];
function getProblem(context, node, method, options) {
const {type} = node;
const name = type === 'Identifier' ? node.name : '';
const problem = {
node,
messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
data: {
name,
method,
},
};
if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
return problem;
}
problem.suggest = [];
const {parameters, minParameters, returnsUndefined} = options;
for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
const suggest = {
messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
data: {
name,
parameters: suggestionParameters,
},
fix(fixer) {
let text = getParenthesizedText(node, context);
if (
!isParenthesized(node, context)
&& shouldAddParenthesesToCallExpressionCallee(node)
) {
text = `(${text})`;
}
return fixer.replaceTextRange(
getParenthesizedRange(node, context),
returnsUndefined
? `(${suggestionParameters}) => { ${text}(${suggestionParameters}); }`
: `(${suggestionParameters}) => ${text}(${suggestionParameters})`,
);
},
};
problem.suggest.push(suggest);
}
return problem;
}
function * getTernaryConsequentAndALternate(node) {
if (node.type === 'ConditionalExpression') {
yield * getTernaryConsequentAndALternate(node.consequent);
yield * getTernaryConsequentAndALternate(node.alternate);
return;
}
yield node;
}
// These methods have dedicated type-predicate overloads in TypeScript's lib files.
// Wrapping a type guard can lose narrowing, so direct references should be allowed here.
const methodsWithTypePredicateOverloads = new Set([
'every',
'filter',
'find',
'findLast',
]);
function hasTypePredicateReturnType(node) {
return node.returnType?.typeAnnotation?.type === 'TSTypePredicate';
}
function hasTypePredicateFunctionType(node) {
return node.typeAnnotation?.typeAnnotation?.returnType?.typeAnnotation?.type === 'TSTypePredicate';
}
function isTypePredicateCallback(callback, context) {
if (callback.type !== 'Identifier') {
return false;
}
// Keep this local and syntax-based. Imported/member expressions need type-aware linting.
const variable = findVariable(context.sourceCode.getScope(callback), callback);
const definition = variable?.defs[0];
if (!definition) {
return false;
}
if (definition.type === 'FunctionName') {
return hasTypePredicateReturnType(definition.node);
}
// Imported callbacks may be type guards, but we can't inspect their predicate return
// type without type-aware linting. Be conservative on methods with predicate overloads.
if (definition.type === 'ImportBinding') {
return true;
}
if (definition.type === 'Parameter') {
return hasTypePredicateFunctionType(definition.name);
}
if (definition.type === 'Variable') {
if (hasTypePredicateFunctionType(definition.node.id)) {
return true;
}
const {init} = definition.node;
return init
&& (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
&& hasTypePredicateReturnType(init);
}
return false;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', function * (callExpression) {
if (
!isMethodCall(callExpression, {
minimumArguments: 1,
maximumArguments: 2,
optionalCall: false,
computed: false,
})
|| callExpression.callee.property.type !== 'Identifier'
) {
return;
}
const methodNode = callExpression.callee.property;
const methodName = methodNode.name;
if (!iteratorMethods.has(methodName)) {
return;
}
const options = iteratorMethods.get(methodName);
if (options.shouldIgnoreCallExpression(callExpression)) {
return;
}
for (const callback of getTernaryConsequentAndALternate(callExpression.arguments[0])) {
if (
callback.type === 'FunctionExpression'
|| callback.type === 'ArrowFunctionExpression'
// Ignore all `CallExpression`s, including `function.bind()`
|| callback.type === 'CallExpression'
|| options.shouldIgnoreCallback(callback)
|| isNodeValueNotFunction(callback)
|| (methodsWithTypePredicateOverloads.has(methodName) && isTypePredicateCallback(callback, context))
) {
continue;
}
yield getProblem(context, callback, methodName, options);
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Prevent passing a function reference directly to iterator methods.',
recommended: true,
},
hasSuggestions: true,
messages,
},
};
export default config;
+498
View File
@@ -0,0 +1,498 @@
import {
isCommaToken,
isSemicolonToken,
isClosingParenToken,
findVariable,
hasSideEffect,
} from '@eslint-community/eslint-utils';
import {
extendFixRange,
fixSpaceAroundKeyword,
removeParentheses,
} from './fix/index.js';
import {
isArrowFunctionBody,
isMethodCall,
isReferenceIdentifier,
functionTypes,
} from './ast/index.js';
import {
needsSemicolon,
shouldAddParenthesesToExpressionStatementExpression,
shouldAddParenthesesToMemberExpressionObject,
isParenthesized,
getParentheses,
getParenthesizedRange,
isFunctionSelfUsedInside,
isNodeMatches,
assertToken,
} from './utils/index.js';
const MESSAGE_ID_ERROR = 'no-array-for-each/error';
const MESSAGE_ID_SUGGESTION = 'no-array-for-each/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Use `for…of` instead of `.forEach(…)`.',
[MESSAGE_ID_SUGGESTION]: 'Switch to `for…of`.',
};
const continueAbleNodeTypes = new Set([
'WhileStatement',
'DoWhileStatement',
'ForStatement',
'ForOfStatement',
'ForInStatement',
]);
const stripChainExpression = node =>
(node.parent.type === 'ChainExpression' && node.parent.expression === node)
? node.parent
: node;
function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction) {
for (let node = returnStatement; node && node !== callbackFunction; node = node.parent) {
if (continueAbleNodeTypes.has(node.type)) {
return true;
}
}
return false;
}
function shouldSwitchReturnStatementToBlockStatement(returnStatement) {
const {parent} = returnStatement;
switch (parent.type) {
case 'IfStatement': {
return parent.consequent === returnStatement || parent.alternate === returnStatement;
}
// These parent's body need switch to `BlockStatement` too, but since they are "continueAble", won't fix
// case 'ForStatement':
// case 'ForInStatement':
// case 'ForOfStatement':
// case 'WhileStatement':
// case 'DoWhileStatement':
case 'WithStatement': {
return parent.body === returnStatement;
}
default: {
return false;
}
}
}
function getFixFunction(callExpression, functionInfo, context) {
const {sourceCode} = context;
const [callback] = callExpression.arguments;
const parameters = callback.params;
const iterableObject = callExpression.callee.object;
const {returnStatements} = functionInfo.get(callback);
const isOptionalObject = callExpression.callee.optional;
const ancestor = stripChainExpression(callExpression).parent;
const objectText = sourceCode.getText(iterableObject);
const getForOfLoopHeadText = () => {
const [elementText, indexText] = parameters.map(parameter => sourceCode.getText(parameter));
const shouldUseEntries = parameters.length === 2;
let text = 'for (';
text += isFunctionParameterVariableReassigned(callback, sourceCode) ? 'let' : 'const';
text += ' ';
text += shouldUseEntries ? `[${indexText}, ${elementText}]` : elementText;
text += ' of ';
const shouldAddParenthesesToObject
= isParenthesized(iterableObject, context)
|| (
// `1?.forEach()` -> `(1).entries()`
isOptionalObject
&& shouldUseEntries
&& shouldAddParenthesesToMemberExpressionObject(iterableObject, context)
);
text += shouldAddParenthesesToObject ? `(${objectText})` : objectText;
if (shouldUseEntries) {
text += '.entries()';
}
text += ') ';
return text;
};
const getForOfLoopHeadRange = () => {
const [start] = sourceCode.getRange(callExpression);
const [end] = getParenthesizedRange(callback.body, context);
return [start, end];
};
function * replaceReturnStatement(returnStatement, fixer) {
const returnToken = sourceCode.getFirstToken(returnStatement);
assertToken(returnToken, {
expected: 'return',
ruleId: 'no-array-for-each',
});
if (!returnStatement.argument) {
yield fixer.replaceText(returnToken, 'continue');
return;
}
// Remove `return`
yield fixer.remove(returnToken);
const previousToken = sourceCode.getTokenBefore(returnToken);
const nextToken = sourceCode.getTokenAfter(returnToken);
let textBefore = '';
let textAfter = '';
const shouldAddParentheses
= !isParenthesized(returnStatement.argument, context)
&& shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument);
if (shouldAddParentheses) {
textBefore = `(${textBefore}`;
textAfter = `${textAfter})`;
}
const insertBraces = shouldSwitchReturnStatementToBlockStatement(returnStatement);
if (insertBraces) {
textBefore = `{ ${textBefore}`;
} else if (needsSemicolon(previousToken, context, shouldAddParentheses ? '(' : nextToken.value)) {
textBefore = `;${textBefore}`;
}
if (textBefore) {
yield fixer.insertTextBefore(nextToken, textBefore);
}
if (textAfter) {
yield fixer.insertTextAfter(returnStatement.argument, textAfter);
}
const returnStatementHasSemicolon = isSemicolonToken(sourceCode.getLastToken(returnStatement));
if (!returnStatementHasSemicolon) {
yield fixer.insertTextAfter(returnStatement, ';');
}
yield fixer.insertTextAfter(returnStatement, ' continue;');
if (insertBraces) {
yield fixer.insertTextAfter(returnStatement, ' }');
}
}
const shouldRemoveExpressionStatementLastToken = token => {
if (!isSemicolonToken(token)) {
return false;
}
if (callback.body.type !== 'BlockStatement') {
return false;
}
return true;
};
function * removeCallbackParentheses(fixer) {
// Opening parenthesis tokens already included in `getForOfLoopHeadRange`
const closingParenthesisTokens = getParentheses(callback, context)
.filter(token => isClosingParenToken(token));
for (const closingParenthesisToken of closingParenthesisTokens) {
yield fixer.remove(closingParenthesisToken);
}
}
return function * (fixer) {
// `(( foo.forEach(bar => bar) ))`
yield removeParentheses(callExpression, fixer, context);
// Replace these with `for (const … of …) `
// foo.forEach(bar => bar)
// ^^^^^^^^^^^^^^^^^^^^^^
// foo.forEach(bar => (bar))
// ^^^^^^^^^^^^^^^^^^^^^^
// foo.forEach(bar => {})
// ^^^^^^^^^^^^^^^^^^^^^^
// foo.forEach(function(bar) {})
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
yield fixer.replaceTextRange(getForOfLoopHeadRange(), getForOfLoopHeadText());
// Parenthesized callback function
// foo.forEach( ((bar => {})) )
// ^^
yield removeCallbackParentheses(fixer);
const [
penultimateToken,
lastToken,
] = sourceCode.getLastTokens(callExpression, 2);
// The possible trailing comma token of `Array#forEach()` CallExpression
// foo.forEach(bar => {},)
// ^
if (isCommaToken(penultimateToken)) {
yield fixer.remove(penultimateToken);
}
// The closing parenthesis token of `Array#forEach()` CallExpression
// foo.forEach(bar => {})
// ^
yield fixer.remove(lastToken);
for (const returnStatement of returnStatements) {
yield replaceReturnStatement(returnStatement, fixer);
}
if (ancestor.type === 'ExpressionStatement') {
const expressionStatementLastToken = sourceCode.getLastToken(ancestor);
// Remove semicolon if it's not needed anymore
// foo.forEach(bar => {});
// ^
if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) {
yield fixer.remove(expressionStatementLastToken, fixer);
}
} else if (ancestor.type === 'ArrowFunctionExpression') {
yield fixer.insertTextBefore(callExpression, '{ ');
yield fixer.insertTextAfter(callExpression, ' }');
}
yield fixSpaceAroundKeyword(fixer, callExpression.parent, context);
if (isOptionalObject) {
yield fixer.insertTextBefore(callExpression, `if (${objectText}) `);
}
// Prevent possible variable conflicts
yield extendFixRange(fixer, sourceCode.getRange(callExpression.parent));
};
}
const isChildScope = (child, parent) => {
for (let scope = child; scope; scope = scope.upper) {
if (scope === parent) {
return true;
}
}
return false;
};
function isFunctionParametersSafeToFix(callbackFunction, {sourceCode, scope, callExpression, allIdentifiers}) {
const variables = sourceCode.getDeclaredVariables(callbackFunction);
for (const variable of variables) {
if (variable.defs.length !== 1) {
return false;
}
const [definition] = variable.defs;
if (definition.type !== 'Parameter') {
continue;
}
const variableName = definition.name.name;
const [callExpressionStart, callExpressionEnd] = sourceCode.getRange(callExpression);
for (const identifier of allIdentifiers) {
const {name} = identifier;
const [start, end] = sourceCode.getRange(identifier);
if (
name !== variableName
|| start < callExpressionStart
|| end > callExpressionEnd
) {
continue;
}
const variable = findVariable(scope, identifier);
if (!variable || variable.scope === scope || isChildScope(scope, variable.scope)) {
return false;
}
}
}
return true;
}
function isFunctionParameterVariableReassigned(callbackFunction, sourceCode) {
return sourceCode.getDeclaredVariables(callbackFunction)
.filter(variable => variable.defs[0].type === 'Parameter')
.some(variable =>
variable.references.some(reference => !reference.init && reference.isWrite()));
}
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceCode}) {
// Check `CallExpression`
if (callExpression.optional || callExpression.arguments.length !== 1) {
return false;
}
// Check ancestors, we only fix `ExpressionStatement`
const callOrChainExpression = stripChainExpression(callExpression);
if (
callOrChainExpression.parent.type !== 'ExpressionStatement'
&& !isArrowFunctionBody(callOrChainExpression)
) {
return false;
}
// Check `CallExpression.arguments[0]`;
const [callback] = callExpression.arguments;
if (
// Leave non-function type to `no-array-callback-reference` rule
(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')
|| callback.async
|| callback.generator
) {
return false;
}
// Check `callback.params`
const parameters = callback.params;
if (
!(parameters.length === 1 || parameters.length === 2)
// `array.forEach((element = defaultValue) => {})`
|| (parameters.length === 1 && parameters[0].type === 'AssignmentPattern')
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1814
|| (parameters.length === 2 && parameters[1].type !== 'Identifier')
|| parameters.some(({type, typeAnnotation}) => type === 'RestElement' || typeAnnotation)
|| !isFunctionParametersSafeToFix(callback, {
scope,
callExpression,
allIdentifiers,
sourceCode,
})
) {
return false;
}
// Check `ReturnStatement`s in `callback`
const {returnStatements, scope: callbackScope} = functionInfo.get(callback);
if (returnStatements.some(returnStatement => isReturnStatementInContinueAbleNodes(returnStatement, callback))) {
return false;
}
if (isFunctionSelfUsedInside(callback, callbackScope)) {
return false;
}
return true;
}
const ignoredObjects = [
'React.Children',
'Children',
'R',
// https://www.npmjs.com/package/p-iteration
'pIteration',
// https://www.npmjs.com/package/effect
'Effect',
];
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const functionStack = [];
const callExpressions = [];
const allIdentifiers = [];
const functionInfo = new Map();
const {sourceCode} = context;
context.on(functionTypes, node => {
functionStack.push(node);
functionInfo.set(node, {
returnStatements: [],
scope: sourceCode.getScope(node),
});
});
context.onExit(functionTypes, () => {
functionStack.pop();
});
context.on('Identifier', node => {
if (isReferenceIdentifier(node)) {
allIdentifiers.push(node);
}
});
context.on('ReturnStatement', node => {
const currentFunction = functionStack.at(-1);
if (!currentFunction) {
return;
}
const {returnStatements} = functionInfo.get(currentFunction);
returnStatements.push(node);
});
context.on('CallExpression', node => {
if (
!isMethodCall(node, {
method: 'forEach',
})
|| isNodeMatches(node.callee.object, ignoredObjects)
) {
return;
}
callExpressions.push({
node,
scope: sourceCode.getScope(node),
});
});
context.onExit('Program', function * () {
for (const {node, scope} of callExpressions) {
const iterable = node.callee;
const problem = {
node: iterable.property,
messageId: MESSAGE_ID_ERROR,
};
if (!isFixable(node, {
scope,
allIdentifiers,
functionInfo,
sourceCode,
})) {
yield problem;
continue;
}
const shouldUseSuggestion = iterable.optional && hasSideEffect(iterable, sourceCode);
const fix = getFixFunction(node, functionInfo, context);
if (shouldUseSuggestion) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
fix,
},
];
} else {
problem.fix = fix;
}
yield problem;
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `for…of` over the `forEach` method.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
@@ -0,0 +1,226 @@
import {hasSideEffect} from '@eslint-community/eslint-utils';
import {removeArgument} from './fix/index.js';
import {
getParentheses,
getParenthesizedText,
shouldAddParenthesesToMemberExpressionObject,
isNodeMatches,
isNodeValueNotFunction,
} from './utils/index.js';
import {isMethodCall} from './ast/index.js';
const ERROR_PROTOTYPE_METHOD = 'error-prototype-method';
const ERROR_STATIC_METHOD = 'error-static-method';
const SUGGESTION_BIND = 'suggestion-bind';
const SUGGESTION_REMOVE = 'suggestion-remove';
const messages = {
[ERROR_PROTOTYPE_METHOD]: 'Do not use the `this` argument in `Array#{{method}}()`.',
[ERROR_STATIC_METHOD]: 'Do not use the `this` argument in `Array.{{method}}()`.',
[SUGGESTION_REMOVE]: 'Remove this argument.',
[SUGGESTION_BIND]: 'Use a bound function.',
};
const ignored = [
'lodash.every',
'_.every',
'underscore.every',
'lodash.filter',
'_.filter',
'underscore.filter',
'Vue.filter',
'R.filter',
'lodash.find',
'_.find',
'underscore.find',
'R.find',
'lodash.findLast',
'_.findLast',
'underscore.findLast',
'R.findLast',
'lodash.findIndex',
'_.findIndex',
'underscore.findIndex',
'R.findIndex',
'lodash.findLastIndex',
'_.findLastIndex',
'underscore.findLastIndex',
'R.findLastIndex',
'lodash.flatMap',
'_.flatMap',
'lodash.forEach',
'_.forEach',
'React.Children.forEach',
'Children.forEach',
'R.forEach',
'lodash.map',
'_.map',
'underscore.map',
'React.Children.map',
'Children.map',
'jQuery.map',
'$.map',
'R.map',
'lodash.some',
'_.some',
'underscore.some',
];
function removeThisArgument(thisArgumentNode, context) {
return fixer => removeArgument(fixer, thisArgumentNode, context);
}
function useBoundFunction(callbackNode, thisArgumentNode, context) {
return function * (fixer) {
yield removeThisArgument(thisArgumentNode, context)(fixer);
const callbackParentheses = getParentheses(callbackNode, context);
const isParenthesized = callbackParentheses.length > 0;
const callbackLastToken = isParenthesized
? callbackParentheses.at(-1)
: callbackNode;
if (
!isParenthesized
&& shouldAddParenthesesToMemberExpressionObject(callbackNode, context)
) {
yield fixer.insertTextBefore(callbackLastToken, '(');
yield fixer.insertTextAfter(callbackLastToken, ')');
}
const thisArgumentText = getParenthesizedText(thisArgumentNode, context);
// `thisArgument` was an argument, no need to add extra parentheses
yield fixer.insertTextAfter(callbackLastToken, `.bind(${thisArgumentText})`);
};
}
function getProblem({
context,
callExpression,
callbackNode,
thisArgumentNode,
messageId,
}) {
const problem = {
node: thisArgumentNode,
messageId,
data: {
method: callExpression.callee.property.name,
},
};
const isArrowCallback = callbackNode.type === 'ArrowFunctionExpression';
if (isArrowCallback) {
const thisArgumentHasSideEffect = hasSideEffect(thisArgumentNode, context.sourceCode);
if (thisArgumentHasSideEffect) {
problem.suggest = [
{
messageId: SUGGESTION_REMOVE,
fix: removeThisArgument(thisArgumentNode, context),
},
];
} else {
problem.fix = removeThisArgument(thisArgumentNode, context);
}
return problem;
}
problem.suggest = [
{
messageId: SUGGESTION_REMOVE,
fix: removeThisArgument(thisArgumentNode, context),
},
{
messageId: SUGGESTION_BIND,
fix: useBoundFunction(callbackNode, thisArgumentNode, context),
},
];
return problem;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
// Prototype methods
context.on('CallExpression', callExpression => {
if (
!isMethodCall(callExpression, {
methods: [
'every',
'filter',
'find',
'findLast',
'findIndex',
'findLastIndex',
'flatMap',
'forEach',
'map',
'some',
],
argumentsLength: 2,
optionalCall: false,
})
|| isNodeMatches(callExpression.callee, ignored)
|| isNodeValueNotFunction(callExpression.arguments[0])
) {
return;
}
return getProblem({
context,
callExpression,
callbackNode: callExpression.arguments[0],
thisArgumentNode: callExpression.arguments[1],
messageId: ERROR_PROTOTYPE_METHOD,
});
});
// `Array.from()` and `Array.fromAsync()`
context.on('CallExpression', callExpression => {
if (
!isMethodCall(callExpression, {
object: 'Array',
methods: ['from', 'fromAsync'],
argumentsLength: 3,
optionalCall: false,
optionalMember: false,
})
|| isNodeValueNotFunction(callExpression.arguments[1])
) {
return;
}
return getProblem({
context,
callExpression,
callbackNode: callExpression.arguments[1],
thisArgumentNode: callExpression.arguments[2],
messageId: ERROR_STATIC_METHOD,
});
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow using the `this` argument in array methods.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
+127
View File
@@ -0,0 +1,127 @@
import {isMethodCall} from './ast/index.js';
import {isNodeValueNotFunction, isArrayPrototypeProperty} from './utils/index.js';
const MESSAGE_ID_REDUCE = 'reduce';
const MESSAGE_ID_REDUCE_RIGHT = 'reduceRight';
const messages = {
[MESSAGE_ID_REDUCE]: '`Array#reduce()` is not allowed. Prefer other types of loop for readability.',
[MESSAGE_ID_REDUCE_RIGHT]: '`Array#reduceRight()` is not allowed. Prefer other types of loop for readability. You may want to call `Array#toReversed()` before looping it.',
};
const cases = [
// `array.{reduce,reduceRight}()`
{
test: callExpression =>
isMethodCall(callExpression, {
methods: ['reduce', 'reduceRight'],
minimumArguments: 1,
maximumArguments: 2,
optionalCall: false,
})
&& !isNodeValueNotFunction(callExpression.arguments[0]),
getMethodNode: callExpression => callExpression.callee.property,
isSimpleOperation(callExpression) {
const [callback] = callExpression.arguments;
return (
callback
&& (
// `array.reduce((accumulator, element) => accumulator + element)`
(callback.type === 'ArrowFunctionExpression' && callback.body.type === 'BinaryExpression')
// `array.reduce((accumulator, element) => {return accumulator + element;})`
// `array.reduce(function (accumulator, element){return accumulator + element;})`
|| (
(callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
&& callback.body.type === 'BlockStatement'
&& callback.body.body.length === 1
&& callback.body.body[0].type === 'ReturnStatement'
&& callback.body.body[0].argument.type === 'BinaryExpression'
)
)
);
},
},
// `[].{reduce,reduceRight}.call()` and `Array.{reduce,reduceRight}.call()`
{
test: callExpression =>
isMethodCall(callExpression, {
method: 'call',
optionalCall: false,
optionalMember: false,
})
&& isArrayPrototypeProperty(callExpression.callee.object, {
properties: ['reduce', 'reduceRight'],
})
&& (
!callExpression.arguments[1]
|| !isNodeValueNotFunction(callExpression.arguments[1])
),
getMethodNode: callExpression => callExpression.callee.object.property,
},
// `[].{reduce,reduceRight}.apply()` and `Array.{reduce,reduceRight}.apply()`
{
test: callExpression =>
isMethodCall(callExpression, {
method: 'apply',
optionalCall: false,
optionalMember: false,
})
&& isArrayPrototypeProperty(callExpression.callee.object, {
properties: ['reduce', 'reduceRight'],
}),
getMethodNode: callExpression => callExpression.callee.object.property,
},
];
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
allowSimpleOperations: {
type: 'boolean',
description: 'Whether to allow simple reduce operations whose callback body is a single binary expression.',
},
},
},
];
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {allowSimpleOperations} = context.options[0];
context.on('CallExpression', function * (callExpression) {
for (const {test, getMethodNode, isSimpleOperation} of cases) {
if (!test(callExpression)) {
continue;
}
if (allowSimpleOperations && isSimpleOperation?.(callExpression)) {
continue;
}
const methodNode = getMethodNode(callExpression);
yield {
node: methodNode,
messageId: methodNode.name,
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.',
recommended: true,
},
schema,
defaultOptions: [{allowSimpleOperations: true}],
messages,
},
};
export default config;
@@ -0,0 +1,6 @@
import noArrayMutateRule from './shared/no-array-mutate-rule.js';
/** @type {import('eslint').Rule.RuleModule} */
const config = noArrayMutateRule('reverse');
export default config;
+6
View File
@@ -0,0 +1,6 @@
import noArrayMutateRule from './shared/no-array-mutate-rule.js';
/** @type {import('eslint').Rule.RuleModule} */
const config = noArrayMutateRule('sort');
export default config;
@@ -0,0 +1,85 @@
import {removeParentheses, removeMemberExpressionProperty} from './fix/index.js';
import {isLiteral} from './ast/index.js';
const MESSAGE_ID = 'no-await-expression-member';
const messages = {
[MESSAGE_ID]: 'Do not access a member directly from an await expression.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('MemberExpression', memberExpression => {
if (memberExpression.object.type !== 'AwaitExpression') {
return;
}
const {property} = memberExpression;
const problem = {
node: property,
messageId: MESSAGE_ID,
};
// `const foo = (await bar)[0]`
if (
memberExpression.computed
&& !memberExpression.optional
&& (isLiteral(property, 0) || isLiteral(property, 1))
&& memberExpression.parent.type === 'VariableDeclarator'
&& memberExpression.parent.init === memberExpression
&& memberExpression.parent.id.type === 'Identifier'
&& !memberExpression.parent.id.typeAnnotation
) {
problem.fix = function * (fixer) {
const variable = memberExpression.parent.id;
yield fixer.insertTextBefore(variable, property.value === 0 ? '[' : '[, ');
yield fixer.insertTextAfter(variable, ']');
yield removeMemberExpressionProperty(fixer, memberExpression, context);
yield removeParentheses(memberExpression.object, fixer, context);
};
return problem;
}
// `const foo = (await bar).foo`
if (
!memberExpression.computed
&& !memberExpression.optional
&& property.type === 'Identifier'
&& memberExpression.parent.type === 'VariableDeclarator'
&& memberExpression.parent.init === memberExpression
&& memberExpression.parent.id.type === 'Identifier'
&& memberExpression.parent.id.name === property.name
&& !memberExpression.parent.id.typeAnnotation
) {
problem.fix = function * (fixer) {
const variable = memberExpression.parent.id;
yield fixer.insertTextBefore(variable, '{');
yield fixer.insertTextAfter(variable, '}');
yield removeMemberExpressionProperty(fixer, memberExpression, context);
yield removeParentheses(memberExpression.object, fixer, context);
};
return problem;
}
return problem;
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow member access from await expression.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,69 @@
import {isMethodCall} from './ast/index.js';
import {removeSpacesAfter} from './fix/index.js';
const MESSAGE_ID_ERROR = 'no-await-in-promise-methods/error';
const MESSAGE_ID_SUGGESTION = 'no-await-in-promise-methods/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Promise in `Promise.{{method}}()` should not be awaited.',
[MESSAGE_ID_SUGGESTION]: 'Remove `await`.',
};
const METHODS = ['all', 'allSettled', 'any', 'race'];
const isPromiseMethodCallWithArrayExpression = node =>
isMethodCall(node, {
object: 'Promise',
methods: METHODS,
optionalMember: false,
optionalCall: false,
argumentsLength: 1,
})
&& node.arguments[0].type === 'ArrayExpression';
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', function * (callExpression) {
if (!isPromiseMethodCallWithArrayExpression(callExpression)) {
return;
}
for (const element of callExpression.arguments[0].elements) {
if (element?.type !== 'AwaitExpression') {
continue;
}
yield {
node: element,
messageId: MESSAGE_ID_ERROR,
data: {
method: callExpression.callee.property.name,
},
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION,
* fix(fixer) {
const awaitToken = context.sourceCode.getFirstToken(element);
yield fixer.remove(awaitToken);
yield removeSpacesAfter(awaitToken, context, fixer);
},
},
],
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow using `await` in `Promise` method parameters.',
recommended: 'unopinionated',
},
hasSuggestions: true,
messages,
},
};
export default config;
+87
View File
@@ -0,0 +1,87 @@
import toLocation from './utils/to-location.js';
import {isStringLiteral, isMethodCall} from './ast/index.js';
const MESSAGE_ID = 'no-console-spaces';
const messages = {
[MESSAGE_ID]: 'Do not use {{position}} space between `console.{{method}}` parameters.',
};
// Find exactly one leading space, allow exactly one space
const hasLeadingSpace = value => value.length > 1 && value.charAt(0) === ' ' && value.charAt(1) !== ' ';
// Find exactly one trailing space, allow exactly one space
const hasTrailingSpace = value => value.length > 1 && value.at(-1) === ' ' && value.at(-2) !== ' ';
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
const getProblem = (node, method, position) => {
const [start, end] = sourceCode.getRange(node);
const index = position === 'leading'
? start + 1
: end - 2;
const range = [index, index + 1];
return {
loc: toLocation(range, context),
messageId: MESSAGE_ID,
data: {method, position},
fix: fixer => fixer.removeRange(range),
};
};
context.on('CallExpression', function * (node) {
if (
!isMethodCall(node, {
object: 'console',
methods: [
'log',
'debug',
'info',
'warn',
'error',
],
minimumArguments: 1,
optionalCall: false,
optionalMember: false,
})
) {
return;
}
const method = node.callee.property.name;
const {arguments: messages} = node;
const {length} = messages;
for (const [index, node] of messages.entries()) {
if (!isStringLiteral(node) && node.type !== 'TemplateLiteral') {
continue;
}
const raw = sourceCode.getText(node).slice(1, -1);
if (index !== 0 && hasLeadingSpace(raw)) {
yield getProblem(node, method, 'leading');
}
if (index !== length - 1 && hasTrailingSpace(raw)) {
yield getProblem(node, method, 'trailing');
}
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Do not use leading/trailing space between `console.log` parameters.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,29 @@
import {GlobalReferenceTracker} from './utils/global-reference-tracker.js';
const MESSAGE_ID = 'no-document-cookie';
const messages = {
[MESSAGE_ID]: 'Do not use `document.cookie` directly.',
};
const tracker = new GlobalReferenceTracker({
object: 'document.cookie',
filter: ({node}) => node.parent.type === 'AssignmentExpression' && node.parent.left === node,
handle: ({node}) => ({node, messageId: MESSAGE_ID}),
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create(context) {
tracker.listen({context});
},
meta: {
type: 'problem',
docs: {
description: 'Do not use `document.cookie` directly.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
+56
View File
@@ -0,0 +1,56 @@
import {isEmptyNode, isDirective} from './ast/index.js';
const MESSAGE_ID = 'no-empty-file';
const messages = {
[MESSAGE_ID]: 'Empty files are not allowed.',
};
const isEmpty = node => isEmptyNode(node, isDirective);
const isTripleSlashDirective = node =>
node.type === 'Line' && node.value.startsWith('/');
const hasTripeSlashDirectives = comments =>
comments.some(currentNode => isTripleSlashDirective(currentNode));
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const filename = context.physicalFilename;
if (!/\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i.test(filename)) {
return;
}
context.on('Program', node => {
if (node.body.some(node => !isEmpty(node))) {
return;
}
const {sourceCode} = context;
const comments = sourceCode.getAllComments();
if (hasTripeSlashDirectives(comments)) {
return;
}
return {
node,
messageId: MESSAGE_ID,
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow empty files.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
+493
View File
@@ -0,0 +1,493 @@
import {isClosingParenToken, getStaticValue} from '@eslint-community/eslint-utils';
import {
getAvailableVariableName,
getScopes,
singular,
toLocation,
getReferences,
} from './utils/index.js';
import {isLiteral} from './ast/index.js';
const MESSAGE_ID = 'no-for-loop';
const messages = {
[MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.',
};
const defaultElementName = 'element';
const isLiteralZero = node => isLiteral(node, 0);
const isLiteralOne = node => isLiteral(node, 1);
const isIdentifierWithName = (node, name) => node?.type === 'Identifier' && node.name === name;
const getTypeReferenceTypeAnnotation = (typeReferenceName, scope) => {
const typeVariable = scope && resolveIdentifierName(typeReferenceName, scope);
const [definition] = typeVariable?.defs ?? [];
if (!definition || definition.type !== 'Type') {
return;
}
if (definition.node.type === 'TSTypeAliasDeclaration') {
return definition.node.typeAnnotation;
}
if (definition.node.type === 'TSTypeParameter') {
return definition.node.constraint;
}
};
const isArrayTypeReference = (node, scope, visitedTypeReferenceNames) => {
if (node.typeName.type !== 'Identifier') {
return false;
}
const typeReferenceName = node.typeName.name;
if (typeReferenceName === 'Array' || typeReferenceName === 'ReadonlyArray') {
return true;
}
if (visitedTypeReferenceNames.has(typeReferenceName)) {
return false;
}
visitedTypeReferenceNames.add(typeReferenceName);
const typeAnnotation = getTypeReferenceTypeAnnotation(typeReferenceName, scope);
const isArray = isArrayType(typeAnnotation, scope, visitedTypeReferenceNames);
visitedTypeReferenceNames.delete(typeReferenceName);
return isArray;
};
const isArrayType = (node, scope, visitedTypeReferenceNames = new Set()) => {
switch (node?.type) {
case 'TSArrayType':
case 'TSTupleType': {
return true;
}
case 'TSTypeReference': {
return isArrayTypeReference(node, scope, visitedTypeReferenceNames);
}
case 'TSTypeOperator': {
return node.operator === 'readonly' && isArrayType(node.typeAnnotation, scope, visitedTypeReferenceNames);
}
case 'TSUnionType': {
return node.types.every(type => isArrayType(type, scope, visitedTypeReferenceNames));
}
case 'TSIntersectionType': {
return node.types.some(type => isArrayType(type, scope, visitedTypeReferenceNames));
}
default: {
return false;
}
}
};
const getIndexIdentifierName = forStatement => {
const {init: variableDeclaration} = forStatement;
if (
!variableDeclaration
|| variableDeclaration.type !== 'VariableDeclaration'
) {
return;
}
if (variableDeclaration.declarations.length !== 1) {
return;
}
const [variableDeclarator] = variableDeclaration.declarations;
if (!isLiteralZero(variableDeclarator.init)) {
return;
}
if (variableDeclarator.id.type !== 'Identifier') {
return;
}
return variableDeclarator.id.name;
};
const getStrictComparisonOperands = binaryExpression => {
if (binaryExpression.operator === '<') {
return {
lesser: binaryExpression.left,
greater: binaryExpression.right,
};
}
if (binaryExpression.operator === '>') {
return {
lesser: binaryExpression.right,
greater: binaryExpression.left,
};
}
};
const getArrayIdentifierFromBinaryExpression = (binaryExpression, indexIdentifierName) => {
const operands = getStrictComparisonOperands(binaryExpression);
if (!operands) {
return;
}
const {lesser, greater} = operands;
if (!isIdentifierWithName(lesser, indexIdentifierName)) {
return;
}
if (greater.type !== 'MemberExpression') {
return;
}
if (
greater.object.type !== 'Identifier'
|| greater.property.type !== 'Identifier'
) {
return;
}
if (greater.property.name !== 'length') {
return;
}
return greater.object;
};
const getArrayIdentifier = (forStatement, indexIdentifierName) => {
const {test} = forStatement;
if (!test || test.type !== 'BinaryExpression') {
return;
}
return getArrayIdentifierFromBinaryExpression(test, indexIdentifierName);
};
const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {
if (node?.type === 'BinaryExpression' && node.operator === '+') {
return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right))
|| (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left));
}
return false;
};
const checkUpdateExpression = (forStatement, indexIdentifierName) => {
const {update} = forStatement;
if (!update) {
return false;
}
if (update.type === 'UpdateExpression') {
return update.operator === '++' && isIdentifierWithName(update.argument, indexIdentifierName);
}
if (
update.type === 'AssignmentExpression'
&& isIdentifierWithName(update.left, indexIdentifierName)
) {
if (update.operator === '+=') {
return isLiteralOne(update.right);
}
if (update.operator === '=') {
return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);
}
}
return false;
};
const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => arrayReferences.every(reference => {
const node = reference.identifier.parent;
if (node.type !== 'MemberExpression') {
return false;
}
if (node.property.name !== indexIdentifierName) {
return false;
}
if (
node.parent.type === 'AssignmentExpression'
&& node.parent.left === node
) {
return false;
}
return true;
});
const getRemovalRange = (node, sourceCode) => {
const declarationNode = node.parent;
if (declarationNode.declarations.length === 1) {
const {line} = sourceCode.getLoc(declarationNode).start;
const lineText = sourceCode.lines[line - 1];
const isOnlyNodeOnLine = lineText.trim() === sourceCode.getText(declarationNode);
return isOnlyNodeOnLine
? [
sourceCode.getIndexFromLoc({line, column: 0}),
sourceCode.getIndexFromLoc({line: line + 1, column: 0}),
]
: sourceCode.getRange(declarationNode);
}
const index = declarationNode.declarations.indexOf(node);
if (index === 0) {
return [
sourceCode.getRange(node)[0],
sourceCode.getRange(declarationNode.declarations[1])[0],
];
}
return [
sourceCode.getRange(declarationNode.declarations[index - 1])[1],
sourceCode.getRange(node)[1],
];
};
const resolveIdentifierName = (name, scope) => {
while (scope) {
const variable = scope.set.get(name);
if (variable) {
return variable;
}
scope = scope.upper;
}
};
const scopeContains = (ancestor, descendant) => {
while (descendant) {
if (descendant === ancestor) {
return true;
}
descendant = descendant.upper;
}
return false;
};
const nodeContains = (ancestor, descendant) => {
while (descendant) {
if (descendant === ancestor) {
return true;
}
descendant = descendant.parent;
}
return false;
};
const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arrayIdentifierName) => {
const inBodyReferences = indexVariable.references.filter(reference => scopeContains(bodyScope, reference.from));
const referencesOtherThanArrayAccess = inBodyReferences.filter(reference => {
const node = reference.identifier.parent;
if (node.type !== 'MemberExpression') {
return true;
}
if (node.object.name !== arrayIdentifierName) {
return true;
}
return false;
});
return referencesOtherThanArrayAccess.length > 0;
};
const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) =>
indexVariable.references
.filter(reference => scopeContains(bodyScope, reference.from))
.some(inBodyReference => inBodyReference.isWrite());
const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) =>
variables.some(variable => !variable.references.every(reference => scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier)));
const getReferencesInChildScopes = (scope, name) =>
getReferences(scope).filter(reference => reference.identifier.name === name);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
const {scopeManager} = sourceCode;
context.on('ForStatement', node => {
const indexIdentifierName = getIndexIdentifierName(node);
if (!indexIdentifierName) {
return;
}
const arrayIdentifier = getArrayIdentifier(node, indexIdentifierName);
if (!arrayIdentifier) {
return;
}
const arrayIdentifierName = arrayIdentifier.name;
const scope = sourceCode.getScope(node);
const staticResult = getStaticValue(arrayIdentifier, scope);
if (staticResult && !Array.isArray(staticResult.value)) {
// Bail out if we can tell that the array variable has a non-array value (i.e. we're looping through the characters of a string constant).
return;
}
if (!checkUpdateExpression(node, indexIdentifierName)) {
return;
}
if (!node.body || node.body.type !== 'BlockStatement') {
return;
}
const forScope = scopeManager.acquire(node);
const bodyScope = scopeManager.acquire(node.body);
if (!bodyScope) {
return;
}
const indexVariable = resolveIdentifierName(indexIdentifierName, bodyScope);
if (isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {
return;
}
const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);
if (arrayReferences.length === 0) {
return;
}
if (!isOnlyArrayOfIndexVariableRead(arrayReferences, indexIdentifierName)) {
return;
}
const [start] = sourceCode.getRange(node);
const closingParenthesisToken = sourceCode.getTokenBefore(node.body, isClosingParenToken);
const [, end] = sourceCode.getRange(closingParenthesisToken);
const problem = {
loc: toLocation([start, end], context),
messageId: MESSAGE_ID,
};
const elementReference = arrayReferences.find(reference => {
const node = reference.identifier.parent;
if (node.parent.type !== 'VariableDeclarator') {
return false;
}
return true;
});
const elementNode = elementReference?.identifier.parent.parent;
const elementIdentifierName = elementNode?.id.name;
const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);
const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
// When `.entries()` would be generated, only autofix if the type annotation confirms it's an array (or there's no type annotation).
const hasNonArrayTypeAnnotation = resolveIdentifierName(arrayIdentifierName, scope)
?.defs.some(definition => {
const typeAnnotation = definition.name.typeAnnotation?.typeAnnotation;
return typeAnnotation && !isArrayType(typeAnnotation, scope);
});
const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope)
&& !elementNode?.id.typeAnnotation
&& !(hasNonArrayTypeAnnotation && shouldGenerateIndex);
if (shouldFix) {
problem.fix = function * (fixer) {
const index = indexIdentifierName;
const element = elementIdentifierName
|| getAvailableVariableName(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));
const array = arrayIdentifierName;
let declarationElement = element;
let declarationType = 'const';
let removeDeclaration = true;
if (elementNode) {
if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {
removeDeclaration = arrayReferences.length === 1;
}
if (removeDeclaration) {
declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;
declarationElement = sourceCode.getText(elementNode.id);
}
}
const parts = [declarationType];
if (shouldGenerateIndex) {
parts.push(` [${index}, ${declarationElement}] of ${array}.entries()`);
} else {
parts.push(` ${declarationElement} of ${array}`);
}
const replacement = parts.join('');
const [start] = sourceCode.getRange(node.init);
const [, end] = sourceCode.getRange(node.update);
yield fixer.replaceTextRange([start, end], replacement);
for (const reference of arrayReferences) {
if (reference !== elementReference) {
yield fixer.replaceText(reference.identifier.parent, element);
}
}
if (elementNode) {
yield removeDeclaration
? fixer.removeRange(getRemovalRange(elementNode, sourceCode))
: fixer.replaceText(elementNode.init, element);
}
};
}
return problem;
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
+55
View File
@@ -0,0 +1,55 @@
import {replaceTemplateElement} from './fix/index.js';
import {isStringLiteral, isRegexLiteral, isTaggedTemplateLiteral} from './ast/index.js';
const MESSAGE_ID = 'no-hex-escape';
const messages = {
[MESSAGE_ID]: 'Use Unicode escapes instead of hexadecimal escapes.',
};
function checkEscape(context, node, value) {
const fixedValue = value.replaceAll(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00');
if (value !== fixedValue) {
return {
node,
messageId: MESSAGE_ID,
fix: fixer =>
node.type === 'TemplateElement'
? replaceTemplateElement(node, fixedValue, context, fixer)
: fixer.replaceText(node, fixedValue),
};
}
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('Literal', node => {
if (isStringLiteral(node) || isRegexLiteral(node)) {
return checkEscape(context, node, node.raw);
}
});
context.on('TemplateElement', node => {
if (isTaggedTemplateLiteral(node.parent, ['String.raw'])) {
return;
}
return checkEscape(context, node, node.value.raw);
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce the use of Unicode escapes instead of hexadecimal escapes.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,778 @@
import {
hasSideEffect,
isCommaToken,
isSemicolonToken,
findVariable,
} from '@eslint-community/eslint-utils';
import {
isMethodCall,
isMemberExpression,
isNewExpression,
} from './ast/index.js';
import {
removeExpressionStatement,
removeArgument,
} from './fix/index.js';
import {
getNextNode,
getCallExpressionArgumentsText,
getParenthesizedText,
getVariableIdentifiers,
getNewExpressionTokens,
isNewExpressionWithParentheses,
} from './utils/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
const MESSAGE_ID_ERROR = 'error';
const MESSAGE_ID_SUGGESTION_ARRAY = 'suggestion/array';
const MESSAGE_ID_SUGGESTION_OBJECT = 'suggestion/object';
const MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN = 'suggestion/object-assign';
const MESSAGE_ID_SUGGESTION_SET = 'suggestion/set';
const MESSAGE_ID_SUGGESTION_MAP = 'suggestion/map';
const messages = {
[MESSAGE_ID_ERROR]: 'Immediate mutation on {{objectType}} is not allowed.',
[MESSAGE_ID_SUGGESTION_ARRAY]: '{{operation}} the elements to the {{assignType}}.',
[MESSAGE_ID_SUGGESTION_OBJECT]: 'Move this property to the {{assignType}}.',
[MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN]: '{{description}} the {{assignType}}.',
[MESSAGE_ID_SUGGESTION_SET]: 'Move the element to the {{assignType}}.',
[MESSAGE_ID_SUGGESTION_MAP]: 'Move the entry to the {{assignType}}.',
};
const hasVariableInNodes = (variable, nodes, context) => {
const {sourceCode} = context;
const identifiers = getVariableIdentifiers(variable);
return nodes.some(node => {
const range = sourceCode.getRange(node);
return identifiers.some(identifier => {
const [start, end] = sourceCode.getRange(identifier);
return start >= range[0] && end <= range[1];
});
});
};
function isCallExpressionWithOptionalArrayExpression(newExpression, names) {
if (!isNewExpression(
newExpression,
{names, maximumArguments: 1},
)) {
return false;
}
// `new Set();` and `new Set([]);`
const [iterable] = newExpression.arguments;
return (!iterable || iterable.type === 'ArrayExpression');
}
function * removeExpressionStatementAfterAssign(expressionStatement, context, fixer) {
const tokenBefore = context.sourceCode.getTokenBefore(expressionStatement);
const shouldPreserveSemiColon = !isSemicolonToken(tokenBefore);
yield removeExpressionStatement(expressionStatement, context, fixer, shouldPreserveSemiColon);
}
function appendListTextToArrayExpressionOrObjectExpression(
context,
fixer,
arrayOrObjectExpression,
listText,
) {
const {sourceCode} = context;
const [
penultimateToken,
closingBracketToken,
] = sourceCode.getLastTokens(arrayOrObjectExpression, 2);
const list = arrayOrObjectExpression.type === 'ArrayExpression'
? arrayOrObjectExpression.elements
: arrayOrObjectExpression.properties;
const shouldInsertComma = list.length > 0 && !isCommaToken(penultimateToken);
return fixer.insertTextBefore(
closingBracketToken,
`${shouldInsertComma ? ',' : ''} ${listText}`,
);
}
function * appendElementsTextToSetConstructor({
context,
fixer,
newExpression,
elementsText,
nextExpressionStatement,
}) {
if (isNewExpressionWithParentheses(newExpression, context)) {
const [setInitialValue] = newExpression.arguments;
if (setInitialValue) {
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, setInitialValue, elementsText);
} else {
const {
openingParenthesisToken,
} = getNewExpressionTokens(newExpression, context);
yield fixer.insertTextAfter(openingParenthesisToken, `[${elementsText}]`);
}
} else {
/*
The new expression doesn't have parentheses
```
const set = (( new (( Set )) ));
set.add(1);
```
*/
yield fixer.insertTextAfter(newExpression, `([${elementsText}])`);
}
yield * removeExpressionStatementAfterAssign(nextExpressionStatement, context, fixer);
}
function getObjectExpressionPropertiesText(objectExpression, context) {
const {sourceCode} = context;
const openingBraceToken = sourceCode.getFirstToken(objectExpression);
const [penultimateToken, closingBraceToken] = sourceCode.getLastTokens(objectExpression, 2);
const [, start] = sourceCode.getRange(openingBraceToken);
const [end] = sourceCode.getRange(isCommaToken(penultimateToken) ? penultimateToken : closingBraceToken);
return sourceCode.text.slice(start, end);
}
/**
@typedef {ESTree.VariableDeclarator['init'] | ESTree.AssignmentExpression['right']} ValueNode
@typedef {(information: ViolationCaseInformation, arguments: any)} GetFix
@typedef {Parameters<ESLint.Rule.RuleContext['report']>[0]} Problem
@typedef {(information: ViolationCaseInformation) => ESTree.Node} GetProblematicNode
@typedef {{
context: ESLint.Rule.RuleContext,
variable: ESLint.Scope.Variable,
variableNode: ESTree.Identifier,
valueNode: ValueNode,
statement: ESTree.VariableDeclaration | ESTree.ExpressionStatement,
nextExpressionStatement: ESTree.ExpressionStatement,
assignType: 'assignment' | 'declaration',
getFix: GetFix,
}} ViolationCaseInformation
@typedef {{
testValue: (value: ValueNode) => boolean,
getProblematicNode: GetProblematicNode,
getProblem: (node: ReturnType<GetProblematicNode>, information: ViolationCaseInformation) => Problem,
getFix: GetFix,
}} ViolationCase
*/
// `Array`
/** @type {ViolationCase} */
const arrayMutationSettings = {
testValue: value => value?.type === 'ArrayExpression',
getProblematicNode({
context,
variable,
nextExpressionStatement,
}) {
const callExpression = nextExpressionStatement.expression;
if (!(
isMethodCall(callExpression, {
object: variable.name,
methods: ['push', 'unshift'],
optionalMember: false,
optionalCall: false,
})
&& callExpression.arguments.length > 0
)) {
return;
}
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
return;
}
return callExpression;
},
getProblem(callExpression, information) {
const {
context,
assignType,
getFix,
} = information;
const {sourceCode} = context;
const memberExpression = callExpression.callee;
const method = memberExpression.property;
const problem = {
node: memberExpression,
messageId: MESSAGE_ID_ERROR,
data: {objectType: 'array'},
};
const isPrepend = method.name === 'unshift';
const fix = getFix(information, {
callExpression,
isPrepend,
});
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_ARRAY,
fix,
data: {operation: isPrepend ? 'Prepend' : 'Append', assignType},
},
];
} else {
problem.fix = fix;
}
return problem;
},
getFix: (
{
context,
valueNode: arrayExpression,
nextExpressionStatement,
},
{
callExpression,
isPrepend,
},
) => function * (fixer) {
const text = getCallExpressionArgumentsText(context, callExpression, /* includeTrailingComma */ false);
yield (
isPrepend
? fixer.insertTextAfter(
context.sourceCode.getFirstToken(arrayExpression),
`${text}, `,
)
: appendListTextToArrayExpressionOrObjectExpression(context, fixer, arrayExpression, text)
);
yield removeExpressionStatementAfterAssign(
nextExpressionStatement,
context,
fixer,
);
},
};
// `Object` + `AssignmentExpression`
/** @type {ViolationCase} */
const objectWithAssignmentExpressionSettings = {
testValue: value => value?.type === 'ObjectExpression',
getProblematicNode({
context,
variable,
nextExpressionStatement,
}) {
const assignmentExpression = nextExpressionStatement.expression;
if (!(
assignmentExpression.type === 'AssignmentExpression'
&& assignmentExpression.operator === '='
&& isMemberExpression(assignmentExpression.left, {object: variable.name, optional: false})
)) {
return;
}
const value = assignmentExpression.right;
const memberExpression = assignmentExpression.left;
const {property} = memberExpression;
if (
hasVariableInNodes(
variable,
memberExpression.computed ? [property, value] : [value],
context,
)
) {
return;
}
return assignmentExpression;
},
getProblem(assignmentExpression, information) {
const {
context,
assignType,
getFix,
} = information;
const {sourceCode} = context;
const {
left: memberExpression,
right: value,
} = assignmentExpression;
const {property} = memberExpression;
const operatorToken = sourceCode.getTokenAfter(memberExpression, token => token.type === 'Punctuator' && token.value === assignmentExpression.operator);
const problem = {
node: assignmentExpression,
loc: {
start: sourceCode.getLoc(assignmentExpression).start,
end: sourceCode.getLoc(operatorToken).end,
},
messageId: MESSAGE_ID_ERROR,
data: {objectType: 'object'},
};
const fix = getFix(information, {
assignmentExpression,
memberExpression,
property,
value,
});
if (
(memberExpression.computed && hasSideEffect(property, sourceCode))
|| hasSideEffect(value, sourceCode)
) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_OBJECT,
data: {assignType},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
},
getFix: (
{
context,
valueNode: objectExpression,
nextExpressionStatement,
},
{
memberExpression,
property,
value,
},
) => function * (fixer) {
let propertyText = getParenthesizedText(property, context);
if (memberExpression.computed) {
propertyText = `[${propertyText}]`;
}
const valueText = getParenthesizedText(value, context);
const text = `${propertyText}: ${valueText},`;
const [
penultimateToken,
closingBraceToken,
] = context.sourceCode.getLastTokens(objectExpression, 2);
const shouldInsertComma = objectExpression.properties.length > 0 && !isCommaToken(penultimateToken);
yield fixer.insertTextBefore(
closingBraceToken,
`${shouldInsertComma ? ',' : ''} ${text}`,
);
yield removeExpressionStatementAfterAssign(
nextExpressionStatement,
context,
fixer,
);
},
};
// `Object` + `Object.assign()`
/** @type {ViolationCase} */
const objectWithObjectAssignSettings = {
testValue: value => value?.type === 'ObjectExpression',
getProblematicNode({
context,
variable,
nextExpressionStatement,
}) {
const callExpression = nextExpressionStatement.expression;
if (!isMethodCall(callExpression, {
object: 'Object',
method: 'assign',
minimumArguments: 2,
optionalMember: false,
optionalCall: false,
})) {
return;
}
const [object, firstValue] = callExpression.arguments;
if (
!(object.type === 'Identifier' && object.name === variable.name)
|| firstValue.type === 'SpreadElement'
|| hasVariableInNodes(variable, [firstValue], context)
) {
return;
}
return callExpression;
},
getProblem(callExpression, information) {
const {
context,
assignType,
getFix,
} = information;
const {sourceCode} = context;
const [, firstValue] = callExpression.arguments;
const problem = {
node: callExpression.callee,
messageId: MESSAGE_ID_ERROR,
data: {objectType: 'object'},
};
const fix = getFix(information, {
callExpression,
firstValue,
});
if (hasSideEffect(firstValue, sourceCode)) {
const description = firstValue.type === 'ObjectExpression'
? 'Move properties to'
: 'Spread properties in';
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN,
data: {description, assignType},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
},
getFix: (
{
context,
valueNode: objectExpression,
nextExpressionStatement,
},
{
callExpression,
firstValue,
},
) => function * (fixer) {
let text;
if (firstValue.type === 'ObjectExpression') {
if (firstValue.properties.length > 0) {
text = getObjectExpressionPropertiesText(firstValue, context);
}
} else {
text = `...${getParenthesizedText(firstValue, context)}`;
}
if (text) {
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, objectExpression, text);
}
if (callExpression.arguments.length !== 2) {
yield removeArgument(fixer, firstValue, context);
return;
}
yield removeExpressionStatementAfterAssign(
nextExpressionStatement,
context,
fixer,
);
},
};
// `Set` and `WeakSet`
/** @type {ViolationCase} */
const setMutationSettings = {
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Set', 'WeakSet']),
getProblematicNode({
context,
variable,
nextExpressionStatement,
}) {
let callExpression = nextExpressionStatement.expression;
if (callExpression.type === 'ChainExpression') {
callExpression = callExpression.expression;
}
if (!isMethodCall(callExpression, {
object: variable.name,
method: 'add',
argumentsLength: 1,
optionalMember: false,
optionalCall: false,
})) {
return;
}
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
return;
}
return callExpression;
},
getProblem(callExpression, information) {
const {
context,
assignType,
valueNode: newExpression,
getFix,
} = information;
const {sourceCode} = context;
const memberExpression = callExpression.callee;
const problem = {
node: memberExpression,
messageId: MESSAGE_ID_ERROR,
data: {objectType: `\`${newExpression.callee.name}\``},
};
const fix = getFix(information, {
callExpression,
newExpression,
});
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_SET,
data: {assignType},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
},
getFix: (
{
context,
nextExpressionStatement,
},
{
callExpression,
newExpression,
},
) => fixer => {
const elementsText = getCallExpressionArgumentsText(
context,
callExpression,
/* IncludeTrailingComma */ false,
);
return appendElementsTextToSetConstructor({
context,
fixer,
newExpression,
elementsText,
nextExpressionStatement,
});
},
};
// `Map` and `WeakMap`
/** @type {ViolationCase} */
const mapMutationSettings = {
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Map', 'WeakMap']),
getProblematicNode({
context,
variable,
nextExpressionStatement,
}) {
const callExpression = nextExpressionStatement.expression;
if (!isMethodCall(callExpression, {
object: variable.name,
method: 'set',
argumentsLength: 2,
optionalCall: false,
})) {
return;
}
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
return;
}
return callExpression;
},
getProblem(callExpression, information) {
const {
context,
assignType,
valueNode: newExpression,
getFix,
} = information;
const {sourceCode} = context;
const memberExpression = callExpression.callee;
const problem = {
node: memberExpression,
messageId: MESSAGE_ID_ERROR,
data: {objectType: `\`${newExpression.callee.name}\``},
};
const fix = getFix(information, {
callExpression,
newExpression,
});
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_MAP,
data: {assignType},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
},
getFix: (
{
context,
nextExpressionStatement,
},
{
callExpression,
newExpression,
},
) => fixer => {
const argumentsText = getCallExpressionArgumentsText(
context,
callExpression,
/* IncludeTrailingComma */ false,
);
const entryText = `[${argumentsText}]`;
return appendElementsTextToSetConstructor({
context,
fixer,
newExpression,
elementsText: entryText,
nextExpressionStatement,
});
},
};
const cases = [
arrayMutationSettings,
objectWithAssignmentExpressionSettings,
objectWithObjectAssignSettings,
setMutationSettings,
mapMutationSettings,
];
function isLastDeclarator(variableDeclarator) {
const variableDeclaration = variableDeclarator.parent;
return (
variableDeclaration.type === 'VariableDeclaration'
&& variableDeclaration.declarations.at(-1) === variableDeclarator
);
}
const getVariable = (node, context) => {
if (node.type === 'VariableDeclarator') {
return context.sourceCode.getDeclaredVariables(node)
.find(variable => variable.defs.length === 1 && variable.defs[0].name === node.id);
}
return findVariable(context.sourceCode.getScope(node), node.left.name);
};
function getCaseProblem(
context,
assignNode,
{
testValue,
getProblematicNode,
getProblem,
getFix,
},
) {
const isAssignment = assignNode.type === 'AssignmentExpression';
const [variableNode, valueNode] = (isAssignment ? ['left', 'right'] : ['id', 'init'])
.map(property => assignNode[property]);
// eslint-disable-next-line no-warning-comments
// TODO[@fisker]: `AssignmentExpression` should not limit to `Identifier`
if (!(variableNode.type === 'Identifier' && testValue(valueNode))) {
return;
}
const statement = assignNode.parent;
if (!(
// eslint-disable-next-line no-warning-comments
// TODO[@fisker]: `AssignmentExpression` should support `a = b = c` too
(
isAssignment
&& assignNode.operator === '='
&& statement.type === 'ExpressionStatement'
&& statement.expression === assignNode)
|| (!isAssignment && isLastDeclarator(assignNode))
)) {
return;
}
const nextExpressionStatement = getNextNode(statement, context);
if (nextExpressionStatement?.type !== 'ExpressionStatement') {
return;
}
const variable = getVariable(assignNode, context);
/* c8 ignore next */
if (!variable) {
return;
}
const information = {
context,
variable,
variableNode,
valueNode,
statement,
nextExpressionStatement,
assignType: isAssignment ? 'assignment' : 'declaration',
getFix,
};
const problematicNode = getProblematicNode(information);
if (!problematicNode) {
return;
}
return getProblem(problematicNode, information);
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
for (const caseSettings of cases) {
context.on(
[
'VariableDeclarator',
'AssignmentExpression',
],
assignNode => getCaseProblem(context, assignNode, caseSettings),
);
}
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow immediate mutation after variable assignment.',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
@@ -0,0 +1,211 @@
import {
checkVueTemplate,
getParenthesizedRange,
getTokenStore,
} from './utils/index.js';
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
import builtinErrors from './shared/builtin-errors.js';
import typedArray from './shared/typed-array.js';
const isInstanceofToken = token => token.value === 'instanceof' && token.type === 'Keyword';
const MESSAGE_ID = 'no-instanceof-builtins';
const MESSAGE_ID_SWITCH_TO_TYPE_OF = 'switch-to-type-of';
const messages = {
[MESSAGE_ID]: 'Avoid using `instanceof` for type checking as it can lead to unreliable results.',
[MESSAGE_ID_SWITCH_TO_TYPE_OF]: 'Switch to `typeof … === \'{{type}}\'`.',
};
const primitiveWrappers = new Set([
'String',
'Number',
'Boolean',
'BigInt',
'Symbol',
]);
const strictStrategyConstructors = [
// Error types
...builtinErrors,
// Collection types
'Map',
'Set',
'WeakMap',
'WeakRef',
'WeakSet',
// Arrays and Typed Arrays
'ArrayBuffer',
...typedArray,
// Data types
'Object',
// Regular Expressions
'RegExp',
// Async and functions
'Promise',
'Proxy',
// Other
'DataView',
'Date',
'SharedArrayBuffer',
'FinalizationRegistry',
];
const replaceWithFunctionCall = (node, context, functionName) => function * (fixer) {
const {left, right} = node;
const tokenStore = getTokenStore(context, node);
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
yield fixSpaceAroundKeyword(fixer, node, context);
const range = getParenthesizedRange(left, {sourceCode: tokenStore});
yield fixer.insertTextBeforeRange(range, functionName + '(');
yield fixer.insertTextAfterRange(range, ')');
yield replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, context, tokenStore);
yield replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
};
const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
const {left, right} = node;
const tokenStore = getTokenStore(context, node);
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
const {sourceCode} = context;
// Check if the node is in a Vue template expression
const vueExpressionContainer = sourceCode.getAncestors(node).findLast(ancestor => ancestor.type === 'VExpressionContainer');
// Get safe quote
const safeQuote = vueExpressionContainer ? (sourceCode.getText(vueExpressionContainer)[0] === '"' ? '\'' : '"') : '\'';
yield fixSpaceAroundKeyword(fixer, node, context);
const leftRange = getParenthesizedRange(left, {sourceCode: tokenStore});
yield fixer.insertTextBeforeRange(leftRange, 'typeof ');
yield fixer.replaceText(instanceofToken, '===');
const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});
yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {
useErrorIsError = false,
strategy = 'loose',
include = [],
exclude = [],
} = context.options[0] ?? {};
const forbiddenConstructors = new Set(strategy === 'strict'
? [...strictStrategyConstructors, ...include]
: include);
context.on('BinaryExpression', /** @param {import('estree').BinaryExpression} node */ node => {
const {right, operator} = node;
if ((operator !== 'instanceof') || (right.type !== 'Identifier') || exclude.includes(right.name)) {
return;
}
const constructorName = right.name;
/** @type {import('eslint').Rule.ReportDescriptor} */
const problem = {
node,
messageId: MESSAGE_ID,
};
if (
constructorName === 'Array'
|| (constructorName === 'Error' && useErrorIsError)
) {
const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
problem.fix = replaceWithFunctionCall(node, context, functionName);
return problem;
}
if (constructorName === 'Function') {
problem.fix = replaceWithTypeOfExpression(node, context);
return problem;
}
if (primitiveWrappers.has(constructorName)) {
problem.suggest = [
{
messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
data: {type: constructorName.toLowerCase()},
fix: replaceWithTypeOfExpression(node, context),
},
];
return problem;
}
if (!forbiddenConstructors.has(constructorName)) {
return;
}
return problem;
});
};
const schema = [
{
type: 'object',
properties: {
useErrorIsError: {
type: 'boolean',
},
strategy: {
enum: [
'loose',
'strict',
],
},
include: {
type: 'array',
items: {
type: 'string',
},
},
exclude: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create: checkVueTemplate(create),
meta: {
type: 'problem',
docs: {
description: 'Disallow `instanceof` with built-in objects',
recommended: 'unopinionated',
},
fixable: 'code',
schema,
defaultOptions: [{
useErrorIsError: false,
strategy: 'loose',
include: [],
exclude: [],
}],
hasSuggestions: true,
messages,
},
};
export default config;
@@ -0,0 +1,112 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {
isCallExpression,
isNewExpression,
isUndefined,
isNullLiteral,
} from './ast/index.js';
const MESSAGE_ID_ERROR = 'no-invalid-fetch-options';
const messages = {
[MESSAGE_ID_ERROR]: '"body" is not allowed when method is "{{method}}".',
};
const isObjectPropertyWithName = (node, name) =>
node.type === 'Property'
&& !node.computed
&& node.key.type === 'Identifier'
&& node.key.name === name;
function checkFetchOptions(context, node) {
if (node.type !== 'ObjectExpression') {
return;
}
const {properties} = node;
const bodyProperty = properties.findLast(property => isObjectPropertyWithName(property, 'body'));
if (!bodyProperty) {
return;
}
const bodyValue = bodyProperty.value;
if (isUndefined(bodyValue) || isNullLiteral(bodyValue)) {
return;
}
const methodProperty = properties.findLast(property => isObjectPropertyWithName(property, 'method'));
// If `method` is omitted but there is a `SpreadElement`, we just ignore the case
if (!methodProperty) {
if (properties.some(node => node.type === 'SpreadElement')) {
return;
}
return {
node: bodyProperty.key,
messageId: MESSAGE_ID_ERROR,
data: {method: 'GET'},
};
}
const methodValue = methodProperty.value;
const scope = context.sourceCode.getScope(methodValue);
let method = getStaticValue(methodValue, scope)?.value;
if (typeof method !== 'string') {
return;
}
method = method.toUpperCase();
if (method !== 'GET' && method !== 'HEAD') {
return;
}
return {
node: bodyProperty.key,
messageId: MESSAGE_ID_ERROR,
data: {method},
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', callExpression => {
if (!isCallExpression(callExpression, {
name: 'fetch',
minimumArguments: 2,
optional: false,
})) {
return;
}
return checkFetchOptions(context, callExpression.arguments[1]);
});
context.on('NewExpression', newExpression => {
if (!isNewExpression(newExpression, {
name: 'Request',
minimumArguments: 2,
})) {
return;
}
return checkFetchOptions(context, newExpression.arguments[1]);
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow invalid options in `fetch()` and `new Request()`.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
@@ -0,0 +1,61 @@
import {getFunctionHeadLocation} from '@eslint-community/eslint-utils';
import {isMethodCall} from './ast/index.js';
const MESSAGE_ID = 'no-invalid-remove-event-listener';
const messages = {
[MESSAGE_ID]: 'The listener argument should be a function reference.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', callExpression => {
if (!(
isMethodCall(callExpression, {
method: 'removeEventListener',
minimumArguments: 2,
optionalCall: false,
})
&& callExpression.arguments[0].type !== 'SpreadElement'
&& (
callExpression.arguments[1].type === 'FunctionExpression'
|| callExpression.arguments[1].type === 'ArrowFunctionExpression'
|| isMethodCall(callExpression.arguments[1], {
method: 'bind',
optionalCall: false,
optionalMember: false,
})
)
)) {
return;
}
const [, listener] = callExpression.arguments;
if (['ArrowFunctionExpression', 'FunctionExpression'].includes(listener.type)) {
return {
node: listener,
loc: getFunctionHeadLocation(listener, context.sourceCode),
messageId: MESSAGE_ID,
};
}
return {
node: listener.callee.property,
messageId: MESSAGE_ID,
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Prevent calling `EventTarget#removeEventListener()` with the result of an expression.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
+203
View File
@@ -0,0 +1,203 @@
import isShorthandPropertyAssignmentPatternLeft from './utils/is-shorthand-property-assignment-pattern-left.js';
const MESSAGE_ID = 'noKeywordPrefix';
const messages = {
[MESSAGE_ID]: 'Do not prefix identifiers with keyword `{{keyword}}`.',
};
const prepareOptions = ({
disallowedPrefixes,
checkProperties = true,
onlyCamelCase = true,
} = {}) => ({
disallowedPrefixes: (disallowedPrefixes || [
'new',
'class',
]),
checkProperties,
onlyCamelCase,
});
function findKeywordPrefix(name, options) {
return options.disallowedPrefixes.find(keyword => {
const suffix = options.onlyCamelCase ? '[A-Z]' : '.';
const regex = new RegExp(`^${keyword}${suffix}`);
return name.match(regex);
});
}
function checkMemberExpression(report, node, options) {
const {name, parent} = node;
const keyword = findKeywordPrefix(name, options);
const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
if (!options.checkProperties) {
return;
}
if (parent.object.type === 'Identifier' && parent.object.name === name && Boolean(keyword)) {
report(node, keyword);
} else if (
effectiveParent.type === 'AssignmentExpression'
&& Boolean(keyword)
&& (effectiveParent.right.type !== 'MemberExpression' || effectiveParent.left.type === 'MemberExpression')
&& effectiveParent.left.property.name === name
) {
report(node, keyword);
}
}
function checkObjectPattern(report, node, options) {
const {name, parent} = node;
const keyword = findKeywordPrefix(name, options);
/* c8 ignore next 3 */
if (parent.shorthand && parent.value.left && Boolean(keyword)) {
report(node, keyword);
}
const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
if (Boolean(keyword) && parent.computed) {
report(node, keyword);
}
// Prevent checking right hand side of destructured object
if (parent.key === node && parent.value !== node) {
return true;
}
const valueIsInvalid = parent.value.name && Boolean(keyword);
// Ignore destructuring if the option is set, unless a new identifier is created
if (valueIsInvalid && !assignmentKeyEqualsValue) {
report(node, keyword);
}
return false;
}
// Core logic copied from:
// https://github.com/eslint/eslint/blob/master/lib/rules/camelcase.js
const create = context => {
const options = prepareOptions(context.options[0]);
// Contains reported nodes to avoid reporting twice on destructuring with shorthand notation
const reported = [];
const ALLOWED_PARENT_TYPES = new Set(['CallExpression', 'NewExpression']);
function report(node, keyword) {
if (!reported.includes(node)) {
reported.push(node);
context.report({
node,
messageId: MESSAGE_ID,
data: {
name: node.name,
keyword,
},
});
}
}
context.on('Identifier', node => {
const {name, parent} = node;
const keyword = findKeywordPrefix(name, options);
const effectiveParent = parent.type === 'MemberExpression' ? parent.parent : parent;
if (parent.type === 'MemberExpression') {
checkMemberExpression(report, node, options);
} else if (
parent.type === 'Property'
|| parent.type === 'AssignmentPattern'
) {
if (parent.parent.type === 'ObjectPattern') {
const finished = checkObjectPattern(report, node, options);
if (finished) {
return;
}
}
if (
!options.checkProperties
) {
return;
}
// Don't check right hand side of AssignmentExpression to prevent duplicate warnings
if (
Boolean(keyword)
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
&& !(parent.right === node)
&& !isShorthandPropertyAssignmentPatternLeft(node)
) {
report(node, keyword);
}
// Check if it's an import specifier
} else if (
[
'ImportSpecifier',
'ImportNamespaceSpecifier',
'ImportDefaultSpecifier',
].includes(parent.type)
) {
// Report only if the local imported identifier is invalid
if (Boolean(keyword) && parent.local?.name === name) {
report(node, keyword);
}
// Report anything that is invalid that isn't a CallExpression
} else if (
Boolean(keyword)
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type)
) {
report(node, keyword);
}
});
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
disallowedPrefixes: {
type: 'array',
items: [
{
type: 'string',
},
],
minItems: 0,
uniqueItems: true,
description: 'The prefixes to disallow.',
},
checkProperties: {
type: 'boolean',
description: 'Whether to check property names.',
},
onlyCamelCase: {
type: 'boolean',
description: 'Whether to only check camelCase names.',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow identifiers starting with `new` or `class`.',
recommended: false,
},
schema,
defaultOptions: [{}],
messages,
},
};
export default config;
+155
View File
@@ -0,0 +1,155 @@
import {isNotSemicolonToken} from '@eslint-community/eslint-utils';
import {isParenthesized, needsSemicolon} from './utils/index.js';
import {removeSpacesAfter} from './fix/index.js';
const MESSAGE_ID = 'no-lonely-if';
const messages = {
[MESSAGE_ID]: 'Unexpected `if` as the only statement in a `if` block without `else`.',
};
const isIfStatementWithoutAlternate = node => node.type === 'IfStatement' && !node.alternate;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
// Lower precedence than `&&`
const needParenthesis = node => (
(node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??'))
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'YieldExpression'
|| node.type === 'SequenceExpression'
);
function getIfStatementTokens(node, sourceCode) {
const tokens = {
ifToken: sourceCode.getFirstToken(node),
openingParenthesisToken: sourceCode.getFirstToken(node, 1),
};
const {consequent} = node;
tokens.closingParenthesisToken = sourceCode.getTokenBefore(consequent);
if (consequent.type === 'BlockStatement') {
tokens.openingBraceToken = sourceCode.getFirstToken(consequent);
tokens.closingBraceToken = sourceCode.getLastToken(consequent);
}
return tokens;
}
function fix(innerIfStatement, context) {
const {sourceCode} = context;
return function * (fixer) {
const outerIfStatement = (
innerIfStatement.parent.type === 'BlockStatement'
? innerIfStatement.parent
: innerIfStatement
).parent;
const outer = {
...outerIfStatement,
...getIfStatementTokens(outerIfStatement, sourceCode),
};
const inner = {
...innerIfStatement,
...getIfStatementTokens(innerIfStatement, sourceCode),
};
// Remove inner `if` token
yield fixer.remove(inner.ifToken);
yield removeSpacesAfter(inner.ifToken, context, fixer);
// Remove outer `{}`
if (outer.openingBraceToken) {
yield fixer.remove(outer.openingBraceToken);
yield removeSpacesAfter(outer.openingBraceToken, context, fixer);
yield fixer.remove(outer.closingBraceToken);
const tokenBefore = sourceCode.getTokenBefore(outer.closingBraceToken, {includeComments: true});
yield removeSpacesAfter(tokenBefore, context, fixer);
}
// Add new `()`
yield fixer.insertTextBefore(outer.openingParenthesisToken, '(');
yield fixer.insertTextAfter(
inner.closingParenthesisToken,
`)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}`,
);
// Add ` && `
yield fixer.insertTextAfter(outer.closingParenthesisToken, ' && ');
// Remove `()` if `test` doesn't need it
for (const {test, openingParenthesisToken, closingParenthesisToken} of [outer, inner]) {
if (
isParenthesized(test, context)
|| !needParenthesis(test)
) {
yield fixer.remove(openingParenthesisToken);
yield fixer.remove(closingParenthesisToken);
}
yield removeSpacesAfter(closingParenthesisToken, context, fixer);
}
// If the `if` statement has no block, and is not followed by a semicolon,
// make sure that fixing the issue would not change semantics due to ASI.
// Similar logic https://github.com/eslint/eslint/blob/2124e1b5dad30a905dc26bde9da472bf622d3f50/lib/rules/no-lonely-if.js#L61-L77
if (inner.consequent.type !== 'BlockStatement') {
const lastToken = sourceCode.getLastToken(inner.consequent);
if (isNotSemicolonToken(lastToken)) {
const nextToken = sourceCode.getTokenAfter(outer);
if (nextToken && needsSemicolon(lastToken, context, nextToken.value)) {
yield fixer.insertTextBefore(nextToken, ';');
}
}
}
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('IfStatement', ifStatement => {
if (!(
isIfStatementWithoutAlternate(ifStatement)
&& (
// `if (a) { if (b) {} }`
(
ifStatement.parent.type === 'BlockStatement'
&& ifStatement.parent.body.length === 1
&& ifStatement.parent.body[0] === ifStatement
&& isIfStatementWithoutAlternate(ifStatement.parent.parent)
&& ifStatement.parent.parent.consequent === ifStatement.parent
)
// `if (a) if (b) {}`
|| (
isIfStatementWithoutAlternate(ifStatement.parent)
&& ifStatement.parent.consequent === ifStatement
)
)
)) {
return;
}
return {
node: ifStatement,
messageId: MESSAGE_ID,
fix: fix(ifStatement, context),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,55 @@
import {isMethodCall, isNumericLiteral} from './ast/index.js';
import {getCallExpressionTokens} from './utils/index.js';
const MESSAGE_ID = 'no-magic-array-flat-depth';
const messages = {
[MESSAGE_ID]: 'Magic number as depth is not allowed.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', callExpression => {
if (!isMethodCall(callExpression, {
method: 'flat',
argumentsLength: 1,
optionalCall: false,
})) {
return;
}
const [depth] = callExpression.arguments;
if (!isNumericLiteral(depth) || depth.value === 1) {
return;
}
const {sourceCode} = context;
const {
openingParenthesisToken,
closingParenthesisToken,
} = getCallExpressionTokens(callExpression, context);
if (sourceCode.commentsExistBetween(openingParenthesisToken, closingParenthesisToken)) {
return;
}
return {
node: depth,
messageId: MESSAGE_ID,
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow a magic number as the `depth` argument in `Array#flat(…).`',
recommended: 'unopinionated',
},
messages,
},
};
export default config;
+102
View File
@@ -0,0 +1,102 @@
import {removeSpecifier} from './fix/index.js';
import assertToken from './utils/assert-token.js';
const MESSAGE_ID = 'no-named-default';
const messages = {
[MESSAGE_ID]: 'Prefer using the default {{type}} over named {{type}}.',
};
const isValueImport = node => !node.importKind || node.importKind === 'value';
const isValueExport = node => !node.exportKind || node.exportKind === 'value';
const fixImportSpecifier = (importSpecifier, context) => function * (fixer) {
const {sourceCode} = context;
const declaration = importSpecifier.parent;
yield removeSpecifier(importSpecifier, fixer, context, /* keepDeclaration */ true);
const nameText = sourceCode.getText(importSpecifier.local);
const hasDefaultImport = declaration.specifiers.some(({type}) => type === 'ImportDefaultSpecifier');
// Insert a new `ImportDeclaration`
if (hasDefaultImport) {
const fromToken = sourceCode.getTokenBefore(declaration.source, token => token.type === 'Identifier' && token.value === 'from');
const [startOfFromToken] = sourceCode.getRange(fromToken);
const [, endOfDeclaration] = sourceCode.getRange(declaration);
const text = `import ${nameText} ${sourceCode.text.slice(startOfFromToken, endOfDeclaration)}`;
yield fixer.insertTextBefore(declaration, `${text}\n`);
return;
}
const importToken = sourceCode.getFirstToken(declaration);
assertToken(importToken, {
expected: {type: 'Keyword', value: 'import'},
ruleId: 'no-named-default',
});
const shouldAddComma = declaration.specifiers.some(specifier => specifier !== importSpecifier && specifier.type === importSpecifier.type);
yield fixer.insertTextAfter(importToken, ` ${nameText}${shouldAddComma ? ',' : ''}`);
};
const fixExportSpecifier = (exportSpecifier, context) => function * (fixer) {
const declaration = exportSpecifier.parent;
yield removeSpecifier(exportSpecifier, fixer, context);
const text = `export default ${context.sourceCode.getText(exportSpecifier.local)};`;
yield fixer.insertTextBefore(declaration, `${text}\n`);
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('ImportSpecifier', specifier => {
if (!(
isValueImport(specifier)
&& specifier.imported.name === 'default'
&& isValueImport(specifier.parent)
)) {
return;
}
return {
node: specifier,
messageId: MESSAGE_ID,
data: {type: 'import'},
fix: fixImportSpecifier(specifier, context),
};
});
context.on('ExportSpecifier', specifier => {
if (!(
isValueExport(specifier)
&& specifier.exported.name === 'default'
&& isValueExport(specifier.parent)
&& !specifier.parent.source
)) {
return;
}
return {
node: specifier,
messageId: MESSAGE_ID,
data: {type: 'export'},
fix: fixExportSpecifier(specifier, context),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow named usage of default import and export.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,148 @@
/*
Based on ESLint builtin `no-negated-condition` rule
https://github.com/eslint/eslint/blob/5c39425fc55ecc0b97bbd07ac22654c0eb4f789c/lib/rules/no-negated-condition.js
*/
import {
removeParentheses,
fixSpaceAroundKeyword,
addParenthesizesToReturnOrThrowExpression,
} from './fix/index.js';
import {
getParenthesizedRange,
isParenthesized,
isOnSameLine,
needsSemicolon,
} from './utils/index.js';
const MESSAGE_ID = 'no-negated-condition';
const messages = {
[MESSAGE_ID]: 'Unexpected negated condition.',
};
function * convertNegatedCondition(fixer, node, context) {
const {sourceCode} = context;
const {test} = node;
if (test.type === 'UnaryExpression') {
const token = sourceCode.getFirstToken(test);
if (node.type === 'IfStatement') {
yield removeParentheses(test.argument, fixer, context);
}
yield fixer.remove(token);
return;
}
const token = sourceCode.getTokenAfter(
test.left,
token => token.type === 'Punctuator' && token.value === test.operator,
);
yield fixer.replaceText(token, '=' + token.value.slice(1));
}
function * swapConsequentAndAlternate(fixer, node, context) {
const isIfStatement = node.type === 'IfStatement';
const [consequent, alternate] = [
node.consequent,
node.alternate,
].map(node => {
const range = getParenthesizedRange(node, context);
let text = context.sourceCode.text.slice(...range);
// `if (!a) b(); else c()` can't fix to `if (!a) c() else b();`
if (isIfStatement && node.type !== 'BlockStatement') {
text = `{${text}}`;
}
return {
range,
text,
};
});
if (consequent.text === alternate.text) {
return;
}
const {sourceCode} = context;
yield fixer.replaceTextRange(sourceCode.getRange(consequent), alternate.text);
yield fixer.replaceTextRange(sourceCode.getRange(alternate), consequent.text);
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on(['IfStatement', 'ConditionalExpression'], node => {
if (
node.type === 'IfStatement'
&& (
!node.alternate
|| node.alternate.type === 'IfStatement'
)
) {
return;
}
const {test} = node;
if (!(
(test.type === 'UnaryExpression' && test.operator === '!')
|| (test.type === 'BinaryExpression' && (test.operator === '!=' || test.operator === '!=='))
)) {
return;
}
return {
node: test,
messageId: MESSAGE_ID,
/** @param {import('eslint').Rule.RuleFixer} fixer */
* fix(fixer) {
yield convertNegatedCondition(fixer, node, context);
yield swapConsequentAndAlternate(fixer, node, context);
if (
node.type !== 'ConditionalExpression'
|| test.type !== 'UnaryExpression'
) {
return;
}
yield fixSpaceAroundKeyword(fixer, node, context);
const {sourceCode} = context;
const {parent} = node;
const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2);
if (
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
&& parent.argument === node
&& !isOnSameLine(firstToken, secondToken, context)
&& !isParenthesized(node, context)
&& !isParenthesized(test, context)
) {
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
return;
}
const tokenBefore = sourceCode.getTokenBefore(node);
if (needsSemicolon(tokenBefore, context, secondToken.value)) {
yield fixer.insertTextBefore(node, ';');
}
},
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow negated conditions.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;
@@ -0,0 +1,99 @@
import {fixSpaceAroundKeyword, addParenthesizesToReturnOrThrowExpression} from './fix/index.js';
import {needsSemicolon, isParenthesized, isOnSameLine} from './utils/index.js';
const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Negated expression is not allowed in equality check.',
[MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.',
};
const EQUALITY_OPERATORS = new Set([
'===',
'!==',
'==',
'!=',
]);
const isEqualityCheck = node => node.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(node.operator);
const isNegatedExpression = node => node.type === 'UnaryExpression' && node.prefix && node.operator === '!';
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('BinaryExpression', binaryExpression => {
const {operator, left} = binaryExpression;
if (!(
isEqualityCheck(binaryExpression)
&& isNegatedExpression(left)
&& !isNegatedExpression(left.argument)
)) {
return;
}
const {sourceCode} = context;
const bangToken = sourceCode.getFirstToken(left);
const negatedOperator = `${operator.startsWith('!') ? '=' : '!'}${operator.slice(1)}`;
return {
node: bangToken,
messageId: MESSAGE_ID_ERROR,
/** @param {import('eslint').Rule.RuleFixer} fixer */
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {
operator: negatedOperator,
},
/** @param {import('eslint').Rule.RuleFixer} fixer */
* fix(fixer) {
yield fixSpaceAroundKeyword(fixer, binaryExpression, context);
const tokenAfterBang = sourceCode.getTokenAfter(bangToken);
const {parent} = binaryExpression;
if (
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
&& !isParenthesized(binaryExpression, context)
) {
const returnToken = sourceCode.getFirstToken(parent);
if (!isOnSameLine(returnToken, tokenAfterBang, context)) {
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
}
}
yield fixer.remove(bangToken);
const previousToken = sourceCode.getTokenBefore(bangToken);
if (needsSemicolon(previousToken, context, tokenAfterBang.value)) {
yield fixer.insertTextAfter(bangToken, ';');
}
const operatorToken = sourceCode.getTokenAfter(
left,
token => token.type === 'Punctuator' && token.value === operator,
);
yield fixer.replaceText(operatorToken, negatedOperator);
},
},
],
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow negated expression in equality check.',
recommended: 'unopinionated',
},
hasSuggestions: true,
messages,
},
};
export default config;
+60
View File
@@ -0,0 +1,60 @@
import {isParenthesized} from './utils/index.js';
const MESSAGE_ID_TOO_DEEP = 'too-deep';
const MESSAGE_ID_SHOULD_PARENTHESIZED = 'should-parenthesized';
const messages = {
[MESSAGE_ID_TOO_DEEP]: 'Do not nest ternary expressions.',
[MESSAGE_ID_SHOULD_PARENTHESIZED]: 'Nested ternary expression should be parenthesized.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('ConditionalExpression', node => {
if ([
node.test,
node.consequent,
node.alternate,
].some(node => node.type === 'ConditionalExpression')) {
return;
}
const {sourceCode} = context;
const ancestors = sourceCode.getAncestors(node).toReversed();
const nestLevel = ancestors.findIndex(node => node.type !== 'ConditionalExpression');
if (nestLevel === 1 && !isParenthesized(node, context)) {
return {
node,
messageId: MESSAGE_ID_SHOULD_PARENTHESIZED,
fix: fixer => [
fixer.insertTextBefore(node, '('),
fixer.insertTextAfter(node, ')'),
],
};
}
// Nesting more than one level not allowed
if (nestLevel > 1) {
return {
node: nestLevel > 2 ? ancestors[nestLevel - 3] : node,
messageId: MESSAGE_ID_TOO_DEEP,
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow nested ternary expressions.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;
+107
View File
@@ -0,0 +1,107 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {
isParenthesized,
needsSemicolon,
isNumber,
} from './utils/index.js';
import {isNewExpression} from './ast/index.js';
const MESSAGE_ID_ERROR = 'error';
const MESSAGE_ID_LENGTH = 'array-length';
const MESSAGE_ID_ONLY_ELEMENT = 'only-element';
const MESSAGE_ID_SPREAD = 'spread';
const messages = {
[MESSAGE_ID_ERROR]: '`new Array()` is unclear in intent; use either `[x]` or `Array.from({length: x})`',
[MESSAGE_ID_LENGTH]: 'The argument is the length of array.',
[MESSAGE_ID_ONLY_ELEMENT]: 'The argument is the only element of array.',
[MESSAGE_ID_SPREAD]: 'Spread the argument.',
};
function getProblem(context, node) {
if (
!isNewExpression(node, {
name: 'Array',
argumentsLength: 1,
allowSpreadElement: true,
})
) {
return;
}
const problem = {
node,
messageId: MESSAGE_ID_ERROR,
};
const [argumentNode] = node.arguments;
const {sourceCode} = context;
let text = sourceCode.getText(argumentNode);
if (isParenthesized(argumentNode, context)) {
text = `(${text})`;
}
const maybeSemiColon = needsSemicolon(sourceCode.getTokenBefore(node), context, '[')
? ';'
: '';
// We are not sure how many `arguments` passed
if (argumentNode.type === 'SpreadElement') {
problem.suggest = [
{
messageId: MESSAGE_ID_SPREAD,
fix: fixer => fixer.replaceText(node, `${maybeSemiColon}[${text}]`),
},
];
return problem;
}
const fromLengthText = `Array.from(${text === 'length' ? '{length}' : `{length: ${text}}`})`;
const scope = sourceCode.getScope(node);
if (isNumber(argumentNode, scope)) {
problem.fix = fixer => fixer.replaceText(node, fromLengthText);
return problem;
}
const onlyElementText = `${maybeSemiColon}[${text}]`;
const result = getStaticValue(argumentNode, scope);
if (result !== null && typeof result.value !== 'number') {
problem.fix = fixer => fixer.replaceText(node, onlyElementText);
return problem;
}
// We don't know the argument is number or not
problem.suggest = [
{
messageId: MESSAGE_ID_LENGTH,
fix: fixer => fixer.replaceText(node, fromLengthText),
},
{
messageId: MESSAGE_ID_ONLY_ELEMENT,
fix: fixer => fixer.replaceText(node, onlyElementText),
},
];
return problem;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('NewExpression', node => getProblem(context, node));
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow `new Array()`.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;
+98
View File
@@ -0,0 +1,98 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
import {switchNewExpressionToCallExpression} from './fix/index.js';
import isNumber from './utils/is-number.js';
import {isNewExpression} from './ast/index.js';
const ERROR = 'error';
const ERROR_UNKNOWN = 'error-unknown';
const SUGGESTION = 'suggestion';
const messages = {
[ERROR]: '`new Buffer()` is deprecated, use `Buffer.{{method}}()` instead.',
[ERROR_UNKNOWN]: '`new Buffer()` is deprecated, use `Buffer.alloc()` or `Buffer.from()` instead.',
[SUGGESTION]: 'Switch to `Buffer.{{replacement}}()`.',
};
const inferMethod = (bufferArguments, scope) => {
if (bufferArguments.length !== 1) {
return 'from';
}
const [firstArgument] = bufferArguments;
if (firstArgument.type === 'SpreadElement') {
return;
}
if (firstArgument.type === 'ArrayExpression' || firstArgument.type === 'TemplateLiteral') {
return 'from';
}
if (isNumber(firstArgument, scope)) {
return 'alloc';
}
const staticResult = getStaticValue(firstArgument, scope);
if (staticResult) {
const {value} = staticResult;
if (
typeof value === 'string'
|| Array.isArray(value)
) {
return 'from';
}
}
};
function fix(node, context, method) {
return function * (fixer) {
yield fixer.insertTextAfter(node.callee, `.${method}`);
yield switchNewExpressionToCallExpression(node, context, fixer);
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
context.on('NewExpression', node => {
if (!isNewExpression(node, {name: 'Buffer'})) {
return;
}
const method = inferMethod(node.arguments, sourceCode.getScope(node));
if (method) {
return {
node,
messageId: ERROR,
data: {method},
fix: fix(node, context, method),
};
}
return {
node,
messageId: ERROR_UNKNOWN,
suggest: ['from', 'alloc'].map(replacement => ({
messageId: SUGGESTION,
data: {replacement},
fix: fix(node, context, replacement),
})),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;

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