Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,9 @@
import globals from 'globals';
const config = {
languageOptions: {
globals: globals.builtin,
},
};
export default config;

View file

@ -0,0 +1,17 @@
import type {ESLint, Linter} from 'eslint';
declare const eslintPluginUnicorn: ESLint.Plugin & {
configs: {
recommended: Linter.FlatConfig;
unopinionated: Linter.FlatConfig;
all: Linter.FlatConfig;
/** @deprecated Use `all` instead. The `flat/` prefix is no longer needed. */
'flat/all': Linter.FlatConfig;
/** @deprecated Use `recommended` instead. The `flat/` prefix is no longer needed. */
'flat/recommended': Linter.FlatConfig;
};
};
export default eslintPluginUnicorn;

View file

@ -0,0 +1,91 @@
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 {createRules} from './rules/rule/index.js';
import packageJson from './package.json' with {type: 'json'};
const rules = createRules(rawRules);
const deprecatedRules = createDeprecatedRules({
// {ruleId: {message: string, replacedBy: string[]}}
'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: {
...createRules(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'),
};
const allConfigs = {
...unicorn,
configs,
};
export default allConfigs;

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.

View file

@ -0,0 +1,128 @@
{
"name": "eslint-plugin-unicorn",
"version": "62.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.0",
"@eslint/plugin-kit": "^0.4.0",
"change-case": "^5.4.4",
"ci-info": "^4.3.1",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.46.0",
"esquery": "^1.6.0",
"find-up-simple": "^1.0.1",
"globals": "^16.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.3",
"strip-indent": "^4.1.1"
},
"devDependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@eslint/eslintrc": "^3.3.1",
"@lubien/fixture-beta-package": "^1.0.0-beta.1",
"@typescript-eslint/parser": "^8.46.2",
"@typescript-eslint/types": "^8.46.2",
"ava": "^6.4.1",
"c8": "^10.1.3",
"enquirer": "^2.4.1",
"eslint": "^9.38.0",
"eslint-ava-rule-tester": "^5.0.1",
"eslint-config-xo": "^0.49.0",
"eslint-doc-generator": "^2.3.0",
"eslint-plugin-eslint-plugin": "^7.1.0",
"eslint-plugin-jsdoc": "^61.1.6",
"eslint-plugin-unicorn": "^61.0.2",
"eslint-remote-tester": "^4.0.3",
"eslint-remote-tester-repositories": "^2.0.2",
"espree": "^10.4.0",
"listr2": "^9.0.5",
"lodash-es": "^4.17.21",
"markdownlint-cli": "^0.45.0",
"nano-spawn": "^2.0.0",
"node-style-text": "^2.1.2",
"npm-package-json-lint": "^9.0.0",
"npm-run-all2": "^8.0.4",
"open-editor": "^5.1.0",
"outdent": "^0.8.0",
"pretty-ms": "^9.3.0",
"typescript": "^5.9.3",
"vue-eslint-parser": "^10.2.0",
"yaml": "^2.8.1"
},
"peerDependencies": {
"eslint": ">=9.38.0"
},
"ava": {
"files": [
"test/*.js",
"test/unit/*.js"
]
},
"c8": {
"reporter": [
"text",
"lcov"
]
}
}

View file

@ -0,0 +1,261 @@
# 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. | ✅ | | |
| [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. | ✅ ☑️ | | |
| [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-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-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. | ✅ | 🔧 | |
| [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)

View file

@ -0,0 +1,120 @@
/**
@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']);

View file

@ -0,0 +1,7 @@
const functionTypes = [
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
];
export default functionTypes;

View file

@ -0,0 +1,30 @@
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 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';

View file

@ -0,0 +1,3 @@
export default function isArrowFunctionBody(node) {
return node.parent.type === 'ArrowFunctionExpression' && node.parent.body === node;
}

View file

@ -0,0 +1,4 @@
const isDirective = node => node.type === 'ExpressionStatement'
&& typeof node.directive === 'string';
export default isDirective;

View file

@ -0,0 +1,5 @@
const isEmptyArrayExpression = node =>
node.type === 'ArrayExpression'
&& node.elements.length === 0;
export default isEmptyArrayExpression;

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;
}

View file

@ -0,0 +1,7 @@
export default function isExpressionStatement(node) {
return node.type === 'ExpressionStatement'
|| (
node.type === 'ChainExpression'
&& node.parent.type === 'ExpressionStatement'
);
}

View file

@ -0,0 +1,5 @@
import functionTypes from './function-types.js';
export default function isFunction(node) {
return functionTypes.includes(node.type);
}

View file

@ -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;
}

View file

@ -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,
})
);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
}

View file

@ -0,0 +1,3 @@
export default function isUndefined(node) {
return node?.type === 'Identifier' && node.name === 'undefined';
}

View file

@ -0,0 +1,24 @@
export function isLiteral(node, value) {
if (node?.type !== 'Literal') {
return false;
}
if (value === null) {
return node.raw === 'null';
}
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);
// eslint-disable-next-line unicorn/no-null
export const isNullLiteral = node => isLiteral(node, null);
export const isBigIntLiteral = node => node.type === 'Literal' && Boolean(node.bigint);
export const isEmptyStringLiteral = node => isLiteral(node, '');

View file

@ -0,0 +1,146 @@
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}}',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sortCharacterClasses} = context.options[0] || {};
const ignoreList = [];
if (sortCharacterClasses === false) {
ignoreList.push('charClassClassrangesMerge');
}
return {
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();
} 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),
});
},
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',
},
},
},
];
/** @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;

View file

@ -0,0 +1,138 @@
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 = {
name: 'error',
ignore: [],
...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));
return {
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',
},
ignore: {
type: 'array',
uniqueItems: true,
},
},
},
];
/** @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: [{}],
messages,
},
};
export default config;

View file

@ -0,0 +1,98 @@
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.name === 'default'
)
// `import {strict as assert} from 'node:assert';`
|| (
moduleName === 'assert'
&& specifier.type === 'ImportSpecifier'
&& specifier.imported.name === 'strict'
);
const NODE_PROTOCOL = 'node:';
/** @type {import('eslint').Rule.RuleModule['create']} */
const create = context => ({
* ImportDeclaration(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;

View file

@ -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 => ({
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;

View file

@ -0,0 +1,168 @@
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;
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
const declarations = new Map();
return {
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;
}
declarations.set(sourceCode.getText(node.init), {
scope: sourceCode.getScope(node),
variables: sourceCode.getDeclaredVariables(node),
objectPattern: node.id,
});
},
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 {scope, objectPattern} = declaration;
const memberScope = sourceCode.getScope(node);
// Property is destructured outside the current scope
if (!isChildInParentScope(memberScope, scope)) {
return;
}
const destructurings = objectPattern.properties.filter(property =>
property.type === 'Property'
&& property.key.type === 'Identifier'
&& property.value.type === 'Identifier',
);
const lastProperty = objectPattern.properties.at(-1);
const hasRest = lastProperty && lastProperty.type === 'RestElement';
const expression = sourceCode.getText(node);
const member = sourceCode.getText(node.property);
// Member might already be destructured
const destructuredMember = destructurings.find(property =>
property.key.name === member,
);
if (!destructuredMember) {
// 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;

View file

@ -0,0 +1,127 @@
import {getStaticValue} from '@eslint-community/eslint-utils';
const MESSAGE_ID = 'consistent-empty-array-spread';
const messages = {
[MESSAGE_ID]: 'Prefer using empty {{replacementDescription}} since the {{anotherNodePosition}} is {{anotherNodeDescription}}.',
};
const isEmptyArrayExpression = node =>
node.type === 'ArrayExpression'
&& node.elements.length === 0;
const isEmptyStringLiteral = node =>
node.type === 'Literal'
&& node.value === '';
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 => ({
* ArrayExpression(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;

View file

@ -0,0 +1,137 @@
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 => ({
/** @param {import('estree').VariableDeclarator} variableDeclarator */
* VariableDeclarator(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;

View file

@ -0,0 +1,265 @@
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
import {getReferences, 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 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 isArrowFunctionWithThis = scope =>
scope.type === 'function'
&& scope.block?.type === 'ArrowFunctionExpression'
&& (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
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) {
const scope = scopeManager.acquire(node);
if (!scope || isArrowFunctionWithThis(scope)) {
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} = {checkArrowFunctions: true, ...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)) {
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',
},
},
},
];
/** @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;

View file

@ -0,0 +1,218 @@
import {upperFirst} 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 getConstructorMethod = className => `
constructor() {
super();
this.name = '${className}';
}
`;
const hasValidSuperClass = node => {
if (!node.superClass) {
return false;
}
let {name, type, property} = node.superClass;
if (type === 'MemberExpression') {
({name} = property);
}
return nameRegexp.test(name);
};
const isSuperExpression = node =>
node.type === 'ExpressionStatement'
&& node.expression.type === 'CallExpression'
&& node.expression.callee.type === 'Super';
const isAssignmentExpression = (node, name) => {
if (
node.type !== 'ExpressionStatement'
|| node.expression.type !== 'AssignmentExpression'
) {
return false;
}
const lhs = node.expression.left;
if (!lhs.object || lhs.object.type !== 'ThisExpression') {
return false;
}
return lhs.property.name === name;
};
const isPropertyDefinition = (node, name) =>
node.type === 'PropertyDefinition'
&& !node.computed
&& node.key.type === 'Identifier'
&& node.key.name === 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 {sourceCode} = context;
const {body} = node.body;
const range = sourceCode.getRange(node.body);
const constructor = body.find(x => x.kind === 'constructor');
if (!constructor) {
yield {
node,
message: 'Add a constructor to your error.',
fix: fixer => fixer.insertTextAfterRange([
range[0],
range[0] + 1,
], getConstructorMethod(name)),
};
return;
}
const constructorBodyNode = constructor.value.body;
// Verify the constructor has a body (TypeScript)
if (!constructorBodyNode) {
return;
}
const constructorBody = constructorBodyNode.body;
const superExpression = constructorBody.find(body => isSuperExpression(body));
const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, '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);
yield fixer.insertTextAfterRange([start, start + 6], rhs.raw || rhs.name);
}
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(x => isAssignmentExpression(x, 'name'));
if (!nameExpression) {
const nameProperty = body.find(node => isPropertyDefinition(node, 'name'));
if (!nameProperty?.value || nameProperty.value.value !== name) {
yield {
node: nameProperty?.value ?? constructorBodyNode,
message: `The \`name\` property should be set to \`${name}\`.`,
};
}
} else if (nameExpression.expression.right.value !== name) {
yield {
node: nameExpression?.expression.right ?? constructorBodyNode,
message: `The \`name\` property should be set to \`${name}\`.`,
};
}
}
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;

View file

@ -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;

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;

View file

@ -0,0 +1,86 @@
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'],
},
];
/** @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;

View file

@ -0,0 +1,584 @@
import path from 'node:path';
import {isRegExp} from 'node:util/types';
import semver from 'semver';
import * as ci from 'ci-info';
import getBuiltinRule from './utils/get-builtin-rule.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 semverComparisonForOperator(operator) {
return {
'>': semver.gt,
'>=': semver.gte,
}[operator];
}
const DEFAULT_OPTIONS = {
terms: ['todo', 'fixme', 'xxx'],
ignore: [],
ignoreDatesOnPullRequests: true,
allowWarningComments: true,
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const options = {
...DEFAULT_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(token => token.type !== 'Shebang')
// 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.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);
const decidedPackageVersion = tryToCoerceVersion(version);
const compare = semverComparisonForOperator(condition);
if (packageVersion && compare(packageVersion, decidedPackageVersion)) {
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 todoVersion = tryToCoerceVersion(dependency.version);
const targetPackageVersion = tryToCoerceVersion(targetPackageRawVersion);
/* c8 ignore start */
if (!hasTargetPackage || !targetPackageVersion) {
// Can't compare `¯\_(ツ)_/¯`
continue;
}
/* c8 ignore end */
const compare = semverComparisonForOperator(dependency.condition);
if (compare(targetPackageVersion, todoVersion)) {
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 todoEngine = tryToCoerceVersion(engine.version);
const targetPackageEngineVersion = tryToCoerceVersion(
targetPackageRawEngineVersion,
);
const compare = semverComparisonForOperator(engine.condition);
if (compare(targetPackageEngineVersion, todoEngine)) {
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;
}
return {
Program() {
rules.Program(); // eslint-disable-line new-cap
},
};
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
terms: {
type: 'array',
items: {
type: 'string',
},
},
ignore: {
type: 'array',
uniqueItems: true,
},
ignoreDatesOnPullRequests: {
type: 'boolean',
},
allowWarningComments: {
type: 'boolean',
},
date: {
type: 'string',
format: '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;

View file

@ -0,0 +1,236 @@
import {isParenthesized, getStaticValue} from '@eslint-community/eslint-utils';
import {checkVueTemplate} from './utils/rule.js';
import isLogicalExpression from './utils/is-logical-expression.js';
import {isBooleanNode, getBooleanAncestor} from './utils/boolean.js';
import {fixSpaceAroundKeyword} from './fix/index.js';
import {isLiteral, isMemberExpression, isNumericLiteral} 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 isNodeValueNumber(node, context) {
if (isNumericLiteral(node)) {
return true;
}
const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
return staticValue && typeof staticValue.value === 'number';
}
function create(context) {
const options = {
'non-zero': 'greater-than',
...context.options[0],
};
const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
const {sourceCode} = context;
function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) {
const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
if (test(node)) {
return;
}
let fixed = `${sourceCode.getText(lengthNode)} ${code}`;
if (
!isParenthesized(node, sourceCode)
&& 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 {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
fix,
},
];
}
return problem;
}
return {
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 (isBooleanNode(ancestor)) {
isZeroLengthCheck = isNegative;
node = ancestor;
} else if (
isLogicalExpression(lengthNode.parent)
&& !(
lengthNode.parent.operator === '||'
&& isNodeValueNumber(lengthNode.parent.right, context)
)
) {
isZeroLengthCheck = isNegative;
node = lengthNode;
autoFix = false;
}
}
if (node) {
return getProblem({
node,
isZeroLengthCheck,
lengthNode,
autoFix,
});
}
},
};
}
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,
messages,
hasSuggestions: true,
},
};
export default config;

View file

@ -0,0 +1,276 @@
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;
}
return {
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',
],
},
ignore: {
type: 'array',
uniqueItems: true,
},
multipleFileExtensions: {
type: 'boolean',
},
},
additionalProperties: false,
},
{
properties: {
cases: {
properties: {
camelCase: {
type: 'boolean',
},
snakeCase: {
type: 'boolean',
},
kebabCase: {
type: 'boolean',
},
pascalCase: {
type: 'boolean',
},
},
additionalProperties: false,
},
ignore: {
type: 'array',
uniqueItems: true,
},
multipleFileExtensions: {
type: 'boolean',
},
},
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;

View file

@ -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, ')');
}

View file

@ -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 accident 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);
}

View file

@ -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, '');
}

View file

@ -0,0 +1,43 @@
import {getParenthesizedRange} from '../utils/parentheses.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, ' ');
}
}

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';

View file

@ -0,0 +1,40 @@
import {isCommaToken} from '@eslint-community/eslint-utils';
import {getParentheses} from '../utils/parentheses.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]);
}

View file

@ -0,0 +1,34 @@
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;

View file

@ -0,0 +1,28 @@
import {getParenthesizedRange} from '../utils/parentheses.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]);
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,19 @@
import {getParentheses} from '../utils/parentheses.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);
}
}

View file

@ -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]);
}

View file

@ -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
}
}

View file

@ -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;

View file

@ -0,0 +1,17 @@
import {getParenthesizedRange} from '../utils/parentheses.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);
}

View file

@ -0,0 +1,25 @@
import {getParenthesizedRange} from '../utils/parentheses.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, '');

View file

@ -0,0 +1,31 @@
import {getParentheses} from '../utils/parentheses.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}`);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,29 @@
import {isParenthesized} from '../utils/parentheses.js';
import shouldAddParenthesesToNewExpressionCallee from '../utils/should-add-parentheses-to-new-expression-callee.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.sourceCode)
&& shouldAddParenthesesToNewExpressionCallee(callee)
) {
yield fixer.insertTextBefore(callee, '(');
yield fixer.insertTextAfter(callee, ')');
}
}

View file

@ -0,0 +1,42 @@
import isNewExpressionWithParentheses from '../utils/is-new-expression-with-parentheses.js';
import {isParenthesized} from '../utils/parentheses.js';
import isOnSameLine from '../utils/is-on-same-line.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.sourceCode)) {
// 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);
}
}

View file

@ -0,0 +1,375 @@
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 => {
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',
},
checkDynamicImport: {
type: 'boolean',
},
checkExportFrom: {
type: 'boolean',
},
checkRequire: {
type: 'boolean',
},
extendDefaultStyles: {
type: 'boolean',
},
styles: {
$ref: '#/definitions/moduleStyles',
},
},
},
],
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;

View file

@ -0,0 +1,143 @@
// 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 '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 '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-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-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 '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';

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;

View file

@ -0,0 +1,63 @@
import {ConfigCommentParser} from '@eslint/plugin-kit';
const MESSAGE_ID = 'no-abusive-eslint-disable';
const messages = {
[MESSAGE_ID]: 'Specify the rules you want to disable.',
};
// https://github.com/eslint/eslint/blob/ecd0ede7fd2ccbb4c0daf0e4732e97ea0f49db1b/lib/linter/linter.js#L509-L512
const eslintDisableDirectives = new Set([
'eslint-disable',
'eslint-disable-line',
'eslint-disable-next-line',
]);
let commentParser;
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
* Program(node) {
for (const comment of node.comments) {
commentParser ??= new ConfigCommentParser();
const result = commentParser.parseDirective(comment.value);
if (!(
// It's a eslint-disable comment
eslintDisableDirectives.has(result?.label)
// But it did not specify any rules
&& !result?.value
)) {
return;
}
const {sourceCode} = context;
yield {
// Can't set it at the given location as the warning
// will be ignored due to the disable comment
loc: {
start: {
...sourceCode.getLoc(comment).start,
column: -1,
},
end: sourceCode.getLoc(comment).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;

View file

@ -0,0 +1,160 @@
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;
return {
/** @param {import('estree').ThisExpression} 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;

View file

@ -0,0 +1,210 @@
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 from './utils/get-class-head-location.js';
import {getParenthesizedRange} from './utils/parentheses.js';
import {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;

View file

@ -0,0 +1,285 @@
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.sourceCode)
&& 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;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
* CallExpression(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 include `function.bind()`
|| callback.type === 'CallExpression'
|| options.shouldIgnoreCallback(callback)
|| isNodeValueNotFunction(callback)
) {
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;

View file

@ -0,0 +1,496 @@
import {
isParenthesized,
isCommaToken,
isSemicolonToken,
isClosingParenToken,
findVariable,
hasSideEffect,
} from '@eslint-community/eslint-utils';
import {
extendFixRange,
fixSpaceAroundKeyword,
removeParentheses,
} from './fix/index.js';
import needsSemicolon from './utils/needs-semicolon.js';
import shouldAddParenthesesToExpressionStatementExpression from './utils/should-add-parentheses-to-expression-statement-expression.js';
import shouldAddParenthesesToMemberExpressionObject from './utils/should-add-parentheses-to-member-expression-object.js';
import {getParentheses, getParenthesizedRange} from './utils/parentheses.js';
import isFunctionSelfUsedInside from './utils/is-function-self-used-inside.js';
import {isNodeMatches} from './utils/is-node-matches.js';
import assertToken from './utils/assert-token.js';
import {
isArrowFunctionBody,
isMethodCall,
isReferenceIdentifier,
functionTypes,
} from './ast/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, sourceCode)
|| (
// `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, sourceCode)
&& 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;

View file

@ -0,0 +1,223 @@
import {hasSideEffect} from '@eslint-community/eslint-utils';
import {removeArgument} from './fix/index.js';
import {getParentheses, getParenthesizedText} from './utils/parentheses.js';
import shouldAddParenthesesToMemberExpressionObject from './utils/should-add-parentheses-to-member-expression-object.js';
import {isNodeMatches} from './utils/is-node-matches.js';
import {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 a argument, no need 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;

View file

@ -0,0 +1,128 @@
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',
},
},
},
];
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {allowSimpleOperations} = {allowSimpleOperations: true, ...context.options[0]};
return {
* CallExpression(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;

View file

@ -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;

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;

View file

@ -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;

View file

@ -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 => ({
* CallExpression(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;

View file

@ -0,0 +1,89 @@
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),
};
};
return {
* CallExpression(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;

View file

@ -0,0 +1,27 @@
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;

View file

@ -0,0 +1,58 @@
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;
}
return {
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;

View file

@ -0,0 +1,419 @@
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 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;
return {
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 shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope)
&& !elementNode?.id.typeAnnotation;
if (shouldFix) {
problem.fix = function * (fixer) {
const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
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;

View file

@ -0,0 +1,54 @@
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 => ({
Literal(node) {
if (isStringLiteral(node) || isRegexLiteral(node)) {
return checkEscape(context, node, node.raw);
}
},
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;

View file

@ -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;

View file

@ -0,0 +1,216 @@
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,
);
return {
/** @param {import('estree').BinaryExpression} node */
BinaryExpression(node) {
const {right, operator} = node;
if (right.type !== 'Identifier' || operator !== 'instanceof' || 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;

View file

@ -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 an `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;

View file

@ -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 => ({
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;

View file

@ -0,0 +1,202 @@
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,
},
});
}
}
return {
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,
},
checkProperties: {
type: 'boolean',
},
onlyCamelCase: {
type: 'boolean',
},
},
},
];
/** @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;

View file

@ -0,0 +1,155 @@
import {isParenthesized, isNotSemicolonToken} from '@eslint-community/eslint-utils';
import {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` don't need it
for (const {test, openingParenthesisToken, closingParenthesisToken} of [outer, inner]) {
if (
isParenthesized(test, sourceCode)
|| !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 => ({
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;

View file

@ -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 => ({
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;

View file

@ -0,0 +1,101 @@
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 => ({
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),
};
},
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;

View file

@ -0,0 +1,145 @@
/*
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} from './utils/parentheses.js';
import isOnSameLine from './utils/is-on-same-line.js';
import needsSemicolon from './utils/needs-semicolon.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) {
const {sourceCode} = context;
yield convertNegatedCondition(fixer, node, context);
yield swapConsequentAndAlternate(fixer, node, context);
if (
node.type !== 'ConditionalExpression'
|| test.type !== 'UnaryExpression'
) {
return;
}
yield fixSpaceAroundKeyword(fixer, node, 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, sourceCode)
&& !isParenthesized(test, sourceCode)
) {
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;

View file

@ -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 => ({
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, sourceCode)
) {
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;

View file

@ -0,0 +1,60 @@
import {isParenthesized} from '@eslint-community/eslint-utils';
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 => ({
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, sourceCode)) {
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;

View file

@ -0,0 +1,106 @@
import {isParenthesized, getStaticValue} from '@eslint-community/eslint-utils';
import needsSemicolon from './utils/needs-semicolon.js';
import isNumber from './utils/is-number.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, sourceCode)) {
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 => ({
NewExpression(node) {
return 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;

View file

@ -0,0 +1,100 @@
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;
return {
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;

View file

@ -0,0 +1,153 @@
import {isMethodCall, isCallExpression, isLiteral} from './ast/index.js';
const ERROR_MESSAGE_ID = 'error';
const SUGGESTION_REPLACE_MESSAGE_ID = 'replace';
const SUGGESTION_REMOVE_MESSAGE_ID = 'remove';
const messages = {
[ERROR_MESSAGE_ID]: 'Use `undefined` instead of `null`.',
[SUGGESTION_REPLACE_MESSAGE_ID]: 'Replace `null` with `undefined`.',
[SUGGESTION_REMOVE_MESSAGE_ID]: 'Remove `null`.',
};
const isLooseEqual = node => node.type === 'BinaryExpression' && ['==', '!='].includes(node.operator);
const isStrictEqual = node => node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {checkStrictEquality} = {
checkStrictEquality: false,
...context.options[0],
};
return {
Literal(node) {
if (
// eslint-disable-next-line unicorn/no-null
!isLiteral(node, null)
|| (!checkStrictEquality && isStrictEqual(node.parent))
// `Object.create(null)`, `Object.create(null, foo)`
|| (
isMethodCall(node.parent, {
object: 'Object',
method: 'create',
minimumArguments: 1,
maximumArguments: 2,
optionalCall: false,
optionalMember: false,
})
&& node.parent.arguments[0] === node
)
// `useRef(null)`
|| (
isCallExpression(node.parent, {
name: 'useRef',
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& node.parent.arguments[0] === node
)
// `React.useRef(null)`
|| (
isMethodCall(node.parent, {
object: 'React',
method: 'useRef',
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& node.parent.arguments[0] === node
)
// `foo.insertBefore(bar, null)`
|| (
isMethodCall(node.parent, {
method: 'insertBefore',
argumentsLength: 2,
optionalCall: false,
})
&& node.parent.arguments[1] === node
)
) {
return;
}
const {parent} = node;
const problem = {
node,
messageId: ERROR_MESSAGE_ID,
};
const useUndefinedFix = fixer => fixer.replaceText(node, 'undefined');
if (isLooseEqual(parent)) {
problem.fix = useUndefinedFix;
return problem;
}
const useUndefinedSuggestion = {
messageId: SUGGESTION_REPLACE_MESSAGE_ID,
fix: useUndefinedFix,
};
if (parent.type === 'ReturnStatement' && parent.argument === node) {
problem.suggest = [
{
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
fix: fixer => fixer.remove(node),
},
useUndefinedSuggestion,
];
return problem;
}
if (parent.type === 'VariableDeclarator' && parent.init === node && parent.parent.kind !== 'const') {
const {sourceCode} = context;
const [, start] = sourceCode.getRange(parent.id);
const [, end] = sourceCode.getRange(node);
problem.suggest = [
{
messageId: SUGGESTION_REMOVE_MESSAGE_ID,
fix: fixer => fixer.removeRange([start, end]),
},
useUndefinedSuggestion,
];
return problem;
}
problem.suggest = [useUndefinedSuggestion];
return problem;
},
};
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
checkStrictEquality: {
type: 'boolean',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of the `null` literal.',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
schema,
defaultOptions: [{checkStrictEquality: false}],
messages,
},
};
export default config;

View file

@ -0,0 +1,52 @@
import {isFunction} from './ast/index.js';
const MESSAGE_ID_IDENTIFIER = 'identifier';
const MESSAGE_ID_NON_IDENTIFIER = 'non-identifier';
const messages = {
[MESSAGE_ID_IDENTIFIER]: 'Do not use an object literal as default for parameter `{{parameter}}`.',
[MESSAGE_ID_NON_IDENTIFIER]: 'Do not use an object literal as default.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = () => ({
AssignmentPattern(node) {
if (!(
node.right.type === 'ObjectExpression'
&& node.right.properties.length > 0
&& isFunction(node.parent)
&& node.parent.params.includes(node)
)) {
return;
}
const {left, right} = node;
if (left.type === 'Identifier') {
return {
node: left,
messageId: MESSAGE_ID_IDENTIFIER,
data: {parameter: left.name},
};
}
return {
node: right,
messageId: MESSAGE_ID_NON_IDENTIFIER,
};
},
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of objects as default parameters.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;

View file

@ -0,0 +1,106 @@
import {isStaticRequire, isMethodCall, isLiteral} from './ast/index.js';
const MESSAGE_ID = 'no-process-exit';
const messages = {
[MESSAGE_ID]: 'Only use `process.exit()` in CLI apps. Throw an error instead.',
};
const isWorkerThreads = node =>
isLiteral(node, 'node:worker_threads')
|| isLiteral(node, 'worker_threads');
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const startsWithHashBang = context.sourceCode.lines[0].indexOf('#!') === 0;
if (startsWithHashBang) {
return {};
}
let processEventHandler;
// Only report if it's outside an worker thread context. See #328.
let requiredWorkerThreadsModule = false;
const problemNodes = [];
// `require('worker_threads')`
context.on('CallExpression', callExpression => {
if (
isStaticRequire(callExpression)
&& isWorkerThreads(callExpression.arguments[0])
) {
requiredWorkerThreadsModule = true;
}
});
// `import workerThreads from 'worker_threads'`
context.on('ImportDeclaration', importDeclaration => {
if (
importDeclaration.source.type === 'Literal'
&& isWorkerThreads(importDeclaration.source)
) {
requiredWorkerThreadsModule = true;
}
});
// Check `process.on` / `process.once` call
context.on('CallExpression', node => {
if (isMethodCall(node, {
object: 'process',
methods: ['on', 'once'],
minimumArguments: 1,
optionalCall: false,
optionalMember: false,
})) {
processEventHandler = node;
}
});
context.onExit('CallExpression', node => {
if (node === processEventHandler) {
processEventHandler = undefined;
}
});
// Check `process.exit` call
context.on('CallExpression', node => {
if (
!processEventHandler
&& isMethodCall(node, {
object: 'process',
method: 'exit',
optionalCall: false,
optionalMember: false,
})
) {
problemNodes.push(node);
}
});
context.onExit('Program', function * () {
if (requiredWorkerThreadsModule) {
return;
}
for (const node of problemNodes) {
yield {
node,
messageId: MESSAGE_ID,
};
}
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow `process.exit()`.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;

View file

@ -0,0 +1,179 @@
import {isCommaToken} from '@eslint-community/eslint-utils';
import {isMethodCall, isExpressionStatement} from './ast/index.js';
import {
getParenthesizedText,
isParenthesized,
needsSemicolon,
shouldAddParenthesesToAwaitExpressionArgument,
} from './utils/index.js';
const MESSAGE_ID_ERROR = 'no-single-promise-in-promise-methods/error';
const MESSAGE_ID_SUGGESTION_UNWRAP = 'no-single-promise-in-promise-methods/unwrap';
const MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE = 'no-single-promise-in-promise-methods/use-promise-resolve';
const messages = {
[MESSAGE_ID_ERROR]: 'Wrapping single-element array with `Promise.{{method}}()` is unnecessary.',
[MESSAGE_ID_SUGGESTION_UNWRAP]: 'Use the value directly.',
[MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE]: 'Switch to `Promise.resolve(…)`.',
};
const METHODS = ['all', 'any', 'race'];
const isPromiseMethodCallWithSingleElementArray = node =>
isMethodCall(node, {
object: 'Promise',
methods: METHODS,
optionalMember: false,
optionalCall: false,
argumentsLength: 1,
})
&& node.arguments[0].type === 'ArrayExpression'
&& node.arguments[0].elements.length === 1
&& node.arguments[0].elements[0]
&& node.arguments[0].elements[0].type !== 'SpreadElement';
const unwrapAwaitedCallExpression = (callExpression, context) => fixer => {
const {sourceCode} = context;
const [promiseNode] = callExpression.arguments[0].elements;
let text = getParenthesizedText(promiseNode, context);
if (
!isParenthesized(promiseNode, sourceCode)
&& shouldAddParenthesesToAwaitExpressionArgument(promiseNode)
) {
text = `(${text})`;
}
// The next node is already behind a `CallExpression`, there should be no ASI problem
return fixer.replaceText(callExpression, text);
};
const unwrapNonAwaitedCallExpression = (callExpression, context) => fixer => {
const {sourceCode} = context;
const [promiseNode] = callExpression.arguments[0].elements;
let text = getParenthesizedText(promiseNode, context);
if (
!isParenthesized(promiseNode, sourceCode)
// Since the original call expression can be anywhere, it's hard to tell if the promise
// need to be parenthesized, but it's safe to add parentheses
&& !(
// Known cases that not need parentheses
promiseNode.type === 'Identifier'
|| promiseNode.type === 'MemberExpression'
)
) {
text = `(${text})`;
}
const previousToken = sourceCode.getTokenBefore(callExpression);
if (needsSemicolon(previousToken, context, text)) {
text = `;${text}`;
}
return fixer.replaceText(callExpression, text);
};
const switchToPromiseResolve = (callExpression, sourceCode) => function * (fixer) {
/*
```
Promise.race([promise,])
// ^^^^ methodNameNode
```
*/
const methodNameNode = callExpression.callee.property;
yield fixer.replaceText(methodNameNode, 'resolve');
const [arrayExpression] = callExpression.arguments;
/*
```
Promise.race([promise,])
// ^ openingBracketToken
```
*/
const openingBracketToken = sourceCode.getFirstToken(arrayExpression);
/*
```
Promise.race([promise,])
// ^ penultimateToken
// ^ closingBracketToken
```
*/
const [
penultimateToken,
closingBracketToken,
] = sourceCode.getLastTokens(arrayExpression, 2);
yield fixer.remove(openingBracketToken);
yield fixer.remove(closingBracketToken);
if (isCommaToken(penultimateToken)) {
yield fixer.remove(penultimateToken);
}
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
CallExpression(callExpression) {
if (!isPromiseMethodCallWithSingleElementArray(callExpression)) {
return;
}
const methodName = callExpression.callee.property.name;
const problem = {
node: callExpression.arguments[0],
messageId: MESSAGE_ID_ERROR,
data: {
method: methodName,
},
};
const {sourceCode} = context;
if (
callExpression.parent.type === 'AwaitExpression'
&& callExpression.parent.argument === callExpression
&& (
methodName !== 'all'
|| isExpressionStatement(callExpression.parent.parent)
)
) {
problem.fix = unwrapAwaitedCallExpression(callExpression, context);
return problem;
}
if (methodName === 'all') {
return problem;
}
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION_UNWRAP,
fix: unwrapNonAwaitedCallExpression(callExpression, context),
},
{
messageId: MESSAGE_ID_SUGGESTION_SWITCH_TO_PROMISE_RESOLVE,
fix: switchToPromiseResolve(callExpression, sourceCode),
},
];
return problem;
},
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow passing single-element arrays to `Promise` methods.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;

View file

@ -0,0 +1,226 @@
import {isSemicolonToken} from '@eslint-community/eslint-utils';
import getClassHeadLocation from './utils/get-class-head-location.js';
import assertToken from './utils/assert-token.js';
import {removeSpacesAfter} from './fix/index.js';
const MESSAGE_ID = 'no-static-only-class';
const messages = {
[MESSAGE_ID]: 'Use an object instead of a class with only static members.',
};
const isEqualToken = ({type, value}) => type === 'Punctuator' && value === '=';
const isDeclarationOfExportDefaultDeclaration = node =>
node.type === 'ClassDeclaration'
&& node.parent.type === 'ExportDefaultDeclaration'
&& node.parent.declaration === node;
const isPropertyDefinition = node => node.type === 'PropertyDefinition';
const isMethodDefinition = node => node.type === 'MethodDefinition';
function isStaticMember(node) {
const {
private: isPrivate,
static: isStatic,
declare: isDeclare,
readonly: isReadonly,
accessibility,
decorators,
key,
} = node;
// Avoid matching unexpected node. For example: https://github.com/tc39/proposal-class-static-block
if (!isPropertyDefinition(node) && !isMethodDefinition(node)) {
return false;
}
if (!isStatic || isPrivate || key.type === 'PrivateIdentifier') {
return false;
}
// TypeScript class
if (
isDeclare
|| isReadonly
|| accessibility !== undefined
|| (Array.isArray(decorators) && decorators.length > 0)
) {
return false;
}
return true;
}
function * switchClassMemberToObjectProperty(node, context, fixer) {
const {sourceCode} = context;
const staticToken = sourceCode.getFirstToken(node);
assertToken(staticToken, {
expected: {type: 'Keyword', value: 'static'},
ruleId: 'no-static-only-class',
});
yield fixer.remove(staticToken);
yield removeSpacesAfter(staticToken, context, fixer);
const maybeSemicolonToken = isPropertyDefinition(node)
? sourceCode.getLastToken(node)
: sourceCode.getTokenAfter(node);
const hasSemicolonToken = isSemicolonToken(maybeSemicolonToken);
if (isPropertyDefinition(node)) {
const {key, value} = node;
if (value) {
// Computed key may have `]` after `key`
const equalToken = sourceCode.getTokenAfter(key, isEqualToken);
yield fixer.replaceText(equalToken, ':');
} else if (hasSemicolonToken) {
yield fixer.insertTextBefore(maybeSemicolonToken, ': undefined');
} else {
yield fixer.insertTextAfter(node, ': undefined');
}
}
yield (
hasSemicolonToken
? fixer.replaceText(maybeSemicolonToken, ',')
: fixer.insertTextAfter(node, ',')
);
}
function switchClassToObject(node, context) {
const {
type,
id,
body,
declare: isDeclare,
abstract: isAbstract,
implements: classImplements,
parent,
} = node;
if (
isDeclare
|| isAbstract
|| (Array.isArray(classImplements) && classImplements.length > 0)
) {
return;
}
if (type === 'ClassExpression' && id) {
return;
}
const isExportDefault = isDeclarationOfExportDefaultDeclaration(node);
if (isExportDefault && id) {
return;
}
const {sourceCode} = context;
for (const node of body.body) {
if (
isPropertyDefinition(node)
&& (
node.typeAnnotation
// This is a stupid way to check if `value` of `PropertyDefinition` uses `this`
|| (node.value && sourceCode.getText(node.value).includes('this'))
)
) {
return;
}
}
return function * (fixer) {
const classToken = sourceCode.getFirstToken(node);
/* c8 ignore next */
assertToken(classToken, {
expected: {type: 'Keyword', value: 'class'},
ruleId: 'no-static-only-class',
});
if (isExportDefault || type === 'ClassExpression') {
/*
There are comments after return, and `{` is not on same line
```js
function a() {
return class // comment
{
static a() {}
}
}
```
*/
if (
type === 'ClassExpression'
&& parent.type === 'ReturnStatement'
&& sourceCode.getLoc(body).start.line !== sourceCode.getLoc(parent).start.line
&& sourceCode.text.slice(sourceCode.getRange(classToken)[1], sourceCode.getRange(body)[0]).trim()
) {
yield fixer.replaceText(classToken, '{');
const openingBraceToken = sourceCode.getFirstToken(body);
yield fixer.remove(openingBraceToken);
} else {
yield fixer.replaceText(classToken, '');
/*
Avoid breaking case like
```js
return class
{};
```
*/
yield removeSpacesAfter(classToken, context, fixer);
}
// There should not be ASI problem
} else {
yield fixer.replaceText(classToken, 'const');
yield fixer.insertTextBefore(body, '= ');
yield fixer.insertTextAfter(body, ';');
}
for (const node of body.body) {
yield switchClassMemberToObjectProperty(node, context, fixer);
}
};
}
function create(context) {
context.on(['ClassDeclaration', 'ClassExpression'], node => {
if (
node.superClass
|| (node.decorators && node.decorators.length > 0)
|| node.body.type !== 'ClassBody'
|| node.body.body.length === 0
|| node.body.body.some(node => !isStaticMember(node))
) {
return;
}
return {
node,
loc: getClassHeadLocation(node, context),
messageId: MESSAGE_ID,
fix: switchClassToObject(node, context),
};
});
}
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow classes that only have static members.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;

View file

@ -0,0 +1,194 @@
import {getStaticValue, getPropertyName} from '@eslint-community/eslint-utils';
import {isMethodCall} from './ast/index.js';
const MESSAGE_ID_OBJECT = 'no-thenable-object';
const MESSAGE_ID_EXPORT = 'no-thenable-export';
const MESSAGE_ID_CLASS = 'no-thenable-class';
const messages = {
[MESSAGE_ID_OBJECT]: 'Do not add `then` to an object.',
[MESSAGE_ID_EXPORT]: 'Do not export `then`.',
[MESSAGE_ID_CLASS]: 'Do not add `then` to a class.',
};
const isStringThen = (node, context) =>
getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then';
const isPropertyThen = (node, context) =>
getPropertyName(node, context.sourceCode.getScope(node)) === 'then';
const cases = [
// `{then() {}}`,
// `{get then() {}}`,
// `{[computedKey]() {}}`,
// `{get [computedKey]() {}}`,
{
selector: 'ObjectExpression',
* getNodes(node, context) {
for (const property of node.properties) {
if (property.type === 'Property' && isPropertyThen(property, context)) {
yield property.key;
}
}
},
messageId: MESSAGE_ID_OBJECT,
},
// `class Foo {then}`,
// `class Foo {static then}`,
// `class Foo {get then() {}}`,
// `class Foo {static get then() {}}`,
{
selectors: ['PropertyDefinition', 'MethodDefinition'],
* getNodes(node, context) {
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') {
yield node.key;
}
},
messageId: MESSAGE_ID_CLASS,
},
// `foo.then = …`
// `foo[computedKey] = …`
{
selector: 'MemberExpression',
* getNodes(node, context) {
if (!(node.parent.type === 'AssignmentExpression' && node.parent.left === node)) {
return;
}
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') {
yield node.property;
}
},
messageId: MESSAGE_ID_OBJECT,
},
// `Object.defineProperty(foo, 'then', …)`
// `Reflect.defineProperty(foo, 'then', …)`
{
selector: 'CallExpression',
* getNodes(node, context) {
if (!(
isMethodCall(node, {
objects: ['Object', 'Reflect'],
method: 'defineProperty',
minimumArguments: 3,
optionalCall: false,
optionalMember: false,
})
&& node.arguments[0].type !== 'SpreadElement'
)) {
return;
}
const [, secondArgument] = node.arguments;
if (isStringThen(secondArgument, context)) {
yield secondArgument;
}
},
messageId: MESSAGE_ID_OBJECT,
},
// `Object.fromEntries([['then', …]])`
{
selector: 'CallExpression',
* getNodes(node, context) {
if (!(
isMethodCall(node, {
object: 'Object',
method: 'fromEntries',
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& node.arguments[0].type === 'ArrayExpression'
)) {
return;
}
for (const pairs of node.arguments[0].elements) {
if (
pairs?.type === 'ArrayExpression'
&& pairs.elements[0]
&& pairs.elements[0].type !== 'SpreadElement'
) {
const [key] = pairs.elements;
if (isStringThen(key, context)) {
yield key;
}
}
}
},
messageId: MESSAGE_ID_OBJECT,
},
// `export {then}`
{
selector: 'Identifier',
* getNodes(node) {
if (
node.name === 'then'
&& node.parent.type === 'ExportSpecifier'
&& node.parent.exported === node
) {
yield node;
}
},
messageId: MESSAGE_ID_EXPORT,
},
// `export function then() {}`,
// `export class then {}`,
{
selector: 'Identifier',
* getNodes(node) {
if (
node.name === 'then'
&& (node.parent.type === 'FunctionDeclaration' || node.parent.type === 'ClassDeclaration')
&& node.parent.id === node
&& node.parent.parent.type === 'ExportNamedDeclaration'
&& node.parent.parent.declaration === node.parent
) {
yield node;
}
},
messageId: MESSAGE_ID_EXPORT,
},
// `export const … = …`;
{
selector: 'VariableDeclaration',
* getNodes(node, context) {
if (!(node.parent.type === 'ExportNamedDeclaration' && node.parent.declaration === node)) {
return;
}
for (const variable of context.sourceCode.getDeclaredVariables(node)) {
if (variable.name === 'then') {
yield * variable.identifiers;
}
}
},
messageId: MESSAGE_ID_EXPORT,
},
];
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
for (const {selector, selectors, messageId, getNodes} of cases) {
context.on(selector ?? selectors, function * (node) {
for (const problematicNode of getNodes(node, context)) {
yield {node: problematicNode, messageId};
}
});
}
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow `then` property.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;

View file

@ -0,0 +1,40 @@
const MESSAGE_ID = 'no-this-assignment';
const messages = {
[MESSAGE_ID]: 'Do not assign `this` to `{{name}}`.',
};
function getProblem(variableNode, valueNode) {
if (
variableNode.type !== 'Identifier'
|| valueNode?.type !== 'ThisExpression'
) {
return;
}
return {
node: valueNode.parent,
data: {name: variableNode.name},
messageId: MESSAGE_ID,
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('VariableDeclarator', node => getProblem(node.id, node.init));
context.on('AssignmentExpression', node => getProblem(node.left, node.right));
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow assigning `this` to a variable.',
recommended: 'unopinionated',
},
messages,
},
};
export default config;

View file

@ -0,0 +1,146 @@
import {isLiteral} from './ast/index.js';
import {
addParenthesizesToReturnOrThrowExpression,
removeSpacesAfter,
} from './fix/index.js';
import {
needsSemicolon,
isParenthesized,
isOnSameLine,
isUnresolvedVariable,
} from './utils/index.js';
const MESSAGE_ID_ERROR = 'no-typeof-undefined/error';
const MESSAGE_ID_SUGGESTION = 'no-typeof-undefined/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Compare with `undefined` directly instead of using `typeof`.',
[MESSAGE_ID_SUGGESTION]: 'Switch to `… {{operator}} undefined`.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {
checkGlobalVariables,
} = {
checkGlobalVariables: false,
...context.options[0],
};
const {sourceCode} = context;
return {
BinaryExpression(binaryExpression) {
if (!(
(
binaryExpression.operator === '==='
|| binaryExpression.operator === '!=='
|| binaryExpression.operator === '=='
|| binaryExpression.operator === '!='
)
&& binaryExpression.left.type === 'UnaryExpression'
&& binaryExpression.left.operator === 'typeof'
&& binaryExpression.left.prefix
&& isLiteral(binaryExpression.right, 'undefined')
)) {
return;
}
const {left: typeofNode, right: undefinedString, operator} = binaryExpression;
const valueNode = typeofNode.argument;
const isGlobalVariable = valueNode.type === 'Identifier'
&& (sourceCode.isGlobalReference(valueNode) || isUnresolvedVariable(valueNode, context));
if (!checkGlobalVariables && isGlobalVariable) {
return;
}
const [typeofToken, secondToken] = sourceCode.getFirstTokens(typeofNode, 2);
const fix = function * (fixer) {
// Change `==`/`!=` to `===`/`!==`
if (operator === '==' || operator === '!=') {
const operatorToken = sourceCode.getTokenAfter(
typeofNode,
token => token.type === 'Punctuator' && token.value === operator,
);
yield fixer.insertTextAfter(operatorToken, '=');
}
yield fixer.replaceText(undefinedString, 'undefined');
yield fixer.remove(typeofToken);
yield removeSpacesAfter(typeofToken, context, fixer);
const {parent} = binaryExpression;
if (
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
&& parent.argument === binaryExpression
&& !isOnSameLine(typeofToken, secondToken, context)
&& !isParenthesized(binaryExpression, sourceCode)
&& !isParenthesized(typeofNode, sourceCode)
) {
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
return;
}
const tokenBefore = sourceCode.getTokenBefore(binaryExpression);
if (needsSemicolon(tokenBefore, context, secondToken.value)) {
yield fixer.insertTextBefore(binaryExpression, ';');
}
};
const problem = {
node: binaryExpression,
loc: sourceCode.getLoc(typeofToken),
messageId: MESSAGE_ID_ERROR,
};
if (isGlobalVariable) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {operator: operator.startsWith('!') ? '!==' : '==='},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
},
};
};
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
checkGlobalVariables: {
type: 'boolean',
},
},
},
];
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow comparing `undefined` using `typeof`.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
schema,
defaultOptions: [{checkGlobalVariables: false}],
messages,
},
};
export default config;

View file

@ -0,0 +1,49 @@
import {isMethodCall, isLiteral} from './ast/index.js';
import {removeArgument} from './fix/index.js';
const MESSAGE_ID = 'no-unnecessary-array-flat-depth';
const messages = {
[MESSAGE_ID]: 'Passing `1` as the `depth` argument is unnecessary.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', callExpression => {
if (!(
isMethodCall(callExpression, {
method: 'flat',
argumentsLength: 1,
optionalCall: false,
})
&& isLiteral(callExpression.arguments[0], 1)
)) {
return;
}
const [numberOne] = callExpression.arguments;
return {
node: numberOne,
messageId: MESSAGE_ID,
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => removeArgument(fixer, numberOne, context),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow using `1` as the `depth` argument of `Array#flat()`.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
},
};
export default config;

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