eslint-plugin-unicorn
Advanced tools
Comparing version 34.0.1 to 49.0.0
122
index.js
'use strict'; | ||
const createDeprecatedRules = require('./rules/utils/create-deprecated-rules.js'); | ||
const {loadRules} = require('./rules/utils/rule.js'); | ||
const recommendedConfig = require('./configs/recommended.js'); | ||
const allRulesEnabledConfig = require('./configs/all.js'); | ||
const {name, version} = require('./package.json'); | ||
const deprecatedRules = createDeprecatedRules({ | ||
// {ruleId: ReplacementRuleId | ReplacementRuleId[]}, if no replacement, use `{ruleId: []}` | ||
'import-index': [], | ||
'no-array-instanceof': 'unicorn/no-instanceof-array', | ||
'no-fn-reference-in-iterator': 'unicorn/no-array-callback-reference', | ||
'no-reduce': 'unicorn/no-array-reduce', | ||
'no-unsafe-regex': [], | ||
'prefer-dataset': 'unicorn/prefer-dom-node-dataset', | ||
@@ -16,2 +21,3 @@ 'prefer-event-key': 'unicorn/prefer-keyboard-event-key', | ||
'prefer-node-remove': 'unicorn/prefer-dom-node-remove', | ||
'prefer-object-has-own': 'prefer-object-has-own', | ||
'prefer-replace-all': 'unicorn/prefer-string-replace-all', | ||
@@ -21,116 +27,18 @@ 'prefer-starts-ends-with': 'unicorn/prefer-string-starts-ends-with', | ||
'prefer-trim-start-end': 'unicorn/prefer-string-trim-start-end', | ||
'regex-shorthand': 'unicorn/better-regex' | ||
'regex-shorthand': 'unicorn/better-regex', | ||
}); | ||
module.exports = { | ||
meta: { | ||
name, | ||
version, | ||
}, | ||
rules: { | ||
...loadRules(), | ||
...deprecatedRules | ||
...deprecatedRules, | ||
}, | ||
configs: { | ||
recommended: { | ||
env: { | ||
es6: true | ||
}, | ||
parserOptions: { | ||
ecmaVersion: 2021, | ||
sourceType: 'module' | ||
}, | ||
plugins: [ | ||
'unicorn' | ||
], | ||
rules: { | ||
'unicorn/better-regex': 'error', | ||
'unicorn/catch-error-name': 'error', | ||
'unicorn/consistent-destructuring': 'error', | ||
'unicorn/consistent-function-scoping': 'error', | ||
'unicorn/custom-error-definition': 'off', | ||
'unicorn/empty-brace-spaces': 'error', | ||
'unicorn/error-message': 'error', | ||
'unicorn/escape-case': 'error', | ||
'unicorn/expiring-todo-comments': 'error', | ||
'unicorn/explicit-length-check': 'error', | ||
'unicorn/filename-case': 'error', | ||
'unicorn/import-index': 'off', | ||
'unicorn/import-style': 'error', | ||
'unicorn/new-for-builtins': 'error', | ||
'unicorn/no-abusive-eslint-disable': 'error', | ||
'unicorn/no-array-callback-reference': 'error', | ||
'unicorn/no-array-for-each': 'error', | ||
'unicorn/no-array-method-this-argument': 'error', | ||
'unicorn/no-array-push-push': 'error', | ||
'unicorn/no-array-reduce': 'error', | ||
'unicorn/no-console-spaces': 'error', | ||
'unicorn/no-document-cookie': 'error', | ||
'unicorn/no-for-loop': 'error', | ||
'unicorn/no-hex-escape': 'error', | ||
'unicorn/no-instanceof-array': 'error', | ||
'unicorn/no-keyword-prefix': 'off', | ||
'unicorn/no-lonely-if': 'error', | ||
'no-nested-ternary': 'off', | ||
'unicorn/no-nested-ternary': 'error', | ||
'unicorn/no-new-array': 'error', | ||
'unicorn/no-new-buffer': 'error', | ||
'unicorn/no-null': 'error', | ||
'unicorn/no-object-as-default-parameter': 'error', | ||
'unicorn/no-process-exit': 'error', | ||
'unicorn/no-static-only-class': 'error', | ||
'unicorn/no-this-assignment': 'error', | ||
'unicorn/no-unreadable-array-destructuring': 'error', | ||
'unicorn/no-unsafe-regex': 'off', | ||
'unicorn/no-unused-properties': 'off', | ||
'unicorn/no-useless-undefined': 'error', | ||
'unicorn/no-zero-fractions': 'error', | ||
'unicorn/number-literal-case': 'error', | ||
'unicorn/numeric-separators-style': 'error', | ||
'unicorn/prefer-add-event-listener': 'error', | ||
'unicorn/prefer-array-find': 'error', | ||
'unicorn/prefer-array-flat': 'error', | ||
'unicorn/prefer-array-flat-map': 'error', | ||
'unicorn/prefer-array-index-of': 'error', | ||
'unicorn/prefer-array-some': 'error', | ||
// TODO: Enable this by default when targeting a Node.js version that supports `Array#at`. | ||
'unicorn/prefer-at': 'off', | ||
'unicorn/prefer-date-now': 'error', | ||
'unicorn/prefer-default-parameters': 'error', | ||
'unicorn/prefer-dom-node-append': 'error', | ||
'unicorn/prefer-dom-node-dataset': 'error', | ||
'unicorn/prefer-dom-node-remove': 'error', | ||
'unicorn/prefer-dom-node-text-content': 'error', | ||
'unicorn/prefer-includes': 'error', | ||
'unicorn/prefer-keyboard-event-key': 'error', | ||
'unicorn/prefer-math-trunc': 'error', | ||
'unicorn/prefer-modern-dom-apis': 'error', | ||
'unicorn/prefer-module': 'error', | ||
'unicorn/prefer-negative-index': 'error', | ||
'unicorn/prefer-node-protocol': 'error', | ||
'unicorn/prefer-number-properties': 'error', | ||
// TODO: Enable this by default when targeting a Node.js version that supports `Object.hasOwn`. | ||
'unicorn/prefer-object-has-own': 'off', | ||
'unicorn/prefer-optional-catch-binding': 'error', | ||
'unicorn/prefer-prototype-methods': 'error', | ||
'unicorn/prefer-query-selector': 'error', | ||
'unicorn/prefer-reflect-apply': 'error', | ||
'unicorn/prefer-regexp-test': 'error', | ||
'unicorn/prefer-set-has': 'error', | ||
'unicorn/prefer-spread': 'error', | ||
// TODO: Enable this by default when targeting Node.js 16. | ||
'unicorn/prefer-string-replace-all': 'off', | ||
'unicorn/prefer-string-slice': 'error', | ||
'unicorn/prefer-string-starts-ends-with': 'error', | ||
'unicorn/prefer-string-trim-start-end': 'error', | ||
'unicorn/prefer-switch': 'error', | ||
'unicorn/prefer-ternary': 'error', | ||
// TODO: Enable this by default when targeting Node.js 14. | ||
'unicorn/prefer-top-level-await': 'off', | ||
'unicorn/prefer-type-error': 'error', | ||
'unicorn/prevent-abbreviations': 'error', | ||
'unicorn/require-array-join-separator': 'error', | ||
'unicorn/require-number-to-fixed-digits-argument': 'error', | ||
'unicorn/require-post-message-target-origin': 'error', | ||
'unicorn/string-content': 'off', | ||
'unicorn/throw-new-error': 'error' | ||
} | ||
} | ||
} | ||
recommended: recommendedConfig, | ||
all: allRulesEnabledConfig, | ||
}, | ||
}; |
158
package.json
{ | ||
"name": "eslint-plugin-unicorn", | ||
"version": "34.0.1", | ||
"description": "Various awesome ESLint rules", | ||
"version": "49.0.0", | ||
"description": "More than 100 powerful ESLint rules", | ||
"license": "MIT", | ||
@@ -14,16 +14,26 @@ "repository": "sindresorhus/eslint-plugin-unicorn", | ||
"engines": { | ||
"node": ">=12" | ||
"node": ">=16" | ||
}, | ||
"scripts": { | ||
"test": "xo && nyc ava", | ||
"create-rule": "node ./scripts/create-rule.mjs && npm run generate-rules-table && npm run generate-usage-example", | ||
"create-rule": "node ./scripts/create-rule.mjs && npm run fix:eslint-docs", | ||
"fix": "run-p --continue-on-error fix:*", | ||
"fix:eslint-docs": "eslint-doc-generator", | ||
"fix:js": "npm run lint:js -- --fix", | ||
"fix:md": "npm run lint:md -- --fix", | ||
"integration": "node ./test/integration/test.mjs", | ||
"lint": "run-p --continue-on-error lint:*", | ||
"lint:eslint-docs": "npm run fix:eslint-docs -- --check", | ||
"lint:js": "xo", | ||
"lint:md": "markdownlint \"**/*.md\"", | ||
"lint:package-json": "npmPkgJsonLint .", | ||
"run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.mjs", | ||
"integration": "node ./test/integration/test.mjs", | ||
"bundle-lodash": "echo \"export {defaultsDeep, camelCase, kebabCase, snakeCase, upperFirst, lowerFirst} from 'lodash-es';\" | npx esbuild --bundle --outfile=rules/utils/lodash.js --format=cjs", | ||
"smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.js", | ||
"generate-rules-table": "node ./scripts/generate-rules-table.mjs", | ||
"generate-usage-example": "node ./scripts/generate-usage-example.mjs" | ||
"test": "npm-run-all --continue-on-error lint test:*", | ||
"test:js": "c8 ava" | ||
}, | ||
"files": [ | ||
"index.js", | ||
"rules" | ||
"rules", | ||
"configs" | ||
], | ||
@@ -41,41 +51,49 @@ "keywords": [ | ||
"dependencies": { | ||
"ci-info": "^3.2.0", | ||
"@babel/helper-validator-identifier": "^7.22.20", | ||
"@eslint-community/eslint-utils": "^4.4.0", | ||
"ci-info": "^3.8.0", | ||
"clean-regexp": "^1.0.0", | ||
"eslint-template-visitor": "^2.3.2", | ||
"eslint-utils": "^3.0.0", | ||
"is-builtin-module": "^3.1.0", | ||
"lodash": "^4.17.21", | ||
"esquery": "^1.5.0", | ||
"indent-string": "^4.0.0", | ||
"is-builtin-module": "^3.2.1", | ||
"jsesc": "^3.0.2", | ||
"pluralize": "^8.0.0", | ||
"read-pkg-up": "^7.0.1", | ||
"regexp-tree": "^0.1.23", | ||
"reserved-words": "^0.1.2", | ||
"safe-regex": "^2.1.1", | ||
"semver": "^7.3.5" | ||
"regexp-tree": "^0.1.27", | ||
"regjsparser": "^0.10.0", | ||
"semver": "^7.5.4", | ||
"strip-indent": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/code-frame": "7.12.13", | ||
"@babel/core": "7.14.6", | ||
"@babel/eslint-parser": "7.14.7", | ||
"@babel/code-frame": "^7.22.13", | ||
"@babel/core": "^7.23.2", | ||
"@babel/eslint-parser": "^7.22.15", | ||
"@lubien/fixture-beta-package": "^1.0.0-beta.1", | ||
"@typescript-eslint/parser": "^4.26.1", | ||
"@typescript-eslint/parser": "^6.9.0", | ||
"ava": "^3.15.0", | ||
"chalk": "^4.1.1", | ||
"enquirer": "2.3.6", | ||
"eslint": "^7.28.0", | ||
"c8": "^8.0.1", | ||
"chalk": "^5.3.0", | ||
"enquirer": "^2.4.1", | ||
"eslint": "^8.52.0", | ||
"eslint-ava-rule-tester": "^4.0.0", | ||
"eslint-plugin-eslint-plugin": "^3.1.0", | ||
"eslint-remote-tester": "^1.2.0", | ||
"execa": "^5.1.1", | ||
"eslint-doc-generator": "^1.5.2", | ||
"eslint-plugin-eslint-plugin": "^5.1.1", | ||
"eslint-plugin-internal-rules": "file:./scripts/internal-rules/", | ||
"eslint-remote-tester": "^3.0.1", | ||
"eslint-remote-tester-repositories": "^1.0.1", | ||
"execa": "^8.0.1", | ||
"listr": "^0.14.3", | ||
"lodash-es": "4.17.21", | ||
"mem": "8.1.1", | ||
"nyc": "^15.1.0", | ||
"lodash-es": "^4.17.21", | ||
"markdownlint-cli": "^0.37.0", | ||
"mem": "^9.0.2", | ||
"npm-package-json-lint": "^7.0.0", | ||
"npm-run-all2": "^6.1.1", | ||
"outdent": "^0.8.0", | ||
"pify": "^5.0.0", | ||
"typescript": "^4.3.2", | ||
"vue-eslint-parser": "^7.6.0", | ||
"xo": "^0.40.2" | ||
"typescript": "^5.2.2", | ||
"vue-eslint-parser": "^9.3.2", | ||
"xo": "^0.56.0", | ||
"yaml": "^2.3.3" | ||
}, | ||
"peerDependencies": { | ||
"eslint": ">=7.28.0" | ||
"eslint": ">=8.52.0" | ||
}, | ||
@@ -88,3 +106,3 @@ "ava": { | ||
}, | ||
"nyc": { | ||
"c8": { | ||
"reporter": [ | ||
@@ -96,34 +114,27 @@ "text", | ||
"xo": { | ||
"plugins": [ | ||
"eslint-plugin" | ||
], | ||
"extends": [ | ||
"plugin:eslint-plugin/all" | ||
"plugin:internal-rules/all" | ||
], | ||
"ignores": [ | ||
"test/integration/{fixtures,fixtures-local}/**", | ||
".cache-eslint-remote-tester", | ||
"eslint-remote-tester-results" | ||
"eslint-remote-tester-results", | ||
"rules/utils/lodash.js", | ||
"test/integration/{fixtures,fixtures-local}/**" | ||
], | ||
"rules": { | ||
"unicorn/expiring-todo-comments": "off", | ||
"unicorn/no-null": "error", | ||
"unicorn/prevent-abbreviations": [ | ||
"unicorn/prefer-array-flat": [ | ||
"error", | ||
{ | ||
"replacements": { | ||
"ref": { | ||
"reference": true | ||
} | ||
} | ||
"functions": [ | ||
"flat", | ||
"flatten" | ||
] | ||
} | ||
] | ||
], | ||
"import/order": "off" | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": "rules/utils/*.js", | ||
"rules": { | ||
"eslint-plugin/prefer-object-rule": "off" | ||
} | ||
}, | ||
{ | ||
"files": [ | ||
@@ -137,6 +148,16 @@ "**/*.js" | ||
"strict": "error", | ||
"unicorn/prefer-module": "off", | ||
"eslint-plugin/require-meta-schema": "off", | ||
"eslint-plugin/require-meta-has-suggestions": "off", | ||
"eslint-plugin/require-meta-docs-url": "off", | ||
"unicorn/prefer-module": "off" | ||
} | ||
}, | ||
{ | ||
"files": [ | ||
"rules/*.js" | ||
], | ||
"plugins": [ | ||
"eslint-plugin" | ||
], | ||
"extends": [ | ||
"plugin:eslint-plugin/all" | ||
], | ||
"rules": { | ||
"eslint-plugin/require-meta-docs-description": [ | ||
@@ -147,7 +168,22 @@ "error", | ||
} | ||
] | ||
], | ||
"eslint-plugin/require-meta-docs-url": "off", | ||
"eslint-plugin/require-meta-has-suggestions": "off", | ||
"eslint-plugin/require-meta-schema": "off" | ||
} | ||
} | ||
] | ||
}, | ||
"npmpackagejsonlint": { | ||
"rules": { | ||
"prefer-caret-version-devDependencies": [ | ||
"error", | ||
{ | ||
"exceptions": [ | ||
"eslint-plugin-internal-rules" | ||
] | ||
} | ||
] | ||
} | ||
} | ||
} |
351
readme.md
@@ -1,6 +0,7 @@ | ||
# 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) | ||
# 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"> | ||
> Various awesome ESLint rules | ||
> More than 100 powerful ESLint rules | ||
@@ -13,4 +14,4 @@ You might want to check out [XO](https://github.com/xojs/xo), which includes this plugin. | ||
```console | ||
$ npm install --save-dev eslint eslint-plugin-unicorn | ||
```sh | ||
npm install --save-dev eslint eslint-plugin-unicorn | ||
``` | ||
@@ -20,6 +21,6 @@ | ||
Configure it in `package.json`. | ||
Use a [preset config](#preset-configs) or configure each rule in `package.json`. | ||
<!-- Do not manually modify this table. Run: `npm run generate-usage-example` --> | ||
<!-- USAGE_EXAMPLE_START --> | ||
If you don't use the preset, ensure you use the same `env` and `parserOptions` config as below. | ||
```json | ||
@@ -30,6 +31,6 @@ { | ||
"env": { | ||
"es6": true | ||
"es2024": true | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": 2021, | ||
"ecmaVersion": "latest", | ||
"sourceType": "module" | ||
@@ -42,87 +43,3 @@ }, | ||
"unicorn/better-regex": "error", | ||
"unicorn/catch-error-name": "error", | ||
"unicorn/consistent-destructuring": "error", | ||
"unicorn/consistent-function-scoping": "error", | ||
"unicorn/custom-error-definition": "off", | ||
"unicorn/empty-brace-spaces": "error", | ||
"unicorn/error-message": "error", | ||
"unicorn/escape-case": "error", | ||
"unicorn/expiring-todo-comments": "error", | ||
"unicorn/explicit-length-check": "error", | ||
"unicorn/filename-case": "error", | ||
"unicorn/import-index": "off", | ||
"unicorn/import-style": "error", | ||
"unicorn/new-for-builtins": "error", | ||
"unicorn/no-abusive-eslint-disable": "error", | ||
"unicorn/no-array-callback-reference": "error", | ||
"unicorn/no-array-for-each": "error", | ||
"unicorn/no-array-method-this-argument": "error", | ||
"unicorn/no-array-push-push": "error", | ||
"unicorn/no-array-reduce": "error", | ||
"unicorn/no-console-spaces": "error", | ||
"unicorn/no-document-cookie": "error", | ||
"unicorn/no-for-loop": "error", | ||
"unicorn/no-hex-escape": "error", | ||
"unicorn/no-instanceof-array": "error", | ||
"unicorn/no-keyword-prefix": "off", | ||
"unicorn/no-lonely-if": "error", | ||
"no-nested-ternary": "off", | ||
"unicorn/no-nested-ternary": "error", | ||
"unicorn/no-new-array": "error", | ||
"unicorn/no-new-buffer": "error", | ||
"unicorn/no-null": "error", | ||
"unicorn/no-object-as-default-parameter": "error", | ||
"unicorn/no-process-exit": "error", | ||
"unicorn/no-static-only-class": "error", | ||
"unicorn/no-this-assignment": "error", | ||
"unicorn/no-unreadable-array-destructuring": "error", | ||
"unicorn/no-unsafe-regex": "off", | ||
"unicorn/no-unused-properties": "off", | ||
"unicorn/no-useless-undefined": "error", | ||
"unicorn/no-zero-fractions": "error", | ||
"unicorn/number-literal-case": "error", | ||
"unicorn/numeric-separators-style": "error", | ||
"unicorn/prefer-add-event-listener": "error", | ||
"unicorn/prefer-array-find": "error", | ||
"unicorn/prefer-array-flat": "error", | ||
"unicorn/prefer-array-flat-map": "error", | ||
"unicorn/prefer-array-index-of": "error", | ||
"unicorn/prefer-array-some": "error", | ||
"unicorn/prefer-at": "off", | ||
"unicorn/prefer-date-now": "error", | ||
"unicorn/prefer-default-parameters": "error", | ||
"unicorn/prefer-dom-node-append": "error", | ||
"unicorn/prefer-dom-node-dataset": "error", | ||
"unicorn/prefer-dom-node-remove": "error", | ||
"unicorn/prefer-dom-node-text-content": "error", | ||
"unicorn/prefer-includes": "error", | ||
"unicorn/prefer-keyboard-event-key": "error", | ||
"unicorn/prefer-math-trunc": "error", | ||
"unicorn/prefer-modern-dom-apis": "error", | ||
"unicorn/prefer-module": "error", | ||
"unicorn/prefer-negative-index": "error", | ||
"unicorn/prefer-node-protocol": "error", | ||
"unicorn/prefer-number-properties": "error", | ||
"unicorn/prefer-object-has-own": "off", | ||
"unicorn/prefer-optional-catch-binding": "error", | ||
"unicorn/prefer-prototype-methods": "error", | ||
"unicorn/prefer-query-selector": "error", | ||
"unicorn/prefer-reflect-apply": "error", | ||
"unicorn/prefer-regexp-test": "error", | ||
"unicorn/prefer-set-has": "error", | ||
"unicorn/prefer-spread": "error", | ||
"unicorn/prefer-string-replace-all": "off", | ||
"unicorn/prefer-string-slice": "error", | ||
"unicorn/prefer-string-starts-ends-with": "error", | ||
"unicorn/prefer-string-trim-start-end": "error", | ||
"unicorn/prefer-switch": "error", | ||
"unicorn/prefer-ternary": "error", | ||
"unicorn/prefer-top-level-await": "off", | ||
"unicorn/prefer-type-error": "error", | ||
"unicorn/prevent-abbreviations": "error", | ||
"unicorn/require-array-join-separator": "error", | ||
"unicorn/require-number-to-fixed-digits-argument": "error", | ||
"unicorn/require-post-message-target-origin": "error", | ||
"unicorn/string-content": "off", | ||
"unicorn/throw-new-error": "error" | ||
"unicorn/…": "error" | ||
} | ||
@@ -132,115 +49,141 @@ } | ||
``` | ||
<!-- USAGE_EXAMPLE_END --> | ||
## Rules | ||
Each rule has emojis denoting: | ||
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` --> | ||
<!-- begin auto-generated rules list --> | ||
* ✅ if it belongs to the `recommended` configuration | ||
* 🔧 if some problems reported by the rule are automatically fixable by the `--fix` [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) option | ||
* 💡 if some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) | ||
💼 [Configurations](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs) enabled in.\ | ||
✅ Set in the `recommended` [configuration](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs).\ | ||
🔧 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/developer-guide/working-with-rules#providing-suggestions). | ||
<!-- Do not manually modify this table. Run: `npm run generate-rules-table` --> | ||
<!-- RULES_TABLE_START --> | ||
| 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-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | ✅ | 🔧 | 💡 | | ||
| [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 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-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-push-push](docs/rules/no-array-push-push.md) | Enforce combining multiple `Array#push()` into one call. | ✅ | 🔧 | 💡 | | ||
| [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | ✅ | | | | ||
| [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. | ✅ | 🔧 | | | ||
| [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-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. | ✅ | 🔧 | | | ||
| [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-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | ✅ | 🔧 | | | ||
| [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-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-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. | ✅ | 🔧 | | | ||
| [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-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}(…)`. | ✅ | 🔧 | 💡 | | ||
| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. | ✅ | 🔧 | 💡 | | ||
| [prefer-blob-reading-methods](docs/rules/prefer-blob-reading-methods.md) | Prefer `Blob#arrayBuffer()` over `FileReader#readAsArrayBuffer(…)` and `Blob#text()` over `FileReader#readAsText(…)`. | ✅ | | | | ||
| [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-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` 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-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()`. | ✅ | 🔧 | | | ||
| [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-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-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split('')`. | ✅ | 🔧 | 💡 | | ||
| [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-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-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 throwing an error. | ✅ | 🔧 | | | ||
| 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-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | ✅ | 🔧 | 💡 | | ||
| [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 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-index](docs/rules/import-index.md) | Enforce importing index files with `.`. | | 🔧 | | | ||
| [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-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 `Array#forEach(…)`. | ✅ | 🔧 | | | ||
| [no-array-method-this-argument](docs/rules/no-array-method-this-argument.md) | Disallow using the `this` argument in array methods. | ✅ | 🔧 | 💡 | | ||
| [no-array-push-push](docs/rules/no-array-push-push.md) | Enforce combining multiple `Array#push()` into one call. | ✅ | 🔧 | 💡 | | ||
| [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | ✅ | | | | ||
| [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-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-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. | ✅ | 🔧 | | | ||
| [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-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-static-only-class](docs/rules/no-static-only-class.md) | Forbid classes that only have static members. | ✅ | 🔧 | | | ||
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. | ✅ | | | | ||
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. | ✅ | 🔧 | | | ||
| [no-unsafe-regex](docs/rules/no-unsafe-regex.md) | Disallow unsafe regular expressions. | | | | | ||
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | | | ||
| [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(…)` over the first 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()` over `Array#findIndex()` 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(…)`. | ✅ | 🔧 | 💡 | | ||
| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. | | 🔧 | 💡 | | ||
| [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 `.setAttribute(…)`. | ✅ | 🔧 | | | ||
| [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-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 | | ||
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ | 🔧 | | | ||
| [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-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | 💡 | | ||
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. | ✅ | 🔧 | | | ||
| [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-has-own](docs/rules/prefer-object-has-own.md) | Prefer `Object.hasOwn(…)` over `Object.prototype.hasOwnProperty.call(…)`. | | 🔧 | | | ||
| [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()`. | ✅ | 🔧 | | | ||
| [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-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 | | ||
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`. | ✅ | 🔧 | 💡 | | ||
| [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-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. | ✅ | 🔧 | | | ||
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. | ✅ | 🔧 | | | ||
| [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. | | 🔧 | 💡 | | ||
| [throw-new-error](docs/rules/throw-new-error.md) | Require `new` when throwing an error. | ✅ | 🔧 | | | ||
<!-- end auto-generated rules list --> | ||
<!-- RULES_TABLE_END --> | ||
### Deprecated Rules | ||
## Deprecated Rules | ||
See [docs/deprecated-rules.md](docs/deprecated-rules.md) | ||
## Recommended config | ||
## Preset configs | ||
This plugin exports a [`recommended` config](index.js) that enforces good practices. | ||
See the [ESLint docs](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending config files. | ||
Enable it in your `package.json` with the `extends` option: | ||
**Note**: Preset configs will also enable the correct [parser options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options) and [environment](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments). | ||
### Recommended config | ||
This plugin exports a [`recommended` config](configs/recommended.js) that enforces good practices. | ||
```json | ||
@@ -255,16 +198,26 @@ { | ||
See the [ESLint docs](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending config files. | ||
### All config | ||
**Note**: This config will also enable the correct [parser options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options) and [environment](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments). | ||
This plugin exports an [`all` config](configs/all.js) that makes use of all rules (except for deprecated ones). | ||
```json | ||
{ | ||
"name": "my-awesome-project", | ||
"eslintConfig": { | ||
"extends": "plugin:unicorn/all" | ||
} | ||
} | ||
``` | ||
## Maintainers | ||
- [Sindre Sorhus](https://github.com/sindresorhus) | ||
- [Adam Babcock](https://github.com/MrHen) | ||
- [Fisker Cheung](https://github.com/fisker) | ||
- [Bryan Mishkin](https://github.com/bmish) | ||
- [futpib](https://github.com/futpib) | ||
- [Fisker Cheung](https://github.com/fisker) | ||
###### Former | ||
### Former | ||
- [Jeroen Engels](https://github.com/jfmengels) | ||
- [Sam Verschueren](https://github.com/SamVerschueren) | ||
- [Adam Babcock](https://github.com/MrHen) |
'use strict'; | ||
const cleanRegexp = require('clean-regexp'); | ||
const {optimize} = require('regexp-tree'); | ||
const quoteString = require('./utils/quote-string.js'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const {isStringLiteral, isNewExpression, isRegexLiteral} = require('./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]: '{{original}} can be optimized to {{optimized}}.', | ||
[MESSAGE_ID_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}', | ||
}; | ||
const newRegExp = [ | ||
newExpressionSelector({name: 'RegExp', min: 1}), | ||
'[arguments.0.type="Literal"]' | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -27,8 +25,11 @@ const {sortCharacterClasses} = context.options[0] || {}; | ||
return { | ||
'Literal[regex]': node => { | ||
Literal(node) { | ||
if (!isRegexLiteral(node)) { | ||
return; | ||
} | ||
const {raw: original, regex} = node; | ||
// Regular Expressions with `u` flag are not well handled by `regexp-tree` | ||
// 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')) { | ||
if (regex.flags.includes('u') || regex.flags.includes('v')) { | ||
return; | ||
@@ -44,7 +45,7 @@ } | ||
node, | ||
messageId: MESSAGE_ID_PARSE_ERROR, | ||
data: { | ||
original, | ||
error: error.message | ||
error: error.message, | ||
}, | ||
message: 'Problem parsing {{original}}: {{error}}' | ||
}; | ||
@@ -57,3 +58,3 @@ } | ||
return { | ||
const problem = { | ||
node, | ||
@@ -63,11 +64,32 @@ messageId: MESSAGE_ID, | ||
original, | ||
optimized | ||
optimized, | ||
}, | ||
fix: fixer => fixer.replaceText(node, 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), | ||
}); | ||
}, | ||
[newRegExp]: node => { | ||
NewExpression(node) { | ||
if (!isNewExpression(node, {name: 'RegExp', minimumArguments: 1})) { | ||
return; | ||
} | ||
const [patternNode, flagsNode] = node.arguments; | ||
if (typeof patternNode.value !== 'string') { | ||
if (!isStringLiteral(patternNode)) { | ||
return; | ||
@@ -77,7 +99,5 @@ } | ||
const oldPattern = patternNode.value; | ||
const flags = flagsNode && | ||
flagsNode.type === 'Literal' && | ||
typeof flagsNode.value === 'string' ? | ||
flagsNode.value : | ||
''; | ||
const flags = isStringLiteral(flagsNode) | ||
? flagsNode.value | ||
: ''; | ||
@@ -92,11 +112,11 @@ const newPattern = cleanRegexp(oldPattern, flags); | ||
original: oldPattern, | ||
optimized: newPattern | ||
optimized: newPattern, | ||
}, | ||
fix: fixer => fixer.replaceText( | ||
patternNode, | ||
quoteString(newPattern) | ||
) | ||
escapeString(newPattern, patternNode.raw.charAt(0)), | ||
), | ||
}; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -108,11 +128,13 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
sortCharacterClasses: { | ||
type: 'boolean', | ||
default: true | ||
} | ||
} | ||
} | ||
default: true, | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -123,8 +145,8 @@ create, | ||
docs: { | ||
description: 'Improve regexes by making them shorter, consistent, and safer.' | ||
description: 'Improve regexes by making them shorter, consistent, and safer.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const renameVariable = require('./utils/rename-variable.js'); | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
const {renameVariable} = require('./fix/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'catch-error-name'; | ||
const messages = { | ||
[MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.' | ||
[MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.', | ||
}; | ||
const selector = matches([ | ||
// `try {} catch (foo) {}` | ||
[ | ||
'CatchClause', | ||
' > ', | ||
'Identifier.param' | ||
].join(''), | ||
// - `promise.then(…, foo => {})` | ||
// - `promise.then(…, function(foo) {})` | ||
// - `promise.catch(foo => {})` | ||
// - `promise.catch(function(foo) {})` | ||
[ | ||
matches([ | ||
methodCallSelector({name: 'then', length: 2}), | ||
methodCallSelector({name: 'catch', length: 1}) | ||
]), | ||
' > ', | ||
':matches(FunctionExpression, ArrowFunctionExpression).arguments:last-child', | ||
' > ', | ||
'Identifier.params:first-child' | ||
].join('') | ||
]); | ||
// - `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, | ||
optionalMember: false, | ||
}) | ||
|| isMethodCall(node.parent.parent, { | ||
method: 'catch', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
&& node.parent.parent.arguments.at(-1) === node.parent; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {ecmaVersion} = context.parserOptions; | ||
const options = { | ||
name: 'error', | ||
ignore: [], | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
const {name: expectedName} = options; | ||
const ignore = options.ignore.map( | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u') | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'), | ||
); | ||
const isNameAllowed = name => | ||
name === expectedName || | ||
ignore.some(regexp => regexp.test(name)) || | ||
name.endsWith(expectedName) || | ||
name.endsWith(expectedName.charAt(0).toUpperCase() + expectedName.slice(1)); | ||
name === expectedName | ||
|| ignore.some(regexp => regexp.test(name)) | ||
|| name.endsWith(expectedName) | ||
|| name.endsWith(expectedName.charAt(0).toUpperCase() + expectedName.slice(1)); | ||
return { | ||
[selector]: node => { | ||
Identifier(node) { | ||
if ( | ||
!(node.parent.type === 'CatchClause' && node.parent.param === node) | ||
&& !isPromiseCatchParameter(node) | ||
) { | ||
return; | ||
} | ||
const originalName = node.name; | ||
if ( | ||
isNameAllowed(originalName) || | ||
isNameAllowed(originalName.replace(/_+$/g, '')) | ||
isNameAllowed(originalName) | ||
|| isNameAllowed(originalName.replaceAll(/_+$/g, '')) | ||
) { | ||
@@ -64,3 +70,3 @@ return; | ||
const scope = context.getScope(); | ||
const scope = context.sourceCode.getScope(node); | ||
const variable = findVariable(scope, node); | ||
@@ -70,3 +76,3 @@ | ||
// But can't reproduce, just ignore this case | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (!variable) { | ||
@@ -82,7 +88,7 @@ return; | ||
variable.scope, | ||
...variable.references.map(({from}) => from) | ||
...variable.references.map(({from}) => from), | ||
]; | ||
const fixedName = avoidCapture(expectedName, scopes, ecmaVersion); | ||
const fixedName = avoidCapture(expectedName, scopes); | ||
return { | ||
const problem = { | ||
node, | ||
@@ -92,7 +98,12 @@ messageId: MESSAGE_ID, | ||
originalName, | ||
fixedName | ||
fixedName: fixedName || expectedName, | ||
}, | ||
fix: fixer => renameVariable(variable, fixedName, fixer) | ||
}; | ||
} | ||
if (fixedName) { | ||
problem.fix = fixer => renameVariable(variable, fixedName, fixer); | ||
} | ||
return problem; | ||
}, | ||
}; | ||
@@ -104,14 +115,16 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
name: { | ||
type: 'string' | ||
type: 'string', | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
} | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -122,8 +135,8 @@ create, | ||
docs: { | ||
description: 'Enforce a specific parameter name in catch clauses.' | ||
description: 'Enforce a specific parameter name in catch clauses.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const {not, notLeftHandSideSelector} = require('./selectors/index.js'); | ||
const isLeftHandSide = require('./utils/is-left-hand-side.js'); | ||
const {isCallOrNewExpression} = require('./ast/index.js'); | ||
@@ -8,19 +9,2 @@ const MESSAGE_ID = 'consistentDestructuring'; | ||
const declaratorSelector = [ | ||
'VariableDeclarator', | ||
'[id.type="ObjectPattern"]', | ||
'[init]', | ||
'[init.type!="Literal"]' | ||
].join(''); | ||
const memberSelector = [ | ||
'MemberExpression', | ||
'[computed!=true]', | ||
notLeftHandSideSelector(), | ||
not([ | ||
'CallExpression > .callee', | ||
'NewExpression> .callee' | ||
]) | ||
].join(''); | ||
const isSimpleExpression = expression => { | ||
@@ -39,4 +23,4 @@ while (expression) { | ||
return expression.type === 'Identifier' || | ||
expression.type === 'ThisExpression'; | ||
return expression.type === 'Identifier' | ||
|| expression.type === 'ThisExpression'; | ||
}; | ||
@@ -56,23 +40,39 @@ | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {ecmaVersion} = context.parserOptions; | ||
const source = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const declarations = new Map(); | ||
return { | ||
[declaratorSelector]: node => { | ||
// Ignore any complex expressions (e.g. arrays, functions) | ||
if (!isSimpleExpression(node.init)) { | ||
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(source.getText(node.init), { | ||
scope: context.getScope(), | ||
variables: context.getDeclaredVariables(node), | ||
objectPattern: node.id | ||
declarations.set(sourceCode.getText(node.init), { | ||
scope: sourceCode.getScope(node), | ||
variables: sourceCode.getDeclaredVariables(node), | ||
objectPattern: node.id, | ||
}); | ||
}, | ||
[memberSelector]: node => { | ||
const declaration = declarations.get(source.getText(node.object)); | ||
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) { | ||
@@ -83,3 +83,3 @@ return; | ||
const {scope, objectPattern} = declaration; | ||
const memberScope = context.getScope(); | ||
const memberScope = sourceCode.getScope(node); | ||
@@ -92,16 +92,16 @@ // Property is destructured outside the current scope | ||
const destructurings = objectPattern.properties.filter(property => | ||
property.type === 'Property' && | ||
property.key.type === 'Identifier' && | ||
property.value.type === 'Identifier' | ||
property.type === 'Property' | ||
&& property.key.type === 'Identifier' | ||
&& property.value.type === 'Identifier', | ||
); | ||
const lastProperty = objectPattern.properties[objectPattern.properties.length - 1]; | ||
const lastProperty = objectPattern.properties.at(-1); | ||
const hasRest = lastProperty && lastProperty.type === 'RestElement'; | ||
const expression = source.getText(node); | ||
const member = source.getText(node.property); | ||
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 | ||
property.key.name === member, | ||
); | ||
@@ -116,3 +116,3 @@ | ||
// Destructured member collides with an existing identifier | ||
if (avoidCapture(member, [memberScope], ecmaVersion) !== member) { | ||
if (avoidCapture(member, [memberScope]) !== member) { | ||
return; | ||
@@ -126,3 +126,3 @@ } | ||
node, | ||
messageId: MESSAGE_ID | ||
messageId: MESSAGE_ID, | ||
}; | ||
@@ -140,7 +140,7 @@ } | ||
expression, | ||
property: newMember | ||
property: newMember, | ||
}, | ||
* fix(fixer) { | ||
const {properties} = objectPattern; | ||
const lastProperty = properties[properties.length - 1]; | ||
const lastProperty = properties.at(-1); | ||
@@ -150,13 +150,14 @@ yield fixer.replaceText(node, newMember); | ||
if (!destructuredMember) { | ||
yield lastProperty ? | ||
fixer.insertTextAfter(lastProperty, `, ${newMember}`) : | ||
fixer.replaceText(objectPattern, `{${newMember}}`); | ||
yield lastProperty | ||
? fixer.insertTextAfter(lastProperty, `, ${newMember}`) | ||
: fixer.replaceText(objectPattern, `{${newMember}}`); | ||
} | ||
} | ||
}] | ||
}, | ||
}], | ||
}; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -167,11 +168,11 @@ create, | ||
docs: { | ||
description: 'Use destructured variables over properties.' | ||
description: 'Use destructured variables over properties.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages: { | ||
[MESSAGE_ID]: 'Use destructured variables over properties.', | ||
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.' | ||
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.', | ||
}, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils'); | ||
const getReferences = require('./utils/get-references.js'); | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils'); | ||
const { | ||
getReferences, | ||
isNodeMatches, | ||
} = require('./utils/index.js'); | ||
const { | ||
functionTypes, | ||
} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'consistent-function-scoping'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.' | ||
[MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.', | ||
}; | ||
@@ -23,3 +29,3 @@ | ||
// Skip recursive function name | ||
if (definition && definition.type === 'FunctionName' && resolved.name === definition.name.name) { | ||
if (definition?.type === 'FunctionName' && resolved.name === definition.name.name) { | ||
return false; | ||
@@ -40,4 +46,4 @@ } | ||
if ( | ||
!identifier.parent || | ||
identifier.parent.type !== 'FunctionDeclaration' | ||
!identifier.parent | ||
|| identifier.parent.type !== 'FunctionDeclaration' | ||
) { | ||
@@ -50,3 +56,3 @@ return false; | ||
// If we have a scope, the earlier checks should have worked so ignore them here | ||
/* istanbul ignore next: Hard to test */ | ||
/* c8 ignore next 3 */ | ||
if (identifierScope) { | ||
@@ -57,3 +63,3 @@ return false; | ||
const identifierParentScope = scopeManager.acquire(identifier.parent); | ||
/* istanbul ignore next: Hard to test */ | ||
/* c8 ignore next 3 */ | ||
if (!identifierParentScope) { | ||
@@ -77,5 +83,5 @@ return false; | ||
.some(variable => | ||
hitReference(variable.references) || | ||
hitDefinitions(variable.defs) || | ||
hitIdentifier(variable.identifiers) | ||
hitReference(variable.references) | ||
|| hitDefinitions(variable.defs) | ||
|| hitIdentifier(variable.identifiers), | ||
); | ||
@@ -85,3 +91,3 @@ } | ||
// https://reactjs.org/docs/hooks-reference.html | ||
const reactHooks = new Set([ | ||
const reactHooks = [ | ||
'useState', | ||
@@ -96,26 +102,22 @@ 'useEffect', | ||
'useLayoutEffect', | ||
'useDebugValue' | ||
]); | ||
'useDebugValue', | ||
].flatMap(hookName => [hookName, `React.${hookName}`]); | ||
const isReactHook = scope => | ||
scope.block && | ||
scope.block.parent && | ||
scope.block.parent.callee && | ||
scope.block.parent.callee.type === 'Identifier' && | ||
reactHooks.has(scope.block.parent.callee.name); | ||
scope.block?.parent?.callee | ||
&& isNodeMatches(scope.block.parent.callee, reactHooks); | ||
const isArrowFunctionWithThis = scope => | ||
scope.type === 'function' && | ||
scope.block && | ||
scope.block.type === 'ArrowFunctionExpression' && | ||
(scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope))); | ||
scope.type === 'function' | ||
&& scope.block?.type === 'ArrowFunctionExpression' | ||
&& (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope))); | ||
const iifeFunctionTypes = new Set([ | ||
'FunctionExpression', | ||
'ArrowFunctionExpression' | ||
'ArrowFunctionExpression', | ||
]); | ||
const isIife = node => node && | ||
iifeFunctionTypes.has(node.type) && | ||
node.parent && | ||
node.parent.type === 'CallExpression' && | ||
node.parent.callee === node; | ||
const isIife = node => | ||
iifeFunctionTypes.has(node.type) | ||
&& node.parent.type === 'CallExpression' | ||
&& node.parent.callee === node; | ||
@@ -148,6 +150,6 @@ function checkNode(node, scopeManager) { | ||
if ( | ||
!parentScope || | ||
parentScope.type === 'global' || | ||
isReactHook(parentScope) || | ||
isIife(parentNode) | ||
!parentScope | ||
|| parentScope.type === 'global' | ||
|| isReactHook(parentScope) | ||
|| isIife(parentNode) | ||
) { | ||
@@ -160,5 +162,6 @@ return true; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {checkArrowFunctions} = {checkArrowFunctions: true, ...context.options[0]}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager} = sourceCode; | ||
@@ -168,37 +171,37 @@ | ||
return { | ||
':function': () => { | ||
functions.push(false); | ||
}, | ||
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; | ||
} | ||
}, | ||
':function:exit': node => { | ||
const currentFunctionHasJsx = functions.pop(); | ||
if (currentFunctionHasJsx) { | ||
return; | ||
} | ||
context.on(functionTypes, () => { | ||
functions.push(false); | ||
}); | ||
if (node.type === 'ArrowFunctionExpression' && !checkArrowFunctions) { | ||
return; | ||
} | ||
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; | ||
} | ||
}); | ||
if (checkNode(node, scopeManager)) { | ||
return; | ||
} | ||
context.onExit(functionTypes, node => { | ||
const currentFunctionHasJsx = functions.pop(); | ||
if (currentFunctionHasJsx) { | ||
return; | ||
} | ||
return { | ||
node, | ||
loc: getFunctionHeadLocation(node, sourceCode), | ||
messageId: MESSAGE_ID, | ||
data: { | ||
functionNameWithKind: getFunctionNameWithKind(node, sourceCode) | ||
} | ||
}; | ||
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), | ||
}, | ||
}; | ||
}); | ||
}; | ||
@@ -209,11 +212,13 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkArrowFunctions: { | ||
type: 'boolean', | ||
default: true | ||
} | ||
} | ||
} | ||
default: true, | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -224,7 +229,7 @@ create, | ||
docs: { | ||
description: 'Move function definitions to the highest possible scope.' | ||
description: 'Move function definitions to the highest possible scope.', | ||
}, | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {upperFirst} = require('lodash'); | ||
const {upperFirst} = require('./utils/lodash.js'); | ||
const MESSAGE_ID_INVALID_EXPORT = 'invalidExport'; | ||
const messages = { | ||
[MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class' | ||
[MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class', | ||
}; | ||
@@ -35,10 +35,10 @@ | ||
const isSuperExpression = node => | ||
node.type === 'ExpressionStatement' && | ||
node.expression.type === 'CallExpression' && | ||
node.expression.callee.type === 'Super'; | ||
node.type === 'ExpressionStatement' | ||
&& node.expression.type === 'CallExpression' | ||
&& node.expression.callee.type === 'Super'; | ||
const isAssignmentExpression = (node, name) => { | ||
if ( | ||
node.type !== 'ExpressionStatement' || | ||
node.expression.type !== 'AssignmentExpression' | ||
node.type !== 'ExpressionStatement' | ||
|| node.expression.type !== 'AssignmentExpression' | ||
) { | ||
@@ -57,19 +57,8 @@ return false; | ||
const isPropertyDefinition = (node, name) => { | ||
const {type, computed, key} = node; | ||
if (type !== 'PropertyDefinition' && type !== 'ClassProperty') { | ||
return false; | ||
} | ||
const isPropertyDefinition = (node, name) => | ||
node.type === 'PropertyDefinition' | ||
&& !node.computed | ||
&& node.key.type === 'Identifier' | ||
&& node.key.name === name; | ||
if (computed) { | ||
return false; | ||
} | ||
if (key.type !== 'Identifier') { | ||
return false; | ||
} | ||
return key.name === name; | ||
}; | ||
function * customErrorDefinition(context, node) { | ||
@@ -90,3 +79,3 @@ if (!hasValidSuperClass(node)) { | ||
node: node.id, | ||
message: `Invalid class name, use \`${className}\`.` | ||
message: `Invalid class name, use \`${className}\`.`, | ||
}; | ||
@@ -104,4 +93,4 @@ } | ||
range[0], | ||
range[0] + 1 | ||
], getConstructorMethod(name)) | ||
range[0] + 1, | ||
], getConstructorMethod(name)), | ||
}; | ||
@@ -126,3 +115,3 @@ return; | ||
node: constructorBodyNode, | ||
message: 'Missing call to `super()` in constructor.' | ||
message: 'Missing call to `super()` in constructor.', | ||
}; | ||
@@ -140,3 +129,3 @@ } else if (messageExpressionIndex !== -1) { | ||
superExpression.range[0], | ||
superExpression.range[0] + 6 | ||
superExpression.range[0] + 6, | ||
], rhs.raw || rhs.name); | ||
@@ -147,5 +136,5 @@ } | ||
messageExpressionIndex === 0 ? constructorBodyNode.range[0] : constructorBody[messageExpressionIndex - 1].range[1], | ||
expression.range[1] | ||
expression.range[1], | ||
]); | ||
} | ||
}, | ||
}; | ||
@@ -158,6 +147,6 @@ } | ||
if (!nameProperty || !nameProperty.value || nameProperty.value.value !== name) { | ||
if (!nameProperty?.value || nameProperty.value.value !== name) { | ||
yield { | ||
node: nameProperty && nameProperty.value ? nameProperty.value : constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.` | ||
node: nameProperty?.value ?? constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.`, | ||
}; | ||
@@ -167,4 +156,4 @@ } | ||
yield { | ||
node: nameExpression ? nameExpression.expression.right : constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.` | ||
node: nameExpression?.expression.right ?? constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.`, | ||
}; | ||
@@ -201,14 +190,26 @@ } | ||
messageId: MESSAGE_ID_INVALID_EXPORT, | ||
fix: fixer => fixer.replaceText(node.left.property, errorName) | ||
fix: fixer => fixer.replaceText(node.left.property, errorName), | ||
}; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
return { | ||
ClassDeclaration: node => customErrorDefinition(context, node), | ||
'AssignmentExpression[right.type="ClassExpression"]': node => customErrorDefinition(context, node.right), | ||
'AssignmentExpression[left.type="MemberExpression"][left.object.type="Identifier"][left.object.name="exports"]': node => customErrorExport(context, node) | ||
}; | ||
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} */ | ||
module.exports = { | ||
@@ -219,7 +220,7 @@ create, | ||
docs: { | ||
description: 'Enforce correct `Error` subclassing.' | ||
description: 'Enforce correct `Error` subclassing.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isOpeningBraceToken} = require('eslint-utils'); | ||
const {matches} = require('./selectors/index.js'); | ||
const {isOpeningBraceToken} = require('@eslint-community/eslint-utils'); | ||
const MESSAGE_ID = 'empty-brace-spaces'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not add spaces between braces.' | ||
[MESSAGE_ID]: 'Do not add spaces between braces.', | ||
}; | ||
const selector = matches([ | ||
'BlockStatement[body.length=0]', | ||
'ClassBody[body.length=0]', | ||
'ObjectExpression[properties.length=0]', | ||
// Experimental https://github.com/tc39/proposal-record-tuple | ||
'RecordExpression[properties.length=0]', | ||
// Experimental https://github.com/tc39/proposal-class-static-block | ||
'StaticBlock[body.length=0]' | ||
]); | ||
const getProblem = (node, context) => { | ||
const {sourceCode} = context; | ||
const filter = node.type === 'RecordExpression' | ||
? token => token.type === 'Punctuator' && (token.value === '#{' || token.value === '{|') | ||
: isOpeningBraceToken; | ||
const openingBrace = sourceCode.getFirstToken(node, {filter}); | ||
const closingBrace = sourceCode.getLastToken(node); | ||
const [, start] = openingBrace.range; | ||
const [end] = closingBrace.range; | ||
const textBetween = sourceCode.text.slice(start, end); | ||
if (!/^\s+$/.test(textBetween)) { | ||
return; | ||
} | ||
return { | ||
loc: { | ||
start: openingBrace.loc.end, | ||
end: closingBrace.loc.start, | ||
}, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.removeRange([start, end]), | ||
}; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
return { | ||
[selector](node) { | ||
const sourceCode = context.getSourceCode(); | ||
const filter = node.type === 'RecordExpression' ? | ||
token => token.type === 'Punctuator' && (token.value === '#{' || token.value === '{|') : | ||
isOpeningBraceToken; | ||
const openingBrace = sourceCode.getFirstToken(node, {filter}); | ||
const closingBrace = sourceCode.getLastToken(node); | ||
const [, start] = openingBrace.range; | ||
const [end] = closingBrace.range; | ||
const textBetween = sourceCode.text.slice(start, end); | ||
context.on([ | ||
'BlockStatement', | ||
'ClassBody', | ||
'StaticBlock', | ||
], node => { | ||
if (node.body.length > 0) { | ||
return; | ||
} | ||
if (!/^\s+$/.test(textBetween)) { | ||
return; | ||
} | ||
return getProblem(node, context); | ||
}); | ||
return { | ||
loc: { | ||
start: openingBrace.loc.end, | ||
end: closingBrace.loc.start | ||
}, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.removeRange([start, end]) | ||
}; | ||
context.on([ | ||
'ObjectExpression', | ||
// Experimental https://github.com/tc39/proposal-record-tuple | ||
'RecordExpression', | ||
], node => { | ||
if (node.properties.length > 0) { | ||
return; | ||
} | ||
}; | ||
return getProblem(node, context); | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -55,7 +67,7 @@ create, | ||
docs: { | ||
description: 'Enforce no spaces between braces.' | ||
description: 'Enforce no spaces between braces.', | ||
}, | ||
fixable: 'whitespace', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {callOrNewExpressionSelector} = require('./selectors/index.js'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const {isCallOrNewExpression} = require('./ast/index.js'); | ||
@@ -11,75 +12,85 @@ const MESSAGE_ID_MISSING_MESSAGE = 'missing-message'; | ||
[MESSAGE_ID_EMPTY_MESSAGE]: 'Error message should not be an empty string.', | ||
[MESSAGE_ID_NOT_STRING]: 'Error message should be a string.' | ||
[MESSAGE_ID_NOT_STRING]: 'Error message should be a string.', | ||
}; | ||
const selector = callOrNewExpressionSelector({ | ||
names: [ | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error | ||
'Error', | ||
'EvalError', | ||
'RangeError', | ||
'ReferenceError', | ||
'SyntaxError', | ||
'TypeError', | ||
'URIError', | ||
'InternalError', | ||
'AggregateError' | ||
] | ||
}); | ||
const builtinErrors = [ | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error | ||
'Error', | ||
'EvalError', | ||
'RangeError', | ||
'ReferenceError', | ||
'SyntaxError', | ||
'TypeError', | ||
'URIError', | ||
'InternalError', | ||
'AggregateError', | ||
]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
return { | ||
[selector](expression) { | ||
const constructorName = expression.callee.name; | ||
const messageArgumentIndex = constructorName === 'AggregateError' ? 1 : 0; | ||
const callArguments = expression.arguments; | ||
context.on(['CallExpression', 'NewExpression'], expression => { | ||
if (!isCallOrNewExpression(expression, { | ||
names: builtinErrors, | ||
optional: false, | ||
})) { | ||
return; | ||
} | ||
// If message is `SpreadElement` or there is `SpreadElement` before message | ||
if (callArguments.some((node, index) => index <= messageArgumentIndex && node.type === 'SpreadElement')) { | ||
return; | ||
} | ||
const scope = context.sourceCode.getScope(expression); | ||
if (isShadowed(scope, expression.callee)) { | ||
return; | ||
} | ||
const node = callArguments[messageArgumentIndex]; | ||
if (!node) { | ||
return { | ||
node: expression, | ||
messageId: MESSAGE_ID_MISSING_MESSAGE, | ||
data: {constructorName} | ||
}; | ||
} | ||
const constructorName = expression.callee.name; | ||
const messageArgumentIndex = constructorName === 'AggregateError' ? 1 : 0; | ||
const callArguments = expression.arguments; | ||
// 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 | ||
}; | ||
} | ||
// If message is `SpreadElement` or there is `SpreadElement` before message | ||
if (callArguments.some((node, index) => index <= messageArgumentIndex && node.type === 'SpreadElement')) { | ||
return; | ||
} | ||
const staticResult = getStaticValue(node, context.getScope()); | ||
const node = callArguments[messageArgumentIndex]; | ||
if (!node) { | ||
return { | ||
node: expression, | ||
messageId: MESSAGE_ID_MISSING_MESSAGE, | ||
data: {constructorName}, | ||
}; | ||
} | ||
// We don't know the value of `message` | ||
if (!staticResult) { | ||
return; | ||
} | ||
// 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 {value} = staticResult; | ||
if (typeof value !== 'string') { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID_NOT_STRING | ||
}; | ||
} | ||
const staticResult = getStaticValue(node, scope); | ||
if (value === '') { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID_EMPTY_MESSAGE | ||
}; | ||
} | ||
// 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} */ | ||
module.exports = { | ||
@@ -90,6 +101,6 @@ create, | ||
docs: { | ||
description: 'Enforce passing a `message` value when creating a built-in error.' | ||
description: 'Enforce passing a `message` value when creating a built-in error.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const replaceTemplateElement = require('./utils/replace-template-element.js'); | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {isRegexLiteral, isStringLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'escape-case'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use uppercase characters for the value of the escape sequence.' | ||
[MESSAGE_ID]: 'Use uppercase characters for the value of the escape sequence.', | ||
}; | ||
@@ -18,3 +19,3 @@ | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fix ? fix(fixer, fixed) : fixer.replaceText(node, fixed) | ||
fix: fixer => fix ? fix(fixer, fixed) : fixer.replaceText(node, fixed), | ||
}; | ||
@@ -24,31 +25,31 @@ } | ||
const create = () => { | ||
return { | ||
Literal(node) { | ||
if (typeof node.value !== 'string') { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
context.on('Literal', node => { | ||
if (isStringLiteral(node)) { | ||
return getProblem({ | ||
node, | ||
original: node.raw | ||
original: node.raw, | ||
}); | ||
}, | ||
'Literal[regex]'(node) { | ||
} | ||
}); | ||
context.on('Literal', node => { | ||
if (isRegexLiteral(node)) { | ||
return getProblem({ | ||
node, | ||
original: node.raw, | ||
regex: escapePatternWithLowercase | ||
regex: escapePatternWithLowercase, | ||
}); | ||
}, | ||
TemplateElement(node) { | ||
return getProblem({ | ||
node, | ||
original: node.value.raw, | ||
fix: (fixer, fixed) => replaceTemplateElement(fixer, node, fixed) | ||
}); | ||
} | ||
}; | ||
}); | ||
context.on('TemplateElement', node => getProblem({ | ||
node, | ||
original: node.value.raw, | ||
fix: (fixer, fixed) => replaceTemplateElement(fixer, node, fixed), | ||
})); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -59,7 +60,7 @@ create, | ||
docs: { | ||
description: 'Require escape sequences to use uppercase values.' | ||
description: 'Require escape sequences to use uppercase values.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const path = require('node:path'); | ||
const readPkgUp = require('read-pkg-up'); | ||
const semver = require('semver'); | ||
const ci = require('ci-info'); | ||
const baseRule = require('eslint/lib/rules/no-warning-comments'); | ||
const getBuiltinRule = require('./utils/get-builtin-rule.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_AVOID_MULTIPLE_PACKAGE_VERSIONS | ||
= 'unicorn/avoidMultiplePackageVersions'; | ||
const MESSAGE_ID_REACHED_PACKAGE_VERSION = 'unicorn/reachedPackageVersion'; | ||
@@ -21,3 +24,3 @@ const MESSAGE_ID_HAVE_PACKAGE = 'unicorn/havePackage'; | ||
// Override of core rule message with a more specific one - no prefix | ||
const MESSSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT = 'unexpectedComment'; | ||
const MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT = 'unexpectedComment'; | ||
const messages = { | ||
@@ -45,147 +48,163 @@ [MESSAGE_ID_AVOID_MULTIPLE_DATES]: | ||
...baseRule.meta.messages, | ||
[MESSSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT]: | ||
'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.' | ||
[MESSAGE_ID_CORE_RULE_UNEXPECTED_COMMENT]: | ||
'Unexpected \'{{matchedTerm}}\' comment without any conditions: \'{{comment}}\'.', | ||
}; | ||
const packageResult = readPkgUp.sync(); | ||
const hasPackage = Boolean(packageResult); | ||
const packageJson = hasPackage ? packageResult.packageJson : {}; | ||
const packageDependencies = { | ||
...packageJson.dependencies, | ||
...packageJson.devDependencies | ||
}; | ||
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 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; | ||
/** @param {string} dirname */ | ||
function getPackageHelpers(dirname) { | ||
// We don't need to normalize the package.json data, because we are only using 2 properties and those 2 properties | ||
// aren't validated by the normalization. But when this plugin is used in a monorepo, the name field in the | ||
// package.json can be invalid and would make this plugin throw an error. See also #1871 | ||
/** @type {readPkgUp.ReadResult | undefined} */ | ||
let packageResult; | ||
try { | ||
packageResult = readPkgUp.sync({normalize: false, cwd: dirname}); | ||
} catch { | ||
// This can happen if package.json files have comments in them etc. | ||
packageResult = undefined; | ||
} | ||
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i; | ||
const result = TODO_ARGUMENT_RE.exec(string); | ||
const hasPackage = Boolean(packageResult); | ||
const packageJson = packageResult ? packageResult.packageJson : {}; | ||
if (!result) { | ||
return false; | ||
} | ||
const packageDependencies = { | ||
...packageJson.dependencies, | ||
...packageJson.devDependencies, | ||
}; | ||
const {rawArguments} = result.groups; | ||
function parseTodoWithArguments(string, {terms}) { | ||
const lowerCaseString = string.toLowerCase(); | ||
const lowerCaseTerms = terms.map(term => term.toLowerCase()); | ||
const hasTerm = lowerCaseTerms.some(term => lowerCaseString.includes(term)); | ||
const parsedArguments = rawArguments | ||
.split(',') | ||
.map(argument => parseArgument(argument.trim())); | ||
if (!hasTerm) { | ||
return false; | ||
} | ||
return createArgumentGroup(parsedArguments); | ||
} | ||
const TODO_ARGUMENT_RE = /\[(?<rawArguments>[^}]+)]/i; | ||
const result = TODO_ARGUMENT_RE.exec(string); | ||
function createArgumentGroup(arguments_) { | ||
const groups = {}; | ||
for (const {value, type} of arguments_) { | ||
groups[type] = groups[type] || []; | ||
groups[type].push(value); | ||
} | ||
if (!result) { | ||
return false; | ||
} | ||
return groups; | ||
} | ||
const {rawArguments} = result.groups; | ||
function parseArgument(argumentString) { | ||
if (ISO8601_DATE.test(argumentString)) { | ||
return { | ||
type: 'dates', | ||
value: argumentString | ||
}; | ||
} | ||
const parsedArguments = rawArguments | ||
.split(',') | ||
.map(argument => parseArgument(argument.trim())); | ||
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 | ||
} | ||
}; | ||
return createArgumentGroup(parsedArguments); | ||
} | ||
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(); | ||
function parseArgument(argumentString, dirname) { | ||
const {hasPackage} = getPackageHelpers(dirname); | ||
if (ISO8601_DATE.test(argumentString)) { | ||
return { | ||
type: 'dates', | ||
value: argumentString, | ||
}; | ||
} | ||
const hasEngineKeyword = name.indexOf('engine:') === 0; | ||
const isNodeEngine = hasEngineKeyword && name === 'engine:node'; | ||
if (hasPackage && DEPENDENCY_INCLUSION_RE.test(argumentString)) { | ||
const condition = argumentString[0] === '+' ? 'in' : 'out'; | ||
const name = argumentString.slice(1).trim(); | ||
if (hasEngineKeyword && isNodeEngine) { | ||
return { | ||
type: 'engines', | ||
type: 'dependencies', | ||
value: { | ||
name, | ||
condition, | ||
version | ||
} | ||
}, | ||
}; | ||
} | ||
if (!hasEngineKeyword) { | ||
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: 'dependencies', | ||
type: 'packageVersions', | ||
value: { | ||
name, | ||
condition, | ||
version | ||
} | ||
condition: condition.trim(), | ||
version: version.trim(), | ||
}, | ||
}; | ||
} | ||
} | ||
if (hasPackage && PKG_VERSION_RE.test(argumentString)) { | ||
const result = PKG_VERSION_RE.exec(argumentString); | ||
const {condition, version} = result.groups; | ||
// Currently being ignored as integration tests pointed | ||
// some TODO comments have `[random data like this]` | ||
return { | ||
type: 'packageVersions', | ||
value: { | ||
condition: condition.trim(), | ||
version: version.trim() | ||
} | ||
type: 'unknowns', | ||
value: argumentString, | ||
}; | ||
} | ||
// 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(']'); | ||
function parseTodoMessage(todoString) { | ||
// @example "TODO [...]: message here" | ||
// @example "TODO [...] message here" | ||
const argumentsEnd = todoString.indexOf(']'); | ||
const afterArguments = todoString.slice(argumentsEnd + 1).trim(); | ||
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(); | ||
} | ||
// Check if have to skip colon | ||
// @example "TODO [...]: message here" | ||
const dropColon = afterArguments[0] === ':'; | ||
if (dropColon) { | ||
return afterArguments.slice(1).trim(); | ||
return afterArguments; | ||
} | ||
return afterArguments; | ||
return {packageResult, hasPackage, packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments}; | ||
} | ||
function reachedDate(past) { | ||
const now = new Date().toISOString().slice(0, 10); | ||
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] || []; | ||
groups[type].push(value); | ||
} | ||
return groups; | ||
} | ||
function reachedDate(past, now) { | ||
return Date.parse(past) < Date.parse(now); | ||
@@ -195,3 +214,4 @@ } | ||
function tryToCoerceVersion(rawVersion) { | ||
/* istanbul ignore if: version in `package.json` and comment can't be empty */ | ||
// `version` in `package.json` and comment can't be empty | ||
/* c8 ignore next 3 */ | ||
if (!rawVersion) { | ||
@@ -210,3 +230,3 @@ return false; | ||
'~', | ||
'^' | ||
'^', | ||
]; | ||
@@ -220,3 +240,4 @@ const foundTrailingNoise = leadingNoises.find(noise => version.startsWith(noise)); | ||
const parts = version.split(' '); | ||
/* istanbul ignore if: We don't have this `package.json` to test */ | ||
// We don't have this `package.json` to test | ||
/* c8 ignore next 3 */ | ||
if (parts.length > 1) { | ||
@@ -226,3 +247,4 @@ version = parts[0]; | ||
/* istanbul ignore if: We don't have this `package.json` to test */ | ||
// We don't have this `package.json` to test | ||
/* c8 ignore next 3 */ | ||
if (semver.valid(version)) { | ||
@@ -237,3 +259,4 @@ return version; | ||
} catch { | ||
/* istanbul ignore next: We don't have this `package.json` to test */ | ||
// We don't have this `package.json` to test | ||
/* c8 ignore next 3 */ | ||
return false; | ||
@@ -246,6 +269,7 @@ } | ||
'>': semver.gt, | ||
'>=': semver.gte | ||
'>=': semver.gte, | ||
}[operator]; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -257,10 +281,14 @@ const options = { | ||
allowWarningComments: true, | ||
...context.options[0] | ||
date: new Date().toISOString().slice(0, 10), | ||
...context.options[0], | ||
}; | ||
const ignoreRegexes = options.ignore.map( | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u') | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'), | ||
); | ||
const sourceCode = context.getSourceCode(); | ||
const dirname = path.dirname(context.filename); | ||
const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname); | ||
const {sourceCode} = context; | ||
const comments = sourceCode.getAllComments(); | ||
@@ -279,4 +307,4 @@ const unusedComments = comments | ||
...comment, | ||
value: line | ||
})) | ||
value: line, | ||
})), | ||
).filter(comment => processComment(comment)); | ||
@@ -287,13 +315,14 @@ | ||
// Since we have priority, we leave only the comments that we didn't use. | ||
const fakeContext = { | ||
...context, | ||
getSourceCode() { | ||
return { | ||
...sourceCode, | ||
getAllComments() { | ||
return options.allowWarningComments ? [] : unusedComments; | ||
} | ||
}; | ||
} | ||
}; | ||
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); | ||
@@ -321,3 +350,3 @@ | ||
engines = [], | ||
unknowns = [] | ||
unknowns = [], | ||
} = parsed; | ||
@@ -332,11 +361,11 @@ | ||
expirationDates: dates.join(', '), | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
} else if (dates.length === 1) { | ||
uses++; | ||
const [date] = dates; | ||
const [expirationDate] = dates; | ||
const shouldIgnore = options.ignoreDatesOnPullRequests && ci.isPR; | ||
if (!shouldIgnore && reachedDate(date)) { | ||
if (!shouldIgnore && reachedDate(expirationDate, options.date)) { | ||
context.report({ | ||
@@ -346,5 +375,5 @@ loc: comment.loc, | ||
data: { | ||
expirationDate: date, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
expirationDate, | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -363,4 +392,4 @@ } | ||
.join(', '), | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -381,4 +410,4 @@ } else if (packageVersions.length === 1) { | ||
comparison: `${condition}${version}`, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -397,6 +426,6 @@ } | ||
if (isInclusion) { | ||
const [trigger, messageId] = | ||
dependency.condition === 'in' ? | ||
[hasTargetPackage, MESSAGE_ID_HAVE_PACKAGE] : | ||
[!hasTargetPackage, MESSAGE_ID_DONT_HAVE_PACKAGE]; | ||
const [trigger, messageId] | ||
= dependency.condition === 'in' | ||
? [hasTargetPackage, MESSAGE_ID_HAVE_PACKAGE] | ||
: [!hasTargetPackage, MESSAGE_ID_DONT_HAVE_PACKAGE]; | ||
@@ -409,4 +438,4 @@ if (trigger) { | ||
package: dependency.name, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -421,3 +450,3 @@ } | ||
/* istanbul ignore if: Can't test in Node.js */ | ||
/* c8 ignore start */ | ||
if (!hasTargetPackage || !targetPackageVersion) { | ||
@@ -427,2 +456,3 @@ // Can't compare `¯\_(ツ)_/¯` | ||
} | ||
/* c8 ignore end */ | ||
@@ -437,4 +467,4 @@ const compare = semverComparisonForOperator(dependency.condition); | ||
comparison: `${dependency.name} ${dependency.condition} ${dependency.version}`, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -452,3 +482,3 @@ } | ||
/* istanbul ignore if: Can't test in this repo */ | ||
/* c8 ignore next 3 */ | ||
if (!hasTargetEngine) { | ||
@@ -460,3 +490,3 @@ continue; | ||
const targetPackageEngineVersion = tryToCoerceVersion( | ||
targetPackageRawEngineVersion | ||
targetPackageRawEngineVersion, | ||
); | ||
@@ -472,4 +502,4 @@ | ||
comparison: `node${engine.condition}${engine.version}`, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -487,3 +517,3 @@ } | ||
0, | ||
comparisonIndex | ||
comparisonIndex, | ||
)}@${unknown.slice(comparisonIndex)}`; | ||
@@ -499,4 +529,4 @@ | ||
fix: testString, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -507,3 +537,3 @@ continue; | ||
const withoutWhitespace = unknown.replace(/ /g, ''); | ||
const withoutWhitespace = unknown.replaceAll(' ', ''); | ||
@@ -518,4 +548,4 @@ if (parseArgument(withoutWhitespace).type !== 'unknowns') { | ||
fix: withoutWhitespace, | ||
message: parseTodoMessage(comment.value) | ||
} | ||
message: parseTodoMessage(comment.value), | ||
}, | ||
}); | ||
@@ -532,3 +562,3 @@ continue; | ||
rules.Program(); // eslint-disable-line new-cap | ||
} | ||
}, | ||
}; | ||
@@ -540,2 +570,3 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
@@ -545,22 +576,26 @@ terms: { | ||
items: { | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
uniqueItems: true, | ||
}, | ||
ignoreDatesOnPullRequests: { | ||
type: 'boolean', | ||
default: true | ||
default: true, | ||
}, | ||
allowWarningComments: { | ||
type: 'boolean', | ||
default: false | ||
} | ||
default: false, | ||
}, | ||
date: { | ||
type: 'string', | ||
format: 'date', | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -571,7 +606,7 @@ create, | ||
docs: { | ||
description: 'Add expiration conditions to TODO comments.' | ||
description: 'Add expiration conditions to TODO comments.', | ||
}, | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('eslint-utils'); | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const isLogicalExpression = require('./utils/is-logical-expression.js'); | ||
const {isBooleanNode, getBooleanAncestor} = require('./utils/boolean.js'); | ||
const {memberExpressionSelector} = require('./selectors/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isLiteral, isMemberExpression, isNumberLiteral} = require('./ast/index.js'); | ||
@@ -15,13 +15,13 @@ const TYPE_NON_ZERO = 'non-zero'; | ||
[TYPE_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is zero.', | ||
[MESSAGE_ID_SUGGESTION]: 'Replace `.{{property}}` with `.{{property}} {{code}}`.' | ||
[MESSAGE_ID_SUGGESTION]: 'Replace `.{{property}}` with `.{{property}} {{code}}`.', | ||
}; | ||
const isCompareRight = (node, operator, value) => | ||
node.type === 'BinaryExpression' && | ||
node.operator === operator && | ||
isLiteralValue(node.right, value); | ||
node.type === 'BinaryExpression' | ||
&& node.operator === operator | ||
&& isLiteral(node.right, value); | ||
const isCompareLeft = (node, operator, value) => | ||
node.type === 'BinaryExpression' && | ||
node.operator === operator && | ||
isLiteralValue(node.left, value); | ||
node.type === 'BinaryExpression' | ||
&& node.operator === operator | ||
&& isLiteral(node.left, value); | ||
const nonZeroStyles = new Map([ | ||
@@ -32,4 +32,4 @@ [ | ||
code: '> 0', | ||
test: node => isCompareRight(node, '>', 0) | ||
} | ||
test: node => isCompareRight(node, '>', 0), | ||
}, | ||
], | ||
@@ -40,20 +40,11 @@ [ | ||
code: '!== 0', | ||
test: node => isCompareRight(node, '!==', 0) | ||
} | ||
test: node => isCompareRight(node, '!==', 0), | ||
}, | ||
], | ||
[ | ||
'greater-than-or-equal', | ||
{ | ||
code: '>= 1', | ||
test: node => isCompareRight(node, '>=', 1) | ||
} | ||
] | ||
]); | ||
const zeroStyle = { | ||
code: '=== 0', | ||
test: node => isCompareRight(node, '===', 0) | ||
test: node => isCompareRight(node, '===', 0), | ||
}; | ||
const lengthSelector = memberExpressionSelector(['length', 'size']); | ||
function getLengthCheckNode(node) { | ||
@@ -65,13 +56,13 @@ node = node.parent; | ||
// `foo.length === 0` | ||
isCompareRight(node, '===', 0) || | ||
isCompareRight(node, '===', 0) | ||
// `foo.length == 0` | ||
isCompareRight(node, '==', 0) || | ||
|| isCompareRight(node, '==', 0) | ||
// `foo.length < 1` | ||
isCompareRight(node, '<', 1) || | ||
|| isCompareRight(node, '<', 1) | ||
// `0 === foo.length` | ||
isCompareLeft(node, '===', 0) || | ||
|| isCompareLeft(node, '===', 0) | ||
// `0 == foo.length` | ||
isCompareLeft(node, '==', 0) || | ||
|| isCompareLeft(node, '==', 0) | ||
// `1 > foo.length` | ||
isCompareLeft(node, '>', 1) | ||
|| isCompareLeft(node, '>', 1) | ||
) { | ||
@@ -84,17 +75,17 @@ return {isZeroLengthCheck: true, node}; | ||
// `foo.length !== 0` | ||
isCompareRight(node, '!==', 0) || | ||
isCompareRight(node, '!==', 0) | ||
// `foo.length != 0` | ||
isCompareRight(node, '!=', 0) || | ||
|| isCompareRight(node, '!=', 0) | ||
// `foo.length > 0` | ||
isCompareRight(node, '>', 0) || | ||
|| isCompareRight(node, '>', 0) | ||
// `foo.length >= 1` | ||
isCompareRight(node, '>=', 1) || | ||
|| isCompareRight(node, '>=', 1) | ||
// `0 !== foo.length` | ||
isCompareLeft(node, '!==', 0) || | ||
|| isCompareLeft(node, '!==', 0) | ||
// `0 !== foo.length` | ||
isCompareLeft(node, '!=', 0) || | ||
|| isCompareLeft(node, '!=', 0) | ||
// `0 < foo.length` | ||
isCompareLeft(node, '<', 0) || | ||
|| isCompareLeft(node, '<', 0) | ||
// `1 <= foo.length` | ||
isCompareLeft(node, '<=', 1) | ||
|| isCompareLeft(node, '<=', 1) | ||
) { | ||
@@ -107,9 +98,18 @@ return {isZeroLengthCheck: false, node}; | ||
function isNodeValueNumber(node, context) { | ||
if (isNumberLiteral(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] | ||
...context.options[0], | ||
}; | ||
const nonZeroStyle = nonZeroStyles.get(options['non-zero']); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -124,5 +124,5 @@ function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) { | ||
if ( | ||
!isParenthesized(node, sourceCode) && | ||
node.type === 'UnaryExpression' && | ||
node.parent.type === 'UnaryExpression' | ||
!isParenthesized(node, sourceCode) | ||
&& node.type === 'UnaryExpression' | ||
&& (node.parent.type === 'UnaryExpression' || node.parent.type === 'AwaitExpression') | ||
) { | ||
@@ -132,3 +132,6 @@ fixed = `(${fixed})`; | ||
const fix = fixer => fixer.replaceText(node, fixed); | ||
const fix = function * (fixer) { | ||
yield fixer.replaceText(node, fixed); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}; | ||
@@ -138,3 +141,3 @@ const problem = { | ||
messageId: isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO, | ||
data: {code, property: lengthNode.property.name} | ||
data: {code, property: lengthNode.property.name}, | ||
}; | ||
@@ -148,5 +151,4 @@ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: problem.data, | ||
fix | ||
} | ||
fix, | ||
}, | ||
]; | ||
@@ -159,8 +161,15 @@ } | ||
return { | ||
[lengthSelector](lengthNode) { | ||
if (lengthNode.object.type === 'ThisExpression') { | ||
MemberExpression(memberExpression) { | ||
if ( | ||
!isMemberExpression(memberExpression, { | ||
properties: ['length', 'size'], | ||
optional: false, | ||
}) | ||
|| memberExpression.object.type === 'ThisExpression' | ||
) { | ||
return; | ||
} | ||
const staticValue = getStaticValue(lengthNode, context.getScope()); | ||
const lengthNode = memberExpression; | ||
const staticValue = getStaticValue(lengthNode, sourceCode.getScope(lengthNode)); | ||
if (staticValue && (!Number.isInteger(staticValue.value) || staticValue.value < 0)) { | ||
@@ -185,3 +194,9 @@ // Ignore known, non-positive-integer length properties. | ||
node = ancestor; | ||
} else if (isLogicalExpression(lengthNode.parent)) { | ||
} else if ( | ||
isLogicalExpression(lengthNode.parent) | ||
&& !( | ||
lengthNode.parent.operator === '||' | ||
&& isNodeValueNumber(lengthNode.parent.right, context) | ||
) | ||
) { | ||
isZeroLengthCheck = isNegative; | ||
@@ -196,3 +211,3 @@ node = lengthNode; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -204,11 +219,13 @@ } | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
'non-zero': { | ||
enum: [...nonZeroStyles.keys()], | ||
default: 'greater-than' | ||
} | ||
} | ||
} | ||
default: 'greater-than', | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -219,3 +236,3 @@ create: checkVueTemplate(create), | ||
docs: { | ||
description: 'Enforce explicitly comparing the `length` or `size` property of a value.' | ||
description: 'Enforce explicitly comparing the `length` or `size` property of a value.', | ||
}, | ||
@@ -225,4 +242,4 @@ fixable: 'code', | ||
messages, | ||
hasSuggestions: true | ||
} | ||
hasSuggestions: true, | ||
}, | ||
}; |
'use strict'; | ||
const path = require('path'); | ||
const {camelCase, kebabCase, snakeCase, upperFirst} = require('lodash'); | ||
const path = require('node:path'); | ||
const {camelCase, kebabCase, snakeCase, upperFirst} = require('./utils/lodash.js'); | ||
const cartesianProductSamples = require('./utils/cartesian-product-samples.js'); | ||
@@ -10,3 +10,3 @@ | ||
[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}}`.' | ||
[MESSAGE_ID_EXTENSION]: 'File extension `{{extension}}` is not in lowercase. Rename it to `{{filename}}`.', | ||
}; | ||
@@ -18,3 +18,3 @@ | ||
const PLACEHOLDER_REGEX = new RegExp(PLACEHOLDER, 'i'); | ||
const isIgnoredChar = char => !/^[a-z\d-_$]$/i.test(char); | ||
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']); | ||
@@ -47,16 +47,16 @@ const isLowerCase = string => string === string.toLowerCase(); | ||
fn: camelCase, | ||
name: 'camel case' | ||
name: 'camel case', | ||
}, | ||
kebabCase: { | ||
fn: kebabCase, | ||
name: 'kebab case' | ||
name: 'kebab case', | ||
}, | ||
snakeCase: { | ||
fn: snakeCase, | ||
name: 'snake case' | ||
name: 'snake case', | ||
}, | ||
pascalCase: { | ||
fn: pascalCase, | ||
name: 'pascal case' | ||
} | ||
name: 'pascal case', | ||
}, | ||
}; | ||
@@ -96,3 +96,3 @@ | ||
const { | ||
samples: combinations | ||
samples: combinations, | ||
} = cartesianProductSamples(replacements); | ||
@@ -114,3 +114,3 @@ | ||
if (lastWord && lastWord.ignored === isIgnored) { | ||
if (lastWord?.ignored === isIgnored) { | ||
lastWord.word += char; | ||
@@ -120,3 +120,3 @@ } else { | ||
word: char, | ||
ignored: isIgnored | ||
ignored: isIgnored, | ||
}; | ||
@@ -129,3 +129,3 @@ words.push(lastWord); | ||
leading, | ||
words | ||
words, | ||
}; | ||
@@ -140,14 +140,5 @@ } | ||
*/ | ||
function englishishJoinWords(words) { | ||
if (words.length === 1) { | ||
return words[0]; | ||
} | ||
const englishishJoinWords = words => new Intl.ListFormat('en-US', {type: 'disjunction'}).format(words); | ||
if (words.length === 2) { | ||
return `${words[0]} or ${words[1]}`; | ||
} | ||
return `${words.slice(0, -1).join(', ')}, or ${words[words.length - 1]}`; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -164,6 +155,6 @@ const options = context.options[0] || {}; | ||
const chosenCasesFunctions = chosenCases.map(case_ => ignoreNumbers(cases[case_].fn)); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') { | ||
return {}; | ||
return; | ||
} | ||
@@ -189,3 +180,3 @@ | ||
messageId: MESSAGE_ID_EXTENSION, | ||
data: {filename: filename + extension.toLowerCase(), extension} | ||
data: {filename: filename + extension.toLowerCase(), extension}, | ||
}; | ||
@@ -199,3 +190,3 @@ } | ||
leading, | ||
extension | ||
extension, | ||
}); | ||
@@ -210,6 +201,6 @@ | ||
chosenCases: englishishJoinWords(chosenCases.map(x => cases[x].name)), | ||
renamedFilenames: englishishJoinWords(renamedFilenames.map(x => `\`${x}\``)) | ||
} | ||
renamedFilenames: englishishJoinWords(renamedFilenames.map(x => `\`${x}\``)), | ||
}, | ||
}; | ||
} | ||
}, | ||
}; | ||
@@ -228,11 +219,11 @@ }; | ||
'kebabCase', | ||
'pascalCase' | ||
] | ||
'pascalCase', | ||
], | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
additionalProperties: false, | ||
}, | ||
@@ -244,27 +235,28 @@ { | ||
camelCase: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
snakeCase: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
kebabCase: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
pascalCase: { | ||
type: 'boolean' | ||
} | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false | ||
additionalProperties: false, | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
] | ||
} | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -275,7 +267,7 @@ create, | ||
docs: { | ||
description: 'Enforce a case style for filenames.' | ||
description: 'Enforce a case style for filenames.', | ||
}, | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
@@ -7,3 +7,3 @@ function appendArgument(fixer, node, text, sourceCode) { | ||
// But parentheses of `NewExpression` could be omitted, add this check to prevent accident use on it | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (node.type !== 'CallExpression') { | ||
@@ -10,0 +10,0 @@ throw new Error(`Unexpected node "${node.type}".`); |
'use strict'; | ||
module.exports = { | ||
// Utilities | ||
extendFixRange: require('./extend-fix-range.js'), | ||
removeParentheses: require('./remove-parentheses.js'), | ||
appendArgument: require('./append-argument.js'), | ||
removeArgument: require('./remove-argument.js'), | ||
replaceArgument: require('./replace-argument.js'), | ||
switchNewExpressionToCallExpression: require('./switch-new-expression-to-call-expression.js'), | ||
switchCallExpressionToNewExpression: require('./switch-call-expression-to-new-expression.js'), | ||
removeMemberExpressionProperty: require('./remove-member-expression-property.js'), | ||
removeMethodCall: require('./remove-method-call.js') | ||
removeMethodCall: require('./remove-method-call.js'), | ||
replaceTemplateElement: require('./replace-template-element.js'), | ||
replaceReferenceIdentifier: require('./replace-reference-identifier.js'), | ||
renameVariable: require('./rename-variable.js'), | ||
replaceNodeOrTokenAndSpacesBefore: require('./replace-node-or-token-and-spaces-before.js'), | ||
removeSpacesAfter: require('./remove-spaces-after.js'), | ||
fixSpaceAroundKeyword: require('./fix-space-around-keywords.js'), | ||
replaceStringLiteral: require('./replace-string-literal.js'), | ||
addParenthesizesToReturnOrThrowExpression: require('./add-parenthesizes-to-return-or-throw-expression.js'), | ||
}; |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const {getParentheses} = require('../utils/parentheses.js'); | ||
@@ -10,3 +10,3 @@ | ||
const firstToken = parentheses[0] || node; | ||
const lastToken = parentheses[parentheses.length - 1] || node; | ||
const lastToken = parentheses.at(-1) || node; | ||
@@ -21,3 +21,3 @@ let [start] = firstToken.range; | ||
// If the removed argument is the only argument, the trailing comma must be removed too | ||
/* istanbul ignore next: Not reachable for now */ | ||
/* c8 ignore start */ | ||
if (callExpression.arguments.length === 1) { | ||
@@ -29,2 +29,3 @@ const tokenAfter = sourceCode.getTokenBefore(lastToken); | ||
} | ||
/* c8 ignore end */ | ||
@@ -31,0 +32,0 @@ return fixer.replaceTextRange([start, end], ''); |
'use strict'; | ||
const isNewExpressionWithParentheses = require('../utils/is-new-expression-with-parentheses.js'); | ||
const {isParenthesized} = require('../utils/parentheses.js'); | ||
const isOnSameLine = require('../utils/is-on-same-line.js'); | ||
const addParenthesizesToReturnOrThrowExpression = require('./add-parenthesizes-to-return-or-throw-expression.js'); | ||
const removeSpaceAfter = require('./remove-spaces-after.js'); | ||
function * fixReturnStatementArgument(newExpression, sourceCode, fixer) { | ||
const {parent} = newExpression; | ||
if ( | ||
parent.type !== 'ReturnStatement' || | ||
parent.argument !== newExpression || | ||
isParenthesized(newExpression, sourceCode) | ||
) { | ||
return; | ||
} | ||
function * switchNewExpressionToCallExpression(newExpression, sourceCode, fixer) { | ||
const newToken = sourceCode.getFirstToken(newExpression); | ||
yield fixer.remove(newToken); | ||
yield removeSpaceAfter(newToken, sourceCode, fixer); | ||
const returnStatement = parent; | ||
const returnToken = sourceCode.getFirstToken(returnStatement); | ||
const classNode = newExpression.callee; | ||
// 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 | ||
if (returnToken.loc.start.line === classNode.loc.start.line) { | ||
return; | ||
if (!isNewExpressionWithParentheses(newExpression, sourceCode)) { | ||
yield fixer.insertTextAfter(newExpression, '()'); | ||
} | ||
yield fixer.insertTextAfter(returnToken, ' ('); | ||
yield fixer.insertTextAfter(newExpression, ')'); | ||
} | ||
function * switchNewExpressionToCallExpression(node, sourceCode, fixer) { | ||
const [start] = node.range; | ||
let end = start + 3; // `3` = length of `new` | ||
const textAfter = sourceCode.text.slice(end); | ||
const [leadingSpaces] = textAfter.match(/^\s*/); | ||
end += leadingSpaces.length; | ||
yield fixer.removeRange([start, end]); | ||
if (!isNewExpressionWithParentheses(node, sourceCode)) { | ||
yield fixer.insertTextAfter(node, '()'); | ||
} | ||
/* | ||
@@ -51,5 +27,9 @@ Remove `new` from this code will makes the function return `undefined` | ||
*/ | ||
yield * fixReturnStatementArgument(node, sourceCode, fixer); | ||
if (!isOnSameLine(newToken, newExpression.callee) && !isParenthesized(newExpression, 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, sourceCode); | ||
} | ||
} | ||
module.exports = switchNewExpressionToCallExpression; |
'use strict'; | ||
const {defaultsDeep} = require('lodash'); | ||
const {getStringIfConstant} = require('eslint-utils'); | ||
const eslintTemplateVisitor = require('eslint-template-visitor'); | ||
const {callExpressionSelector} = require('./selectors/index.js'); | ||
const {defaultsDeep} = require('./utils/lodash.js'); | ||
const {getStringIfConstant} = require('@eslint-community/eslint-utils'); | ||
const {isCallExpression} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'importStyle'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.' | ||
[MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.', | ||
}; | ||
@@ -103,54 +102,26 @@ | ||
// An exotic custom parser or a bug in one could cover it too. | ||
/* istanbul ignore next */ | ||
/* c8 ignore next */ | ||
return []; | ||
}; | ||
const joinOr = words => { | ||
return words | ||
.map((word, index) => { | ||
if (index === words.length - 1) { | ||
return word; | ||
} | ||
const isAssignedDynamicImport = node => | ||
node.parent.type === 'AwaitExpression' | ||
&& node.parent.argument === node | ||
&& node.parent.parent.type === 'VariableDeclarator' | ||
&& node.parent.parent.init === node.parent; | ||
if (index === words.length - 2) { | ||
return word + ' or'; | ||
} | ||
return word + ','; | ||
}) | ||
.join(' '); | ||
}; | ||
// Keep this alphabetically sorted for easier maintenance | ||
const defaultStyles = { | ||
chalk: { | ||
default: true | ||
default: true, | ||
}, | ||
path: { | ||
default: true | ||
default: true, | ||
}, | ||
util: { | ||
named: true | ||
} | ||
named: true, | ||
}, | ||
}; | ||
const templates = eslintTemplateVisitor({ | ||
parserOptions: { | ||
sourceType: 'module', | ||
ecmaVersion: 2018 | ||
} | ||
}); | ||
const variableDeclarationVariable = templates.variableDeclarationVariable(); | ||
const assignmentTargetVariable = templates.variable(); | ||
const moduleNameVariable = templates.variable(); | ||
const assignedDynamicImportTemplate = templates.template`async () => { | ||
${variableDeclarationVariable} ${assignmentTargetVariable} = await import(${moduleNameVariable}); | ||
}`.narrow('BlockStatement > :has(AwaitExpression)'); | ||
const assignedRequireTemplate = templates.template` | ||
${variableDeclarationVariable} ${assignmentTargetVariable} = require(${moduleNameVariable}); | ||
`; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -164,9 +135,9 @@ let [ | ||
checkExportFrom = false, | ||
checkRequire = true | ||
} = {} | ||
checkRequire = true, | ||
} = {}, | ||
] = context.options; | ||
styles = extendDefaultStyles ? | ||
defaultsDeep({}, styles, defaultStyles) : | ||
styles; | ||
styles = extendDefaultStyles | ||
? defaultsDeep({}, styles, defaultStyles) | ||
: styles; | ||
@@ -176,6 +147,8 @@ styles = new Map( | ||
([moduleName, styles]) => | ||
[moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))] | ||
) | ||
[moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))], | ||
), | ||
); | ||
const {sourceCode} = context; | ||
const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => { | ||
@@ -191,3 +164,3 @@ if (!allowedImportStyles || allowedImportStyles.size === 0) { | ||
// 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 theese. | ||
// 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')) { | ||
@@ -203,4 +176,4 @@ effectiveAllowedImportStyles = new Set(allowedImportStyles); | ||
const data = { | ||
allowedStyles: joinOr([...allowedImportStyles.keys()]), | ||
moduleName | ||
allowedStyles: new Intl.ListFormat('en-US', {type: 'disjunction'}).format([...allowedImportStyles.keys()]), | ||
moduleName, | ||
}; | ||
@@ -211,161 +184,177 @@ | ||
messageId: MESSAGE_ID, | ||
data | ||
data, | ||
}); | ||
}; | ||
let visitor = {}; | ||
if (checkImport) { | ||
visitor = { | ||
...visitor, | ||
context.on('ImportDeclaration', node => { | ||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source)); | ||
ImportDeclaration(node) { | ||
const moduleName = getStringIfConstant(node.source, context.getScope()); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualImportDeclarationStyles(node); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualImportDeclarationStyles(node); | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
} | ||
}; | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}); | ||
} | ||
if (checkDynamicImport) { | ||
visitor = { | ||
...visitor, | ||
context.on('ImportExpression', node => { | ||
if (isAssignedDynamicImport(node)) { | ||
return; | ||
} | ||
'ExpressionStatement > ImportExpression'(node) { | ||
const moduleName = getStringIfConstant(node.source, context.getScope()); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = ['unassigned']; | ||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source)); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = ['unassigned']; | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}, | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}); | ||
[assignedDynamicImportTemplate](node) { | ||
const assignmentTargetNode = assignedDynamicImportTemplate.context.getMatch(assignmentTargetVariable); | ||
const moduleNameNode = assignedDynamicImportTemplate.context.getMatch(moduleNameVariable); | ||
const moduleName = getStringIfConstant(moduleNameNode, context.getScope()); | ||
context.on('VariableDeclarator', node => { | ||
if (!( | ||
node.init?.type === 'AwaitExpression' | ||
&& node.init.argument.type === 'ImportExpression' | ||
)) { | ||
return; | ||
} | ||
if (!moduleName) { | ||
return; | ||
} | ||
const assignmentTargetNode = node.id; | ||
const moduleNameNode = node.init.argument.source; | ||
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode)); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode); | ||
if (!moduleName) { | ||
return; | ||
} | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
} | ||
}; | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode); | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}); | ||
} | ||
if (checkExportFrom) { | ||
visitor = { | ||
...visitor, | ||
context.on('ExportAllDeclaration', node => { | ||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source)); | ||
ExportAllDeclaration(node) { | ||
const moduleName = getStringIfConstant(node.source, context.getScope()); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = ['namespace']; | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = ['namespace']; | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}); | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}, | ||
context.on('ExportNamedDeclaration', node => { | ||
const moduleName = getStringIfConstant(node.source, sourceCode.getScope(node.source)); | ||
ExportNamedDeclaration(node) { | ||
const moduleName = getStringIfConstant(node.source, context.getScope()); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualExportDeclarationStyles(node); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualExportDeclarationStyles(node); | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
} | ||
}; | ||
report(node, moduleName, actualImportStyles, allowedImportStyles); | ||
}); | ||
} | ||
if (checkRequire) { | ||
visitor = { | ||
...visitor, | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isCallExpression(node, { | ||
name: 'require', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& (node.parent.type === 'ExpressionStatement' && node.parent.expression === node) | ||
)) { | ||
return; | ||
} | ||
[`ExpressionStatement > ${callExpressionSelector({name: 'require', length: 1})}.expression`](node) { | ||
const moduleName = getStringIfConstant(node.arguments[0], context.getScope()); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = ['unassigned']; | ||
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); | ||
}, | ||
report(node, moduleName, actualImportStyles, allowedImportStyles, true); | ||
}); | ||
[assignedRequireTemplate](node) { | ||
const assignmentTargetNode = assignedRequireTemplate.context.getMatch(assignmentTargetVariable); | ||
const moduleNameNode = assignedRequireTemplate.context.getMatch(moduleNameVariable); | ||
const moduleName = getStringIfConstant(moduleNameNode, context.getScope()); | ||
context.on('VariableDeclarator', node => { | ||
if (!( | ||
node.init?.type === 'CallExpression' | ||
&& node.init.callee.type === 'Identifier' | ||
&& node.init.callee.name === 'require' | ||
)) { | ||
return; | ||
} | ||
if (!moduleName) { | ||
return; | ||
} | ||
const assignmentTargetNode = node.id; | ||
const moduleNameNode = node.init.arguments[0]; | ||
const moduleName = getStringIfConstant(moduleNameNode, sourceCode.getScope(moduleNameNode)); | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode); | ||
if (!moduleName) { | ||
return; | ||
} | ||
report(node, moduleName, actualImportStyles, allowedImportStyles, true); | ||
} | ||
}; | ||
const allowedImportStyles = styles.get(moduleName); | ||
const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode); | ||
report(node, moduleName, actualImportStyles, allowedImportStyles, true); | ||
}); | ||
} | ||
return templates.visitor(visitor); | ||
}; | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
checkImport: { | ||
type: 'boolean' | ||
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', | ||
}, | ||
}, | ||
checkDynamicImport: { | ||
type: 'boolean' | ||
}, | ||
], | ||
definitions: { | ||
moduleStyles: { | ||
type: 'object', | ||
additionalProperties: { | ||
$ref: '#/definitions/styles', | ||
}, | ||
checkExportFrom: { | ||
type: 'boolean' | ||
}, | ||
styles: { | ||
anyOf: [ | ||
{ | ||
enum: [ | ||
false, | ||
], | ||
}, | ||
{ | ||
$ref: '#/definitions/booleanObject', | ||
}, | ||
], | ||
}, | ||
booleanObject: { | ||
type: 'object', | ||
additionalProperties: { | ||
type: 'boolean', | ||
}, | ||
checkRequire: { | ||
type: 'boolean' | ||
}, | ||
extendDefaultStyles: { | ||
type: 'boolean' | ||
}, | ||
styles: { | ||
$ref: '#/items/0/definitions/moduleStyles' | ||
} | ||
}, | ||
additionalProperties: false, | ||
definitions: { | ||
moduleStyles: { | ||
type: 'object', | ||
additionalProperties: { | ||
$ref: '#/items/0/definitions/styles' | ||
} | ||
}, | ||
styles: { | ||
anyOf: [ | ||
{ | ||
enum: [ | ||
false | ||
] | ||
}, | ||
{ | ||
$ref: '#/items/0/definitions/booleanObject' | ||
} | ||
] | ||
}, | ||
booleanObject: { | ||
type: 'object', | ||
additionalProperties: { | ||
type: 'boolean' | ||
} | ||
} | ||
} | ||
} | ||
]; | ||
}, | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -376,7 +365,7 @@ create, | ||
docs: { | ||
description: 'Enforce specific import styles per module.' | ||
description: 'Enforce specific import styles per module.', | ||
}, | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {GlobalReferenceTracker} = require('./utils/global-reference-tracker.js'); | ||
const builtins = require('./utils/builtins.js'); | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const {callExpressionSelector, newExpressionSelector} = require('./selectors/index.js'); | ||
const {switchNewExpressionToCallExpression} = require('./fix/index.js'); | ||
const { | ||
switchCallExpressionToNewExpression, | ||
switchNewExpressionToCallExpression, | ||
} = require('./fix/index.js'); | ||
const messages = { | ||
enforce: 'Use `new {{name}}()` instead of `{{name}}()`.', | ||
disallow: 'Use `{{name}}()` instead of `new {{name}}()`.' | ||
disallow: 'Use `{{name}}()` instead of `new {{name}}()`.', | ||
}; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
function enforceNewExpression({node, path: [name]}, sourceCode) { | ||
if (name === 'Object') { | ||
const {parent} = node; | ||
if ( | ||
parent.type === 'BinaryExpression' | ||
&& (parent.operator === '===' || parent.operator === '!==') | ||
&& (parent.left === node || parent.right === node) | ||
) { | ||
return; | ||
} | ||
} | ||
return { | ||
[callExpressionSelector(builtins.enforceNew)]: node => { | ||
const {callee, parent} = node; | ||
if (isShadowed(context.getScope(), callee)) { | ||
return; | ||
} | ||
node, | ||
messageId: 'enforce', | ||
data: {name}, | ||
fix: fixer => switchCallExpressionToNewExpression(node, sourceCode, fixer), | ||
}; | ||
} | ||
const {name} = callee; | ||
function enforceCallExpression({node, path: [name]}, sourceCode) { | ||
const problem = { | ||
node, | ||
messageId: 'disallow', | ||
data: {name}, | ||
}; | ||
if ( | ||
name === 'Object' && | ||
parent && | ||
parent.type === 'BinaryExpression' && | ||
(parent.operator === '===' || parent.operator === '!==') && | ||
(parent.left === node || parent.right === node) | ||
) { | ||
return; | ||
} | ||
if (name !== 'String' && name !== 'Boolean' && name !== 'Number') { | ||
problem.fix = function * (fixer) { | ||
yield * switchNewExpressionToCallExpression(node, sourceCode, fixer); | ||
}; | ||
} | ||
return { | ||
node, | ||
messageId: 'enforce', | ||
data: {name}, | ||
fix: fixer => fixer.insertTextBefore(node, 'new ') | ||
}; | ||
}, | ||
[newExpressionSelector(builtins.disallowNew)]: node => { | ||
const {callee} = node; | ||
return problem; | ||
} | ||
if (isShadowed(context.getScope(), callee)) { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {sourceCode} = context; | ||
const newExpressionTracker = new GlobalReferenceTracker({ | ||
objects: builtins.disallowNew, | ||
type: GlobalReferenceTracker.CONSTRUCT, | ||
handle: reference => enforceCallExpression(reference, sourceCode), | ||
}); | ||
const callExpressionTracker = new GlobalReferenceTracker({ | ||
objects: builtins.enforceNew, | ||
type: GlobalReferenceTracker.CALL, | ||
handle: reference => enforceNewExpression(reference, sourceCode), | ||
}); | ||
const {name} = callee; | ||
const problem = { | ||
node, | ||
messageId: 'disallow', | ||
data: {name} | ||
}; | ||
return { | ||
* 'Program:exit'(program) { | ||
const scope = sourceCode.getScope(program); | ||
if (name !== 'String' && name !== 'Boolean' && name !== 'Number') { | ||
problem.fix = function * (fixer) { | ||
yield * switchNewExpressionToCallExpression(node, sourceCode, fixer); | ||
}; | ||
} | ||
return problem; | ||
} | ||
yield * newExpressionTracker.track(scope); | ||
yield * callExpressionTracker.track(scope); | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -71,7 +80,7 @@ create, | ||
docs: { | ||
description: 'Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`.' | ||
description: 'Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Specify the rules you want to disable.' | ||
[MESSAGE_ID]: 'Specify the rules you want to disable.', | ||
}; | ||
@@ -11,2 +11,3 @@ | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
@@ -19,4 +20,4 @@ * Program(node) { | ||
if ( | ||
result && // It's a eslint-disable comment | ||
!result.groups.ruleId // But it did not specify any rules | ||
result // It's a eslint-disable comment | ||
&& !result.groups.ruleId // But it did not specify any rules | ||
) { | ||
@@ -29,13 +30,14 @@ yield { | ||
...comment.loc.start, | ||
column: -1 | ||
column: -1, | ||
}, | ||
end: comment.loc.end | ||
end: comment.loc.end, | ||
}, | ||
messageId: MESSAGE_ID | ||
messageId: MESSAGE_ID, | ||
}; | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -46,6 +48,6 @@ create, | ||
docs: { | ||
description: 'Enforce specifying rules to disable in `eslint-disable` comments.' | ||
description: 'Enforce specifying rules to disable in `eslint-disable` comments.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isNodeMatches, isNodeValueNotFunction} = require('./utils/index.js'); | ||
@@ -14,58 +14,127 @@ const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name'; | ||
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.', | ||
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.' | ||
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.', | ||
}; | ||
const iteratorMethods = [ | ||
['every'], | ||
[ | ||
'filter', { | ||
extraSelector: '[callee.object.name!="Vue"]' | ||
const isAwaitExpressionArgument = node => node.parent.type === 'AwaitExpression' && node.parent.argument === node; | ||
const iteratorMethods = new Map([ | ||
{ | ||
method: 'every', | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
{ | ||
method: 'filter', | ||
test: 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', | ||
test: 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, | ||
test, | ||
}) => [method, { | ||
minParameters, | ||
parameters, | ||
returnsUndefined, | ||
test(node) { | ||
if ( | ||
method !== 'reduce' | ||
&& method !== 'reduceRight' | ||
&& isAwaitExpressionArgument(node) | ||
) { | ||
return false; | ||
} | ||
], | ||
['find'], | ||
['findIndex'], | ||
['flatMap'], | ||
[ | ||
'forEach', { | ||
returnsUndefined: true | ||
if (isNodeMatches(node.callee.object, ignoredCallee)) { | ||
return false; | ||
} | ||
], | ||
['map'], | ||
[ | ||
'reduce', { | ||
parameters: [ | ||
'accumulator', | ||
'element', | ||
'index', | ||
'array' | ||
], | ||
minParameters: 2, | ||
ignore: [] | ||
if (node.callee.object.type === 'CallExpression' && isNodeMatches(node.callee.object.callee, ignoredCallee)) { | ||
return false; | ||
} | ||
], | ||
[ | ||
'reduceRight', { | ||
parameters: [ | ||
'accumulator', | ||
'element', | ||
'index', | ||
'array' | ||
], | ||
minParameters: 2, | ||
ignore: [] | ||
const [callback] = node.arguments; | ||
if (callback.type === 'Identifier' && ignore.includes(callback.name)) { | ||
return false; | ||
} | ||
], | ||
['some'] | ||
].map(([method, options]) => { | ||
options = { | ||
parameters: ['element', 'index', 'array'], | ||
ignore: ['Boolean'], | ||
minParameters: 1, | ||
extraSelector: '', | ||
returnsUndefined: false, | ||
...options | ||
}; | ||
return [method, options]; | ||
}); | ||
return !test || test(node); | ||
}, | ||
}])); | ||
const ignoredCallee = [ | ||
@@ -81,3 +150,5 @@ // http://bluebirdjs.com/docs/api/promise.map.html | ||
'async', | ||
'this' | ||
'this', | ||
'$', | ||
'jQuery', | ||
]; | ||
@@ -90,6 +161,2 @@ | ||
if (type === 'Identifier' && options.ignore.includes(name)) { | ||
return; | ||
} | ||
const problem = { | ||
@@ -100,5 +167,5 @@ node, | ||
name, | ||
method | ||
method, | ||
}, | ||
suggest: [] | ||
suggest: [], | ||
}; | ||
@@ -114,6 +181,6 @@ | ||
name, | ||
parameters: suggestionParameters | ||
parameters: suggestionParameters, | ||
}, | ||
fix: fixer => { | ||
const sourceCode = context.getSourceCode(); | ||
fix(fixer) { | ||
const {sourceCode} = context; | ||
let nodeText = sourceCode.getText(node); | ||
@@ -126,7 +193,7 @@ if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') { | ||
node, | ||
returnsUndefined ? | ||
`(${suggestionParameters}) => { ${nodeText}(${suggestionParameters}); }` : | ||
`(${suggestionParameters}) => ${nodeText}(${suggestionParameters})` | ||
returnsUndefined | ||
? `(${suggestionParameters}) => { ${nodeText}(${suggestionParameters}); }` | ||
: `(${suggestionParameters}) => ${nodeText}(${suggestionParameters})`, | ||
); | ||
} | ||
}, | ||
}; | ||
@@ -140,39 +207,47 @@ | ||
const ignoredFirstArgumentSelector = [ | ||
notFunctionSelector('arguments.0'), | ||
// Ignore all `CallExpression`s include `function.bind()` | ||
'[arguments.0.type!="CallExpression"]', | ||
'[arguments.0.type!="FunctionExpression"]', | ||
'[arguments.0.type!="ArrowFunctionExpression"]' | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
computed: false, | ||
}) | ||
|| node.callee.property.type !== 'Identifier' | ||
) { | ||
return; | ||
} | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const rules = {}; | ||
const methodNode = node.callee.property; | ||
const methodName = methodNode.name; | ||
if (!iteratorMethods.has(methodName)) { | ||
return; | ||
} | ||
for (const [method, options] of iteratorMethods) { | ||
const selector = [ | ||
method === 'reduce' || method === 'reduceRight' ? '' : ':not(AwaitExpression) > ', | ||
methodCallSelector({ | ||
name: method, | ||
min: 1, | ||
max: 2 | ||
}), | ||
options.extraSelector, | ||
ignoredFirstArgumentSelector | ||
].join(''); | ||
const [callback] = node.arguments; | ||
rules[selector] = node => { | ||
if (isNodeMatches(node.callee.object, ignoredCallee)) { | ||
return; | ||
} | ||
if ( | ||
callback.type === 'FunctionExpression' | ||
|| callback.type === 'ArrowFunctionExpression' | ||
// Ignore all `CallExpression`s include `function.bind()` | ||
|| callback.type === 'CallExpression' | ||
|| isNodeValueNotFunction(callback) | ||
) { | ||
return; | ||
} | ||
const [iterator] = node.arguments; | ||
return getProblem(context, iterator, method, options, sourceCode); | ||
}; | ||
} | ||
const options = iteratorMethods.get(methodName); | ||
return rules; | ||
}; | ||
if (!options.test(node)) { | ||
return; | ||
} | ||
return getProblem(context, callback, methodName, options); | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -183,7 +258,7 @@ create, | ||
docs: { | ||
description: 'Prevent passing a function reference directly to iterator methods.' | ||
description: 'Prevent passing a function reference directly to iterator methods.', | ||
}, | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const { | ||
isParenthesized, | ||
isArrowToken, | ||
isCommaToken, | ||
isSemicolonToken, | ||
isClosingParenToken, | ||
findVariable | ||
} = require('eslint-utils'); | ||
const {methodCallSelector, referenceIdentifierSelector} = require('./selectors/index.js'); | ||
findVariable, | ||
hasSideEffect, | ||
} = require('@eslint-community/eslint-utils'); | ||
const {extendFixRange} = require('./fix/index.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const shouldAddParenthesesToExpressionStatementExpression = require('./utils/should-add-parentheses-to-expression-statement-expression.js'); | ||
const {getParentheses} = require('./utils/parentheses.js'); | ||
const extendFixRange = require('./utils/extend-fix-range.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {getParentheses, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const isFunctionSelfUsedInside = require('./utils/is-function-self-used-inside.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
const {fixSpaceAroundKeyword, removeParentheses} = require('./fix/index.js'); | ||
const {isArrowFunctionBody, isMethodCall, isReferenceIdentifier, functionTypes} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'no-array-for-each'; | ||
const MESSAGE_ID_ERROR = 'no-array-for-each/error'; | ||
const MESSAGE_ID_SUGGESTION = 'no-array-for-each/suggestion'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use `for…of` instead of `Array#forEach(…)`.' | ||
[MESSAGE_ID_ERROR]: 'Use `for…of` instead of `.forEach(…)`.', | ||
[MESSAGE_ID_SUGGESTION]: 'Switch to `for…of`.', | ||
}; | ||
const arrayForEachCallSelector = methodCallSelector({ | ||
name: 'forEach', | ||
includeOptionalCall: true, | ||
includeOptionalMember: true | ||
}); | ||
const continueAbleNodeTypes = new Set([ | ||
@@ -35,5 +33,10 @@ 'WhileStatement', | ||
'ForOfStatement', | ||
'ForInStatement' | ||
'ForInStatement', | ||
]); | ||
const stripChainExpression = node => | ||
(node.parent.type === 'ChainExpression' && node.parent.expression === node) | ||
? node.parent | ||
: node; | ||
function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction) { | ||
@@ -53,4 +56,5 @@ for (let node = returnStatement; node && node !== callbackFunction; node = node.parent) { | ||
switch (parent.type) { | ||
case 'IfStatement': | ||
case 'IfStatement': { | ||
return parent.consequent === returnStatement || parent.alternate === returnStatement; | ||
} | ||
@@ -63,7 +67,9 @@ // These parent's body need switch to `BlockStatement` too, but since they are "continueAble", won't fix | ||
// case 'DoWhileStatement': | ||
case 'WithStatement': | ||
case 'WithStatement': { | ||
return parent.body === returnStatement; | ||
} | ||
default: | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -73,26 +79,33 @@ } | ||
function getFixFunction(callExpression, functionInfo, context) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const [callback] = callExpression.arguments; | ||
const parameters = callback.params; | ||
const array = callExpression.callee.object; | ||
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 useEntries = parameters.length === 2; | ||
const shouldUseEntries = parameters.length === 2; | ||
let text = 'for ('; | ||
text += isFunctionParameterVariableReassigned(callback, context) ? 'let' : 'const'; | ||
text += isFunctionParameterVariableReassigned(callback, sourceCode) ? 'let' : 'const'; | ||
text += ' '; | ||
text += useEntries ? `[${indexText}, ${elementText}]` : elementText; | ||
text += shouldUseEntries ? `[${indexText}, ${elementText}]` : elementText; | ||
text += ' of '; | ||
let arrayText = sourceCode.getText(array); | ||
if (isParenthesized(array, sourceCode)) { | ||
arrayText = `(${arrayText})`; | ||
} | ||
const shouldAddParenthesesToObject | ||
= isParenthesized(iterableObject, sourceCode) | ||
|| ( | ||
// `1?.forEach()` -> `(1).entries()` | ||
isOptionalObject | ||
&& shouldUseEntries | ||
&& shouldAddParenthesesToMemberExpressionObject(iterableObject, sourceCode) | ||
); | ||
text += arrayText; | ||
text += shouldAddParenthesesToObject ? `(${objectText})` : objectText; | ||
if (useEntries) { | ||
if (shouldUseEntries) { | ||
text += '.entries()'; | ||
@@ -108,13 +121,3 @@ } | ||
const [start] = callExpression.range; | ||
let end; | ||
if (callback.body.type === 'BlockStatement') { | ||
end = callback.body.range[0]; | ||
} else { | ||
// In this case, parentheses are not included in body location, so we look for `=>` token | ||
// foo.forEach(bar => ({bar})) | ||
// ^ | ||
const arrowToken = sourceCode.getTokenBefore(callback.body, isArrowToken); | ||
end = arrowToken.range[1]; | ||
} | ||
const [end] = getParenthesizedRange(callback.body, sourceCode); | ||
return [start, end]; | ||
@@ -127,3 +130,3 @@ }; | ||
expected: 'return', | ||
ruleId: 'no-array-for-each' | ||
ruleId: 'no-array-for-each', | ||
}); | ||
@@ -143,5 +146,5 @@ | ||
let textAfter = ''; | ||
const shouldAddParentheses = | ||
!isParenthesized(returnStatement.argument, sourceCode) && | ||
shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument); | ||
const shouldAddParentheses | ||
= !isParenthesized(returnStatement.argument, sourceCode) | ||
&& shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument); | ||
if (shouldAddParentheses) { | ||
@@ -202,5 +205,10 @@ textBefore = `(${textBefore}`; | ||
return function * (fixer) { | ||
// `(( foo.forEach(bar => bar) ))` | ||
yield * removeParentheses(callExpression, fixer, sourceCode); | ||
// Replace these with `for (const … of …) ` | ||
// foo.forEach(bar => bar) | ||
// ^^^^^^^^^^^^^^^^^^ (space after `=>` didn't included) | ||
// ^^^^^^^^^^^^^^^^^^^^^^ | ||
// foo.forEach(bar => (bar)) | ||
// ^^^^^^^^^^^^^^^^^^^^^^ | ||
// foo.forEach(bar => {}) | ||
@@ -219,3 +227,3 @@ // ^^^^^^^^^^^^^^^^^^^^^^ | ||
penultimateToken, | ||
lastToken | ||
lastToken, | ||
] = sourceCode.getLastTokens(callExpression, 2); | ||
@@ -239,10 +247,21 @@ | ||
const expressionStatementLastToken = sourceCode.getLastToken(callExpression.parent); | ||
// Remove semicolon if it's not needed anymore | ||
// foo.forEach(bar => {}); | ||
// ^ | ||
if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) { | ||
yield fixer.remove(expressionStatementLastToken, 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, sourceCode); | ||
if (isOptionalObject) { | ||
yield fixer.insertTextBefore(callExpression, `if (${objectText}) `); | ||
} | ||
// Prevent possible variable conflicts | ||
@@ -263,4 +282,4 @@ yield * extendFixRange(fixer, callExpression.parent.range); | ||
function isFunctionParametersSafeToFix(callbackFunction, {context, scope, array, allIdentifiers}) { | ||
const variables = context.getDeclaredVariables(callbackFunction); | ||
function isFunctionParametersSafeToFix(callbackFunction, {sourceCode, scope, callExpression, allIdentifiers}) { | ||
const variables = sourceCode.getDeclaredVariables(callbackFunction); | ||
@@ -278,9 +297,9 @@ for (const variable of variables) { | ||
const variableName = definition.name.name; | ||
const [arrayStart, arrayEnd] = array.range; | ||
const [callExpressionStart, callExpressionEnd] = callExpression.range; | ||
for (const identifier of allIdentifiers) { | ||
const {name, range: [start, end]} = identifier; | ||
if ( | ||
name !== variableName || | ||
start < arrayStart || | ||
end > arrayEnd | ||
name !== variableName | ||
|| start < callExpressionStart | ||
|| end > callExpressionEnd | ||
) { | ||
@@ -300,38 +319,25 @@ continue; | ||
function isFunctionParameterVariableReassigned(callbackFunction, context) { | ||
return context.getDeclaredVariables(callbackFunction) | ||
function isFunctionParameterVariableReassigned(callbackFunction, sourceCode) { | ||
return sourceCode.getDeclaredVariables(callbackFunction) | ||
.filter(variable => variable.defs[0].type === 'Parameter') | ||
.some(variable => { | ||
const {references} = variable; | ||
return references.some(reference => { | ||
const node = reference.identifier; | ||
const {parent} = node; | ||
return parent.type === 'UpdateExpression' || | ||
(parent.type === 'AssignmentExpression' && parent.left === node); | ||
}); | ||
}); | ||
.some(variable => | ||
variable.references.some(reference => !reference.init && reference.isWrite()), | ||
); | ||
} | ||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, context}) { | ||
const sourceCode = context.getSourceCode(); | ||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceCode}) { | ||
// Check `CallExpression` | ||
if ( | ||
callExpression.optional || | ||
isParenthesized(callExpression, sourceCode) || | ||
callExpression.arguments.length !== 1 | ||
) { | ||
if (callExpression.optional || callExpression.arguments.length !== 1) { | ||
return false; | ||
} | ||
// Check `CallExpression.parent` | ||
if (callExpression.parent.type !== 'ExpressionStatement') { | ||
// Check ancestors, we only fix `ExpressionStatement` | ||
const callOrChainExpression = stripChainExpression(callExpression); | ||
if ( | ||
callOrChainExpression.parent.type !== 'ExpressionStatement' | ||
&& !isArrowFunctionBody(callOrChainExpression) | ||
) { | ||
return false; | ||
} | ||
// Check `CallExpression.callee` | ||
/* istanbul ignore next: Because of `ChainExpression` wrapper, `foo?.forEach()` is already failed on previous check, keep this just for safety */ | ||
if (callExpression.callee.optional) { | ||
return false; | ||
} | ||
// Check `CallExpression.arguments[0]`; | ||
@@ -341,5 +347,5 @@ const [callback] = callExpression.arguments; | ||
// Leave non-function type to `no-array-callback-reference` rule | ||
(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression') || | ||
callback.async || | ||
callback.generator | ||
(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression') | ||
|| callback.async | ||
|| callback.generator | ||
) { | ||
@@ -352,5 +358,9 @@ return false; | ||
if ( | ||
!(parameters.length === 1 || parameters.length === 2) || | ||
parameters.some(({type, typeAnnotation}) => type === 'RestElement' || typeAnnotation) || | ||
!isFunctionParametersSafeToFix(callback, {scope, array: callExpression, allIdentifiers, context}) | ||
!(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}) | ||
) { | ||
@@ -375,5 +385,9 @@ return false; | ||
'React.Children', | ||
'Children' | ||
'Children', | ||
'R', | ||
// https://www.npmjs.com/package/p-iteration | ||
'pIteration', | ||
]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -384,49 +398,82 @@ const functionStack = []; | ||
const functionInfo = new Map(); | ||
const {sourceCode} = context; | ||
return { | ||
':function'(node) { | ||
functionStack.push(node); | ||
functionInfo.set(node, { | ||
returnStatements: [], | ||
scope: context.getScope() | ||
}); | ||
}, | ||
':function:exit'() { | ||
functionStack.pop(); | ||
}, | ||
[referenceIdentifierSelector()](node) { | ||
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); | ||
}, | ||
':function ReturnStatement'(node) { | ||
const currentFunction = functionStack[functionStack.length - 1]; | ||
const {returnStatements} = functionInfo.get(currentFunction); | ||
returnStatements.push(node); | ||
}, | ||
[arrayForEachCallSelector](node) { | ||
if (isNodeMatches(node.callee.object, ignoredObjects)) { | ||
return; | ||
} | ||
} | ||
}); | ||
callExpressions.push({ | ||
node, | ||
scope: context.getScope() | ||
}); | ||
}, | ||
* 'Program:exit'() { | ||
for (const {node, scope} of callExpressions) { | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: MESSAGE_ID | ||
}; | ||
context.on('ReturnStatement', node => { | ||
const currentFunction = functionStack.at(-1); | ||
if (!currentFunction) { | ||
return; | ||
} | ||
if (isFixable(node, {scope, allIdentifiers, functionInfo, context})) { | ||
problem.fix = getFixFunction(node, functionInfo, context); | ||
} | ||
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} */ | ||
module.exports = { | ||
@@ -437,7 +484,8 @@ create, | ||
docs: { | ||
description: 'Prefer `for…of` over `Array#forEach(…)`.' | ||
description: 'Prefer `for…of` over the `forEach` method.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
hasSuggestions: true, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {hasSideEffect} = require('eslint-utils'); | ||
const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js'); | ||
const {hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const {removeArgument} = require('./fix/index.js'); | ||
@@ -8,2 +7,4 @@ const {getParentheses, getParenthesizedText} = require('./utils/parentheses.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {isNodeValueNotFunction} = require('./utils/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -16,3 +17,3 @@ const ERROR = 'error'; | ||
[SUGGESTION_REMOVE]: 'Remove the second argument.', | ||
[SUGGESTION_BIND]: 'Use a bound function.' | ||
[SUGGESTION_BIND]: 'Use a bound function.', | ||
}; | ||
@@ -29,2 +30,3 @@ | ||
'Vue.filter', | ||
'R.filter', | ||
@@ -34,7 +36,19 @@ 'lodash.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', | ||
@@ -47,2 +61,3 @@ '_.flatMap', | ||
'Children.forEach', | ||
'R.forEach', | ||
@@ -56,25 +71,9 @@ 'lodash.map', | ||
'$.map', | ||
'R.map', | ||
'lodash.some', | ||
'_.some', | ||
'underscore.some' | ||
'underscore.some', | ||
]; | ||
const selector = [ | ||
methodCallSelector({ | ||
names: [ | ||
'every', | ||
'filter', | ||
'find', | ||
'findIndex', | ||
'flatMap', | ||
'forEach', | ||
'map', | ||
'some' | ||
], | ||
length: 2 | ||
}), | ||
notFunctionSelector('arguments.0') | ||
].join(''); | ||
function removeThisArgument(callExpression, sourceCode) { | ||
@@ -92,8 +91,8 @@ return fixer => removeArgument(fixer, callExpression.arguments[1], sourceCode); | ||
const isParenthesized = callbackParentheses.length > 0; | ||
const callbackLastToken = isParenthesized ? | ||
callbackParentheses[callbackParentheses.length - 1] : | ||
callback; | ||
const callbackLastToken = isParenthesized | ||
? callbackParentheses.at(-1) | ||
: callback; | ||
if ( | ||
!isParenthesized && | ||
shouldAddParenthesesToMemberExpressionObject(callback, sourceCode) | ||
!isParenthesized | ||
&& shouldAddParenthesesToMemberExpressionObject(callback, sourceCode) | ||
) { | ||
@@ -112,11 +111,31 @@ yield fixer.insertTextBefore(callbackLastToken, '('); | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[selector](callExpression) { | ||
const {callee} = callExpression; | ||
if (isNodeMatches(callee, ignored)) { | ||
CallExpression(callExpression) { | ||
if ( | ||
!isMethodCall(callExpression, { | ||
methods: [ | ||
'every', | ||
'filter', | ||
'find', | ||
'findLast', | ||
'findIndex', | ||
'findLastIndex', | ||
'flatMap', | ||
'forEach', | ||
'map', | ||
'some', | ||
], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isNodeMatches(callExpression.callee, ignored) | ||
|| isNodeValueNotFunction(callExpression.arguments[0]) | ||
) { | ||
return; | ||
} | ||
const {callee} = callExpression; | ||
const method = callee.property.name; | ||
@@ -128,3 +147,3 @@ const [callback, thisArgument] = callExpression.arguments; | ||
messageId: ERROR, | ||
data: {method} | ||
data: {method}, | ||
}; | ||
@@ -140,4 +159,4 @@ | ||
messageId: SUGGESTION_REMOVE, | ||
fix: removeThisArgument(callExpression, sourceCode) | ||
} | ||
fix: removeThisArgument(callExpression, sourceCode), | ||
}, | ||
]; | ||
@@ -154,15 +173,16 @@ } else { | ||
messageId: SUGGESTION_REMOVE, | ||
fix: removeThisArgument(callExpression, sourceCode) | ||
fix: removeThisArgument(callExpression, sourceCode), | ||
}, | ||
{ | ||
messageId: SUGGESTION_BIND, | ||
fix: useBoundFunction(callExpression, sourceCode) | ||
} | ||
fix: useBoundFunction(callExpression, sourceCode), | ||
}, | ||
]; | ||
return problem; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -173,8 +193,8 @@ create, | ||
docs: { | ||
description: 'Disallow using the `this` argument in array methods.' | ||
description: 'Disallow using the `this` argument in array methods.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {hasSideEffect, isCommaToken, isSemicolonToken} = require('eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {hasSideEffect, isCommaToken, isSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const getCallExpressionArgumentsText = require('./utils/get-call-expression-arguments-text.js'); | ||
const isSameReference = require('./utils/is-same-reference.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const getPreviousNode = require('./utils/get-previous-node.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -12,29 +13,20 @@ const ERROR = 'error'; | ||
[ERROR]: 'Do not call `Array#push()` multiple times.', | ||
[SUGGESTION]: 'Merge with previous one.' | ||
[SUGGESTION]: 'Merge with previous one.', | ||
}; | ||
const arrayPushExpressionStatement = [ | ||
'ExpressionStatement', | ||
methodCallSelector({path: 'expression', name: 'push'}) | ||
].join(''); | ||
const isArrayPushCall = node => | ||
node | ||
&& node.parent.type === 'ExpressionStatement' | ||
&& node.parent.expression === node | ||
&& isMethodCall(node, { | ||
method: 'push', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}); | ||
const selector = `${arrayPushExpressionStatement} + ${arrayPushExpressionStatement}`; | ||
function getFirstExpression(node, sourceCode) { | ||
const {parent} = node; | ||
const visitorKeys = sourceCode.visitorKeys[parent.type] || Object.keys(parent); | ||
for (const property of visitorKeys) { | ||
const value = parent[property]; | ||
if (Array.isArray(value)) { | ||
const index = value.indexOf(node); | ||
if (index !== -1) { | ||
return value[index - 1]; | ||
} | ||
} | ||
function getFirstArrayPushCall(secondCall, sourceCode) { | ||
const firstCall = getPreviousNode(secondCall.parent, sourceCode)?.expression; | ||
if (isArrayPushCall(firstCall)) { | ||
return firstCall; | ||
} | ||
/* istanbul ignore next */ | ||
throw new Error('Cannot find the first `Array#push()` call.\nPlease open an issue at https://github.com/sindresorhus/eslint-plugin-unicorn/issues/new?title=%60no-array-push-push%60%3A%20Cannot%20find%20first%20%60push()%60'); | ||
} | ||
@@ -45,10 +37,21 @@ | ||
ignore: [], | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
const ignoredObjects = ['stream', 'this', 'this.stream', ...ignore]; | ||
const sourceCode = context.getSourceCode(); | ||
const ignoredObjects = [ | ||
'stream', | ||
'this', | ||
'this.stream', | ||
'process.stdin', | ||
'process.stdout', | ||
'process.stderr', | ||
...ignore, | ||
]; | ||
const {sourceCode} = context; | ||
return { | ||
[selector](secondExpression) { | ||
const secondCall = secondExpression.expression; | ||
CallExpression(secondCall) { | ||
if (!isArrayPushCall(secondCall)) { | ||
return; | ||
} | ||
const secondCallArray = secondCall.callee.object; | ||
@@ -60,4 +63,7 @@ | ||
const firstExpression = getFirstExpression(secondExpression, sourceCode); | ||
const firstCall = firstExpression.expression; | ||
const firstCall = getFirstArrayPushCall(secondCall, sourceCode); | ||
if (!firstCall) { | ||
return; | ||
} | ||
const firstCallArray = firstCall.callee.object; | ||
@@ -73,3 +79,3 @@ | ||
node: secondCall.callee.property, | ||
messageId: ERROR | ||
messageId: ERROR, | ||
}; | ||
@@ -83,14 +89,16 @@ | ||
yield ( | ||
isCommaToken(penultimateToken) ? | ||
fixer.insertTextAfter(penultimateToken, ` ${text}`) : | ||
fixer.insertTextBefore(lastToken, firstCall.arguments.length > 0 ? `, ${text}` : text) | ||
isCommaToken(penultimateToken) | ||
? fixer.insertTextAfter(penultimateToken, ` ${text}`) | ||
: fixer.insertTextBefore(lastToken, firstCall.arguments.length > 0 ? `, ${text}` : text) | ||
); | ||
} | ||
const shouldKeepSemicolon = !isSemicolonToken(sourceCode.getLastToken(firstExpression)) && | ||
isSemicolonToken(sourceCode.getLastToken(secondExpression)); | ||
const firstExpression = firstCall.parent; | ||
const secondExpression = secondCall.parent; | ||
const shouldKeepSemicolon = !isSemicolonToken(sourceCode.getLastToken(firstExpression)) | ||
&& isSemicolonToken(sourceCode.getLastToken(secondExpression)); | ||
yield fixer.replaceTextRange( | ||
[firstExpression.range[1], secondExpression.range[1]], | ||
shouldKeepSemicolon ? ';' : '' | ||
shouldKeepSemicolon ? ';' : '', | ||
); | ||
@@ -103,4 +111,4 @@ }; | ||
messageId: SUGGESTION, | ||
fix | ||
} | ||
fix, | ||
}, | ||
]; | ||
@@ -112,3 +120,3 @@ } else { | ||
return problem; | ||
} | ||
}, | ||
}; | ||
@@ -120,12 +128,13 @@ } | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -136,9 +145,9 @@ create, | ||
docs: { | ||
description: 'Enforce combining multiple `Array#push()` into one call.' | ||
description: 'Enforce combining multiple `Array#push()` into one call.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {arrayPrototypeMethodSelector, notFunctionSelector, matches} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isNodeValueNotFunction, isArrayPrototypeProperty} = require('./utils/index.js'); | ||
const MESSAGE_ID = 'no-reduce'; | ||
const messages = { | ||
[MESSAGE_ID]: '`Array#{{method}}()` is not allowed' | ||
[MESSAGE_ID]: '`Array#{{method}}()` is not allowed', | ||
}; | ||
const prototypeSelector = method => [ | ||
methodCallSelector(method), | ||
arrayPrototypeMethodSelector({ | ||
path: 'callee.object', | ||
names: ['reduce', 'reduceRight'] | ||
}) | ||
].join(''); | ||
const selector = matches([ | ||
const cases = [ | ||
// `array.{reduce,reduceRight}()` | ||
[ | ||
methodCallSelector({names: ['reduce', 'reduceRight'], min: 1, max: 2}), | ||
notFunctionSelector('arguments.0'), | ||
' > .callee > .property' | ||
].join(''), | ||
{ | ||
test: callExpression => | ||
isMethodCall(callExpression, { | ||
methods: ['reduce', 'reduceRight'], | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: 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()` | ||
[ | ||
prototypeSelector('call'), | ||
notFunctionSelector('arguments.1'), | ||
' > .callee > .object > .property' | ||
].join(''), | ||
{ | ||
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()` | ||
[ | ||
prototypeSelector('apply'), | ||
' > .callee > .object > .property' | ||
].join('') | ||
]); | ||
{ | ||
test: callExpression => | ||
isMethodCall(callExpression, { | ||
method: 'apply', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayPrototypeProperty(callExpression.callee.object, { | ||
properties: ['reduce', 'reduceRight'], | ||
}), | ||
getMethodNode: callExpression => callExpression.callee.object.property, | ||
}, | ||
]; | ||
const create = () => { | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
allowSimpleOperations: { | ||
type: 'boolean', | ||
default: true, | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {allowSimpleOperations} = {allowSimpleOperations: true, ...context.options[0]}; | ||
return { | ||
[selector](node) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {method: node.name} | ||
}; | ||
} | ||
* 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: MESSAGE_ID, | ||
data: {method: methodNode.name}, | ||
}; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -54,6 +121,7 @@ create, | ||
docs: { | ||
description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.' | ||
description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.', | ||
}, | ||
messages | ||
} | ||
schema, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
const {isStringLiteral, isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'no-console-spaces'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not use {{position}} space between `console.{{method}}` parameters.' | ||
[MESSAGE_ID]: 'Do not use {{position}} space between `console.{{method}}` parameters.', | ||
}; | ||
const methods = [ | ||
'log', | ||
'debug', | ||
'info', | ||
'warn', | ||
'error' | ||
]; | ||
const selector = methodCallSelector({ | ||
names: methods, | ||
min: 1, | ||
object: 'console' | ||
}); | ||
// Find exactly one leading space, allow exactly one space | ||
@@ -28,10 +14,11 @@ 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.charAt(value.length - 1) === ' ' && value.charAt(value.length - 2) !== ' '; | ||
const hasTrailingSpace = value => value.length > 1 && value.at(-1) === ' ' && value.at(-2) !== ' '; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const getProblem = (node, method, position) => { | ||
const index = position === 'leading' ? | ||
node.range[0] + 1 : | ||
node.range[1] - 2; | ||
const index = position === 'leading' | ||
? node.range[0] + 1 | ||
: node.range[1] - 2; | ||
const range = [index, index + 1]; | ||
@@ -43,3 +30,3 @@ | ||
data: {method, position}, | ||
fix: fixer => fixer.removeRange(range) | ||
fix: fixer => fixer.removeRange(range), | ||
}; | ||
@@ -49,3 +36,21 @@ }; | ||
return { | ||
* [selector](node) { | ||
* 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; | ||
@@ -55,7 +60,3 @@ const {arguments: messages} = node; | ||
for (const [index, node] of messages.entries()) { | ||
const {type, value} = node; | ||
if ( | ||
!(type === 'Literal' && typeof value === 'string') && | ||
type !== 'TemplateLiteral' | ||
) { | ||
if (!isStringLiteral(node) && node.type !== 'TemplateLiteral') { | ||
continue; | ||
@@ -74,6 +75,7 @@ } | ||
} | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -84,7 +86,7 @@ create, | ||
docs: { | ||
description: 'Do not use leading/trailing space between `console.log` parameters.' | ||
description: 'Do not use leading/trailing space between `console.log` parameters.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const getPropertyName = require('./utils/get-property-name.js'); | ||
const {GlobalReferenceTracker} = require('./utils/global-reference-tracker.js'); | ||
const MESSAGE_ID = 'no-document-cookie'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not use `document.cookie` directly.' | ||
[MESSAGE_ID]: 'Do not use `document.cookie` directly.', | ||
}; | ||
const selector = [ | ||
'AssignmentExpression', | ||
'>', | ||
'MemberExpression.left', | ||
'[object.type="Identifier"]', | ||
'[object.name="document"]' | ||
].join(''); | ||
const tracker = new GlobalReferenceTracker({ | ||
object: 'document.cookie', | ||
filter: ({node}) => node.parent.type === 'AssignmentExpression' && node.parent.left === node, | ||
handle: ({node}) => ({node, messageId: MESSAGE_ID}), | ||
}); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
return { | ||
[selector](node) { | ||
if (getPropertyName(node, context.getScope()) !== 'cookie') { | ||
return; | ||
} | ||
return { | ||
node, | ||
messageId: MESSAGE_ID | ||
}; | ||
} | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
create: context => tracker.createListeners(context), | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Do not use `document.cookie` directly.' | ||
description: 'Do not use `document.cookie` directly.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isClosingParenToken, getStaticValue} = require('eslint-utils'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const {isClosingParenToken, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const getChildScopesRecursive = require('./utils/get-child-scopes-recursive.js'); | ||
const getScopes = require('./utils/get-scopes.js'); | ||
const singular = require('./utils/singular.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
const getReferences = require('./utils/get-references.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'no-for-loop'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.' | ||
[MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.', | ||
}; | ||
const defaultElementName = 'element'; | ||
const isLiteralZero = node => isLiteralValue(node, 0); | ||
const isLiteralOne = node => isLiteralValue(node, 1); | ||
const isLiteralZero = node => isLiteral(node, 0); | ||
const isLiteralOne = node => isLiteral(node, 1); | ||
const isIdentifierWithName = (node, name) => node && node.type === 'Identifier' && node.name === name; | ||
const isIdentifierWithName = (node, name) => node?.type === 'Identifier' && node.name === name; | ||
@@ -24,4 +25,4 @@ const getIndexIdentifierName = forStatement => { | ||
if ( | ||
!variableDeclaration || | ||
variableDeclaration.type !== 'VariableDeclaration' | ||
!variableDeclaration | ||
|| variableDeclaration.type !== 'VariableDeclaration' | ||
) { | ||
@@ -52,3 +53,3 @@ return; | ||
lesser: binaryExpression.left, | ||
greater: binaryExpression.right | ||
greater: binaryExpression.right, | ||
}; | ||
@@ -60,3 +61,3 @@ } | ||
lesser: binaryExpression.right, | ||
greater: binaryExpression.left | ||
greater: binaryExpression.left, | ||
}; | ||
@@ -84,4 +85,4 @@ } | ||
if ( | ||
greater.object.type !== 'Identifier' || | ||
greater.property.type !== 'Identifier' | ||
greater.object.type !== 'Identifier' | ||
|| greater.property.type !== 'Identifier' | ||
) { | ||
@@ -109,5 +110,5 @@ return; | ||
const isLiteralOnePlusIdentifierWithName = (node, identifierName) => { | ||
if (node && node.type === 'BinaryExpression' && node.operator === '+') { | ||
return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right)) || | ||
(isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left)); | ||
if (node?.type === 'BinaryExpression' && node.operator === '+') { | ||
return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right)) | ||
|| (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left)); | ||
} | ||
@@ -130,4 +131,4 @@ | ||
if ( | ||
update.type === 'AssignmentExpression' && | ||
isIdentifierWithName(update.left, indexIdentifierName) | ||
update.type === 'AssignmentExpression' | ||
&& isIdentifierWithName(update.left, indexIdentifierName) | ||
) { | ||
@@ -146,24 +147,22 @@ if (update.operator === '+=') { | ||
const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => { | ||
return arrayReferences.every(reference => { | ||
const node = reference.identifier.parent; | ||
const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => arrayReferences.every(reference => { | ||
const node = reference.identifier.parent; | ||
if (node.type !== 'MemberExpression') { | ||
return false; | ||
} | ||
if (node.type !== 'MemberExpression') { | ||
return false; | ||
} | ||
if (node.property.name !== indexIdentifierName) { | ||
return false; | ||
} | ||
if (node.property.name !== indexIdentifierName) { | ||
return false; | ||
} | ||
if ( | ||
node.parent.type === 'AssignmentExpression' && | ||
node.parent.left === node | ||
) { | ||
return false; | ||
} | ||
if ( | ||
node.parent.type === 'AssignmentExpression' | ||
&& node.parent.left === node | ||
) { | ||
return false; | ||
} | ||
return true; | ||
}); | ||
}; | ||
return true; | ||
}); | ||
@@ -181,3 +180,3 @@ const getRemovalRange = (node, sourceCode) => { | ||
sourceCode.getIndexFromLoc({line, column: 0}), | ||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}) | ||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}), | ||
] : declarationNode.range; | ||
@@ -191,3 +190,3 @@ } | ||
node.range[0], | ||
declarationNode.declarations[1].range[0] | ||
declarationNode.declarations[1].range[0], | ||
]; | ||
@@ -198,3 +197,3 @@ } | ||
declarationNode.declarations[index - 1].range[1], | ||
node.range[1] | ||
node.range[1], | ||
]; | ||
@@ -259,27 +258,20 @@ }; | ||
const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) => { | ||
return indexVariable.references | ||
const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) => | ||
indexVariable.references | ||
.filter(reference => scopeContains(bodyScope, reference.from)) | ||
.some(inBodyReference => inBodyReference.isWrite()); | ||
}; | ||
const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) => { | ||
return variables.some(variable => { | ||
return !variable.references.every(reference => { | ||
return scopeContains(forScope, reference.from) || | ||
nodeContains(forStatement, reference.identifier); | ||
}); | ||
}); | ||
}; | ||
const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) => | ||
variables.some( | ||
variable => !variable.references.every( | ||
reference => scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier), | ||
), | ||
); | ||
const getReferencesInChildScopes = (scope, name) => { | ||
const references = scope.references.filter(reference => reference.identifier.name === name); | ||
return [ | ||
...references, | ||
...scope.childScopes.flatMap(s => getReferencesInChildScopes(s, name)) | ||
]; | ||
}; | ||
const getReferencesInChildScopes = (scope, name) => | ||
getReferences(scope).filter(reference => reference.identifier.name === name); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager, text: sourceCodeText} = sourceCode; | ||
@@ -302,3 +294,3 @@ | ||
const scope = context.getScope(); | ||
const scope = sourceCode.getScope(node); | ||
const staticResult = getStaticValue(arrayIdentifier, scope); | ||
@@ -346,3 +338,3 @@ if (staticResult && !Array.isArray(staticResult.value)) { | ||
loc: toLocation([start, end], sourceCode), | ||
messageId: MESSAGE_ID | ||
messageId: MESSAGE_ID, | ||
}; | ||
@@ -359,4 +351,4 @@ | ||
}); | ||
const elementNode = elementReference && elementReference.identifier.parent.parent; | ||
const elementIdentifierName = elementNode && elementNode.id.name; | ||
const elementNode = elementReference?.identifier.parent.parent; | ||
const elementIdentifierName = elementNode?.id.name; | ||
const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope); | ||
@@ -371,4 +363,4 @@ | ||
const index = indexIdentifierName; | ||
const element = elementIdentifierName || | ||
avoidCapture(singular(arrayIdentifierName) || defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); | ||
const element = elementIdentifierName | ||
|| avoidCapture(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope)); | ||
const array = arrayIdentifierName; | ||
@@ -392,3 +384,3 @@ | ||
elementNode.id.typeAnnotation, | ||
-1 // Skip leading `:` | ||
-1, // Skip leading `:` | ||
).trim(); | ||
@@ -417,3 +409,3 @@ } else { | ||
node.init.range[0], | ||
node.update.range[1] | ||
node.update.range[1], | ||
], replacement); | ||
@@ -428,5 +420,5 @@ | ||
if (elementNode) { | ||
yield removeDeclaration ? | ||
fixer.removeRange(getRemovalRange(elementNode, sourceCode)) : | ||
fixer.replaceText(elementNode.init, element); | ||
yield removeDeclaration | ||
? fixer.removeRange(getRemovalRange(elementNode, sourceCode)) | ||
: fixer.replaceText(elementNode.init, element); | ||
} | ||
@@ -437,6 +429,7 @@ }; | ||
return problem; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -447,8 +440,8 @@ create, | ||
docs: { | ||
description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.' | ||
description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.', | ||
}, | ||
fixable: 'code', | ||
messages, | ||
hasSuggestion: true | ||
} | ||
hasSuggestion: true, | ||
}, | ||
}; |
'use strict'; | ||
const replaceTemplateElement = require('./utils/replace-template-element.js'); | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {isStringLiteral, isRegexLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'no-hex-escape'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use Unicode escapes instead of hexadecimal escapes.' | ||
[MESSAGE_ID]: 'Use Unicode escapes instead of hexadecimal escapes.', | ||
}; | ||
function checkEscape(context, node, value) { | ||
const fixedValue = value.replace(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00'); | ||
const fixedValue = value.replaceAll(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00'); | ||
@@ -17,5 +18,5 @@ if (value !== fixedValue) { | ||
fix: fixer => | ||
node.type === 'TemplateElement' ? | ||
replaceTemplateElement(fixer, node, fixedValue) : | ||
fixer.replaceText(node, fixedValue) | ||
node.type === 'TemplateElement' | ||
? replaceTemplateElement(fixer, node, fixedValue) | ||
: fixer.replaceText(node, fixedValue), | ||
}; | ||
@@ -25,15 +26,13 @@ } | ||
const create = context => { | ||
return { | ||
Literal: node => { | ||
if (node.regex || typeof node.value === 'string') { | ||
return checkEscape(context, node, node.raw); | ||
} | ||
}, | ||
TemplateElement: node => { | ||
return checkEscape(context, node, node.value.raw); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
Literal(node) { | ||
if (isStringLiteral(node) || isRegexLiteral(node)) { | ||
return checkEscape(context, node, node.raw); | ||
} | ||
}; | ||
}; | ||
}, | ||
TemplateElement: node => checkEscape(context, node, node.value.raw), | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -44,7 +43,7 @@ create, | ||
docs: { | ||
description: 'Enforce the use of Unicode escapes instead of hexadecimal escapes.' | ||
description: 'Enforce the use of Unicode escapes instead of hexadecimal escapes.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const replaceNodeOrTokenAndSpacesBefore = require('./utils/replace-node-or-token-and-spaces-before.js'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
@@ -9,49 +10,57 @@ const isInstanceofToken = token => token.value === 'instanceof' && token.type === 'Keyword'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use `Array.isArray()` instead of `instanceof Array`.' | ||
[MESSAGE_ID]: 'Use `Array.isArray()` instead of `instanceof Array`.', | ||
}; | ||
const selector = [ | ||
'BinaryExpression', | ||
'[operator="instanceof"]', | ||
'[right.type="Identifier"]', | ||
'[right.name="Array"]' | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[selector]: node => ({ | ||
node, | ||
messageId: MESSAGE_ID, | ||
* fix(fixer) { | ||
const {left, right} = node; | ||
BinaryExpression(node) { | ||
if (!( | ||
node.operator === 'instanceof' | ||
&& node.right.type === 'Identifier' | ||
&& node.right.name === 'Array' | ||
)) { | ||
return; | ||
} | ||
let leftStartNodeOrToken = left; | ||
let leftEndNodeOrToken = left; | ||
if (isParenthesized(left, sourceCode)) { | ||
leftStartNodeOrToken = sourceCode.getTokenBefore(left, isOpeningParenToken); | ||
leftEndNodeOrToken = sourceCode.getTokenAfter(left, isClosingParenToken); | ||
} | ||
const {left, right} = node; | ||
let tokenStore = sourceCode; | ||
let instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken); | ||
if (!instanceofToken && sourceCode.parserServices.getTemplateBodyTokenStore) { | ||
tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore(); | ||
instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken); | ||
} | ||
yield fixer.insertTextBefore(leftStartNodeOrToken, 'Array.isArray('); | ||
yield fixer.insertTextAfter(leftEndNodeOrToken, ')'); | ||
return { | ||
node: instanceofToken, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
const instanceofToken = sourceCode.getTokenAfter(left, isInstanceofToken); | ||
yield * replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, sourceCode); | ||
yield * replaceNodeOrTokenAndSpacesBefore(right, '', fixer, sourceCode); | ||
} | ||
}) | ||
const range = getParenthesizedRange(left, tokenStore); | ||
yield fixer.insertTextBeforeRange(range, 'Array.isArray('); | ||
yield fixer.insertTextAfterRange(range, ')'); | ||
yield * replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, sourceCode, tokenStore); | ||
yield * replaceNodeOrTokenAndSpacesBefore(right, '', fixer, sourceCode, tokenStore); | ||
}, | ||
}; | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
create: checkVueTemplate(create), | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Require `Array.isArray()` instead of `instanceof Array`.' | ||
description: 'Require `Array.isArray()` instead of `instanceof Array`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isShorthandPropertyAssignmentPatternLeft = require('./utils/is-shorthand-property-assignment-pattern-left.js'); | ||
const MESSAGE_ID = 'noKeywordPrefix'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not prefix identifiers with keyword `{{keyword}}`.' | ||
[MESSAGE_ID]: 'Do not prefix identifiers with keyword `{{keyword}}`.', | ||
}; | ||
@@ -11,13 +12,11 @@ | ||
checkProperties = true, | ||
onlyCamelCase = true | ||
} = {}) => { | ||
return { | ||
disallowedPrefixes: (disallowedPrefixes || [ | ||
'new', | ||
'class' | ||
]), | ||
checkProperties, | ||
onlyCamelCase | ||
}; | ||
}; | ||
onlyCamelCase = true, | ||
} = {}) => ({ | ||
disallowedPrefixes: (disallowedPrefixes || [ | ||
'new', | ||
'class', | ||
]), | ||
checkProperties, | ||
onlyCamelCase, | ||
}); | ||
@@ -44,6 +43,6 @@ function findKeywordPrefix(name, options) { | ||
} else if ( | ||
effectiveParent.type === 'AssignmentExpression' && | ||
Boolean(keyword) && | ||
(effectiveParent.right.type !== 'MemberExpression' || effectiveParent.left.type === 'MemberExpression') && | ||
effectiveParent.left.property.name === name | ||
effectiveParent.type === 'AssignmentExpression' | ||
&& Boolean(keyword) | ||
&& (effectiveParent.right.type !== 'MemberExpression' || effectiveParent.left.type === 'MemberExpression') | ||
&& effectiveParent.left.property.name === name | ||
) { | ||
@@ -58,3 +57,3 @@ report(node, keyword); | ||
/* istanbul ignore next: Can't find a case to cover this line */ | ||
/* c8 ignore next 3 */ | ||
if (parent.shorthand && parent.value.left && Boolean(keyword)) { | ||
@@ -70,3 +69,3 @@ report(node, keyword); | ||
// Prevent checking righthand side of destructured object | ||
// Prevent checking right hand side of destructured object | ||
if (parent.key === node && parent.value !== node) { | ||
@@ -103,4 +102,4 @@ return true; | ||
name: node.name, | ||
keyword | ||
} | ||
keyword, | ||
}, | ||
}); | ||
@@ -111,3 +110,3 @@ } | ||
return { | ||
Identifier: node => { | ||
Identifier(node) { | ||
const {name, parent} = node; | ||
@@ -120,6 +119,6 @@ const keyword = findKeywordPrefix(name, options); | ||
} else if ( | ||
parent.type === 'Property' || | ||
parent.type === 'AssignmentPattern' | ||
parent.type === 'Property' | ||
|| parent.type === 'AssignmentPattern' | ||
) { | ||
if (parent.parent && parent.parent.type === 'ObjectPattern') { | ||
if (parent.parent.type === 'ObjectPattern') { | ||
const finished = checkObjectPattern(report, node, options); | ||
@@ -139,5 +138,6 @@ if (finished) { | ||
if ( | ||
Boolean(keyword) && | ||
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) && | ||
!(parent.right === node) | ||
Boolean(keyword) | ||
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type) | ||
&& !(parent.right === node) | ||
&& !isShorthandPropertyAssignmentPatternLeft(node) | ||
) { | ||
@@ -152,11 +152,7 @@ report(node, keyword); | ||
'ImportNamespaceSpecifier', | ||
'ImportDefaultSpecifier' | ||
'ImportDefaultSpecifier', | ||
].includes(parent.type) | ||
) { | ||
// Report only if the local imported identifier is invalid | ||
if ( | ||
Boolean(keyword) && | ||
parent.local && | ||
parent.local.name === name | ||
) { | ||
if (Boolean(keyword) && parent.local?.name === name) { | ||
report(node, keyword); | ||
@@ -167,8 +163,8 @@ } | ||
} else if ( | ||
Boolean(keyword) && | ||
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) | ||
Boolean(keyword) | ||
&& !ALLOWED_PARENT_TYPES.has(effectiveParent.type) | ||
) { | ||
report(node, keyword); | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -180,2 +176,3 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
@@ -186,19 +183,19 @@ disallowedPrefixes: { | ||
{ | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
], | ||
minItems: 0, | ||
uniqueItems: true | ||
uniqueItems: true, | ||
}, | ||
checkProperties: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
onlyCamelCase: { | ||
type: 'boolean' | ||
} | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -209,7 +206,7 @@ create, | ||
docs: { | ||
description: 'Disallow identifiers starting with `new` or `class`.' | ||
description: 'Disallow identifiers starting with `new` or `class`.', | ||
}, | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, isNotSemicolonToken} = require('eslint-utils'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const removeSpacesAfter = require('./utils/remove-spaces-after.js'); | ||
const {matches} = require('./selectors/index.js'); | ||
const {isParenthesized, isNotSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const {needsSemicolon} = require('./utils/index.js'); | ||
const {removeSpacesAfter} = require('./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`.' | ||
[MESSAGE_ID]: 'Unexpected `if` as the only statement in a `if` block without `else`.', | ||
}; | ||
const ifStatementWithoutAlternate = 'IfStatement:not([alternate])'; | ||
const selector = matches([ | ||
// `if (a) { if (b) {} }` | ||
[ | ||
ifStatementWithoutAlternate, | ||
' > ', | ||
'BlockStatement.consequent', | ||
'[body.length=1]', | ||
' > ', | ||
`${ifStatementWithoutAlternate}.body` | ||
].join(''), | ||
const isIfStatementWithoutAlternate = node => node.type === 'IfStatement' && !node.alternate; | ||
// `if (a) if (b) {}` | ||
`${ifStatementWithoutAlternate} > ${ifStatementWithoutAlternate}.consequent` | ||
]); | ||
// 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' | ||
(node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??')) | ||
|| node.type === 'ConditionalExpression' | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'YieldExpression' | ||
|| node.type === 'SequenceExpression' | ||
); | ||
@@ -58,13 +43,13 @@ | ||
const outerIfStatement = ( | ||
innerIfStatement.parent.type === 'BlockStatement' ? | ||
innerIfStatement.parent : | ||
innerIfStatement | ||
innerIfStatement.parent.type === 'BlockStatement' | ||
? innerIfStatement.parent | ||
: innerIfStatement | ||
).parent; | ||
const outer = { | ||
...outerIfStatement, | ||
...getIfStatementTokens(outerIfStatement, sourceCode) | ||
...getIfStatementTokens(outerIfStatement, sourceCode), | ||
}; | ||
const inner = { | ||
...innerIfStatement, | ||
...getIfStatementTokens(innerIfStatement, sourceCode) | ||
...getIfStatementTokens(innerIfStatement, sourceCode), | ||
}; | ||
@@ -90,3 +75,3 @@ | ||
inner.closingParenthesisToken, | ||
`)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}` | ||
`)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}`, | ||
); | ||
@@ -100,4 +85,4 @@ | ||
if ( | ||
isParenthesized(test, sourceCode) || | ||
!needParenthesis(test) | ||
isParenthesized(test, sourceCode) | ||
|| !needParenthesis(test) | ||
) { | ||
@@ -118,3 +103,3 @@ yield fixer.remove(openingParenthesisToken); | ||
const nextToken = sourceCode.getTokenAfter(outer); | ||
if (needsSemicolon(lastToken, sourceCode, nextToken.value)) { | ||
if (nextToken && needsSemicolon(lastToken, sourceCode, nextToken.value)) { | ||
yield fixer.insertTextBefore(nextToken, ';'); | ||
@@ -127,16 +112,35 @@ } | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
return { | ||
[selector](node) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fix(node, sourceCode) | ||
}; | ||
/** @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.sourceCode), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -147,7 +151,7 @@ create, | ||
docs: { | ||
description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.' | ||
description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
@@ -8,30 +8,42 @@ const MESSAGE_ID_TOO_DEEP = 'too-deep'; | ||
[MESSAGE_ID_TOO_DEEP]: 'Do not nest ternary expressions.', | ||
[MESSAGE_ID_SHOULD_PARENTHESIZED]: 'Nest ternary expression should be parenthesized.' | ||
[MESSAGE_ID_SHOULD_PARENTHESIZED]: 'Nest ternary expression should be parenthesized.', | ||
}; | ||
const nestTernarySelector = level => `:not(ConditionalExpression)${' > ConditionalExpression'.repeat(level)}`; | ||
/** @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 create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const ancestors = sourceCode.getAncestors(node).reverse(); | ||
const nestLevel = ancestors.findIndex(node => node.type !== 'ConditionalExpression'); | ||
return { | ||
[nestTernarySelector(3)]: node => { | ||
// Nesting more than one level not allowed. | ||
return {node, messageId: MESSAGE_ID_TOO_DEEP}; | ||
}, | ||
[nestTernarySelector(2)]: node => { | ||
if (!isParenthesized(node, sourceCode)) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID_SHOULD_PARENTHESIZED, | ||
fix: fixer => [ | ||
fixer.insertTextBefore(node, '('), | ||
fixer.insertTextAfter(node, ')') | ||
] | ||
}; | ||
} | ||
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} */ | ||
module.exports = { | ||
@@ -42,7 +54,7 @@ create, | ||
docs: { | ||
description: 'Disallow nested ternary expressions.' | ||
description: 'Disallow nested ternary expressions.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('eslint-utils'); | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const isNumber = require('./utils/is-number.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
@@ -14,10 +15,19 @@ const MESSAGE_ID_ERROR = 'error'; | ||
[MESSAGE_ID_ONLY_ELEMENT]: 'The argument is the only element of array.', | ||
[MESSAGE_ID_SPREAD]: 'Spread the argument.' | ||
[MESSAGE_ID_SPREAD]: 'Spread the argument.', | ||
}; | ||
const newArraySelector = newExpressionSelector({name: 'Array', length: 1, allowSpreadElement: true}); | ||
function getProblem(context, node) { | ||
if ( | ||
!isNewExpression(node, { | ||
name: 'Array', | ||
argumentsLength: 1, | ||
allowSpreadElement: true, | ||
}) | ||
) { | ||
return; | ||
} | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_ERROR | ||
messageId: MESSAGE_ID_ERROR, | ||
}; | ||
@@ -27,3 +37,3 @@ | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
let text = sourceCode.getText(argumentNode); | ||
@@ -34,5 +44,5 @@ if (isParenthesized(argumentNode, sourceCode)) { | ||
const maybeSemiColon = needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[') ? | ||
';' : | ||
''; | ||
const maybeSemiColon = needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[') | ||
? ';' | ||
: ''; | ||
@@ -44,4 +54,4 @@ // We are not sure how many `arguments` passed | ||
messageId: MESSAGE_ID_SPREAD, | ||
fix: fixer => fixer.replaceText(node, `${maybeSemiColon}[${text}]`) | ||
} | ||
fix: fixer => fixer.replaceText(node, `${maybeSemiColon}[${text}]`), | ||
}, | ||
]; | ||
@@ -51,35 +61,38 @@ return problem; | ||
const result = getStaticValue(argumentNode, context.getScope()); | ||
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}]`; | ||
// We don't know the argument is number or not | ||
if (result === null) { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_LENGTH, | ||
fix: fixer => fixer.replaceText(node, fromLengthText) | ||
}, | ||
{ | ||
messageId: MESSAGE_ID_ONLY_ELEMENT, | ||
fix: fixer => fixer.replaceText(node, onlyElementText) | ||
} | ||
]; | ||
const result = getStaticValue(argumentNode, scope); | ||
if (result !== null && typeof result.value !== 'number') { | ||
problem.fix = fixer => fixer.replaceText(node, onlyElementText); | ||
return problem; | ||
} | ||
problem.fix = fixer => fixer.replaceText( | ||
node, | ||
typeof result.value === 'number' ? fromLengthText : onlyElementText | ||
); | ||
// 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 => ({ | ||
[newArraySelector](node) { | ||
NewExpression(node) { | ||
return getProblem(context, node); | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -90,8 +103,8 @@ create, | ||
docs: { | ||
description: 'Disallow `new Array()`.' | ||
description: 'Disallow `new Array()`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {switchNewExpressionToCallExpression} = require('./fix/index.js'); | ||
const isNumber = require('./utils/is-number.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
@@ -12,3 +13,3 @@ const ERROR = 'error'; | ||
[ERROR_UNKNOWN]: '`new Buffer()` is deprecated, use `Buffer.alloc()` or `Buffer.from()` instead.', | ||
[SUGGESTION]: 'Switch to `Buffer.{{method}}()`.' | ||
[SUGGESTION]: 'Switch to `Buffer.{{replacement}}()`.', | ||
}; | ||
@@ -30,12 +31,12 @@ | ||
if (isNumber(firstArgument, scope)) { | ||
return 'alloc'; | ||
} | ||
const staticResult = getStaticValue(firstArgument, scope); | ||
if (staticResult) { | ||
const {value} = staticResult; | ||
if (typeof value === 'number') { | ||
return 'alloc'; | ||
} | ||
if ( | ||
typeof value === 'string' || | ||
Array.isArray(value) | ||
typeof value === 'string' | ||
|| Array.isArray(value) | ||
) { | ||
@@ -54,8 +55,13 @@ return 'from'; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[newExpressionSelector('Buffer')]: node => { | ||
const method = inferMethod(node.arguments, context.getScope()); | ||
NewExpression(node) { | ||
if (!isNewExpression(node, {name: 'Buffer'})) { | ||
return; | ||
} | ||
const method = inferMethod(node.arguments, sourceCode.getScope(node)); | ||
if (method) { | ||
@@ -66,3 +72,3 @@ return { | ||
data: {method}, | ||
fix: fix(node, sourceCode, method) | ||
fix: fix(node, sourceCode, method), | ||
}; | ||
@@ -74,12 +80,13 @@ } | ||
messageId: ERROR_UNKNOWN, | ||
suggest: ['from', 'alloc'].map(method => ({ | ||
suggest: ['from', 'alloc'].map(replacement => ({ | ||
messageId: SUGGESTION, | ||
data: {method}, | ||
fix: fix(node, sourceCode, method) | ||
})) | ||
data: {replacement}, | ||
fix: fix(node, sourceCode, replacement), | ||
})), | ||
}; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -90,8 +97,8 @@ create, | ||
docs: { | ||
description: 'Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`.' | ||
description: 'Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const { | ||
not, | ||
matches, | ||
methodCallSelector, | ||
callExpressionSelector | ||
} = require('./selectors/index.js'); | ||
isMethodCall, | ||
isCallExpression, | ||
isLiteral, | ||
} = require('./ast/index.js'); | ||
@@ -15,63 +14,73 @@ const ERROR_MESSAGE_ID = 'error'; | ||
[SUGGESTION_REPLACE_MESSAGE_ID]: 'Replace `null` with `undefined`.', | ||
[SUGGESTION_REMOVE_MESSAGE_ID]: 'Remove `null`.' | ||
[SUGGESTION_REMOVE_MESSAGE_ID]: 'Remove `null`.', | ||
}; | ||
const objectCreateSelector = methodCallSelector({ | ||
object: 'Object', | ||
name: 'create', | ||
length: 1 | ||
}); | ||
// `useRef(null)` | ||
// eslint-disable-next-line unicorn/prevent-abbreviations | ||
const useRefSelector = callExpressionSelector({name: 'useRef', length: 1}); | ||
// `React.useRef(null)` | ||
// eslint-disable-next-line unicorn/prevent-abbreviations | ||
const reactUseRefSelector = methodCallSelector({ | ||
object: 'React', | ||
name: 'useRef', | ||
length: 1 | ||
}); | ||
const selector = [ | ||
'Literal', | ||
'[raw="null"]', | ||
not(`${matches([objectCreateSelector, useRefSelector, reactUseRefSelector])} > .arguments`) | ||
].join(''); | ||
const isLooseEqual = node => node.type === 'BinaryExpression' && ['==', '!='].includes(node.operator); | ||
const isStrictEqual = node => node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator); | ||
const isSecondArgumentOfInsertBefore = node => | ||
node.parent.type === 'CallExpression' && | ||
!node.parent.optional && | ||
node.parent.arguments.length === 2 && | ||
node.parent.arguments[0].type !== 'SpreadElement' && | ||
node.parent.arguments[1] === node && | ||
node.parent.callee.type === 'MemberExpression' && | ||
!node.parent.callee.computed && | ||
!node.parent.callee.optional && | ||
node.parent.callee.property.type === 'Identifier' && | ||
node.parent.callee.property.name === 'insertBefore'; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {checkStrictEquality} = { | ||
checkStrictEquality: false, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
return { | ||
[selector]: node => { | ||
const {parent} = node; | ||
if (!checkStrictEquality && isStrictEqual(parent)) { | ||
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, | ||
optionalMember: false, | ||
}) | ||
&& node.parent.arguments[1] === node | ||
) | ||
) { | ||
return; | ||
} | ||
if (isSecondArgumentOfInsertBefore(node)) { | ||
return; | ||
} | ||
const {parent} = node; | ||
const problem = { | ||
node, | ||
messageId: ERROR_MESSAGE_ID | ||
messageId: ERROR_MESSAGE_ID, | ||
}; | ||
@@ -88,3 +97,3 @@ | ||
messageId: SUGGESTION_REPLACE_MESSAGE_ID, | ||
fix: useUndefinedFix | ||
fix: useUndefinedFix, | ||
}; | ||
@@ -96,5 +105,5 @@ | ||
messageId: SUGGESTION_REMOVE_MESSAGE_ID, | ||
fix: fixer => fixer.remove(node) | ||
fix: fixer => fixer.remove(node), | ||
}, | ||
useUndefinedSuggestion | ||
useUndefinedSuggestion, | ||
]; | ||
@@ -108,5 +117,5 @@ return problem; | ||
messageId: SUGGESTION_REMOVE_MESSAGE_ID, | ||
fix: fixer => fixer.removeRange([parent.id.range[1], node.range[1]]) | ||
fix: fixer => fixer.removeRange([parent.id.range[1], node.range[1]]), | ||
}, | ||
useUndefinedSuggestion | ||
useUndefinedSuggestion, | ||
]; | ||
@@ -118,3 +127,3 @@ return problem; | ||
return problem; | ||
} | ||
}, | ||
}; | ||
@@ -126,12 +135,13 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkStrictEquality: { | ||
type: 'boolean', | ||
default: false | ||
} | ||
default: false, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -142,9 +152,9 @@ create, | ||
docs: { | ||
description: 'Disallow the use of the `null` literal.' | ||
description: 'Disallow the use of the `null` literal.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {isFunction} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'noObjectAsDefaultParameter'; | ||
const MESSAGE_ID_IDENTIFIER = 'identifier'; | ||
const MESSAGE_ID_NON_IDENTIFIER = 'non-identifier'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not use an object literal as default for parameter `{{parameter}}`.' | ||
[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.', | ||
}; | ||
const objectParameterSelector = [ | ||
':function > AssignmentPattern.params', | ||
'[left.type="Identifier"]', | ||
'[right.type="ObjectExpression"]', | ||
'[right.properties.length>0]' | ||
].join(''); | ||
/** @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 create = () => { | ||
return { | ||
[objectParameterSelector]: node => { | ||
const {left, right} = node; | ||
if (left.type === 'Identifier') { | ||
return { | ||
node: node.left, | ||
messageId: MESSAGE_ID, | ||
data: {parameter: node.left.name} | ||
node: left, | ||
messageId: MESSAGE_ID_IDENTIFIER, | ||
data: {parameter: left.name}, | ||
}; | ||
} | ||
}; | ||
}; | ||
return { | ||
node: right, | ||
messageId: MESSAGE_ID_NON_IDENTIFIER, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -32,6 +46,6 @@ create, | ||
docs: { | ||
description: 'Disallow the use of objects as default parameters.' | ||
description: 'Disallow the use of objects as default parameters.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector, STATIC_REQUIRE_SELECTOR} = require('./selectors/index.js'); | ||
const {isStaticRequire, isMethodCall, isLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'no-process-exit'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Only use `process.exit()` in CLI apps. Throw an error instead.' | ||
[MESSAGE_ID]: 'Only use `process.exit()` in CLI apps. Throw an error instead.', | ||
}; | ||
const importWorkerThreadsSelector = [ | ||
// `require('worker_threads')` | ||
[ | ||
STATIC_REQUIRE_SELECTOR, | ||
'[arguments.0.value="worker_threads"]' | ||
].join(''), | ||
// `import workerThreads from 'worker_threads'` | ||
[ | ||
'ImportDeclaration', | ||
'[source.type="Literal"]', | ||
'[source.value="worker_threads"]' | ||
].join('') | ||
].join(', '); | ||
const processOnOrOnceCallSelector = methodCallSelector({ | ||
object: 'process', | ||
names: ['on', 'once'], | ||
min: 1 | ||
}); | ||
const processExitCallSelector = methodCallSelector({ | ||
object: 'process', | ||
name: 'exit' | ||
}); | ||
const isWorkerThreads = node => | ||
isLiteral(node, 'node:worker_threads') | ||
|| isLiteral(node, 'worker_threads'); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const startsWithHashBang = context.getSourceCode().lines[0].indexOf('#!') === 0; | ||
const startsWithHashBang = context.sourceCode.lines[0].indexOf('#!') === 0; | ||
@@ -45,35 +27,70 @@ if (startsWithHashBang) { | ||
return { | ||
// Check `worker_threads` require / import | ||
[importWorkerThreadsSelector]: () => { | ||
// `require('worker_threads')` | ||
context.on('CallExpression', callExpression => { | ||
if ( | ||
isStaticRequire(callExpression) | ||
&& isWorkerThreads(callExpression.arguments[0]) | ||
) { | ||
requiredWorkerThreadsModule = true; | ||
}, | ||
// Check `process.on` / `process.once` call | ||
[processOnOrOnceCallSelector]: node => { | ||
} | ||
}); | ||
// `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; | ||
}, | ||
// Check `process.exit` call | ||
[processExitCallSelector]: node => { | ||
if (!processEventHandler) { | ||
problemNodes.push(node); | ||
} | ||
}, | ||
'CallExpression:exit': node => { | ||
if (node === processEventHandler) { | ||
processEventHandler = undefined; | ||
} | ||
}, | ||
* 'Program:exit'() { | ||
if (!requiredWorkerThreadsModule) { | ||
for (const node of problemNodes) { | ||
yield { | ||
node, | ||
messageId: MESSAGE_ID | ||
}; | ||
} | ||
} | ||
} | ||
}; | ||
}); | ||
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} */ | ||
module.exports = { | ||
@@ -84,6 +101,6 @@ create, | ||
docs: { | ||
description: 'Disallow `process.exit()`.' | ||
description: 'Disallow `process.exit()`.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isSemicolonToken} = require('eslint-utils'); | ||
const {isSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const getClassHeadLocation = require('./utils/get-class-head-location.js'); | ||
const removeSpacesAfter = require('./utils/remove-spaces-after.js'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
const {removeSpacesAfter} = require('./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.' | ||
[MESSAGE_ID]: 'Use an object instead of a class with only static members.', | ||
}; | ||
const selector = [ | ||
':matches(ClassDeclaration, ClassExpression)', | ||
':not([superClass], [decorators.length>0])', | ||
'[body.type="ClassBody"]', | ||
'[body.body.length>0]' | ||
].join(''); | ||
const isEqualToken = ({type, value}) => type === 'Punctuator' && value === '='; | ||
const isDeclarationOfExportDefaultDeclaration = node => | ||
node.type === 'ClassDeclaration' && | ||
node.parent.type === 'ExportDefaultDeclaration' && | ||
node.parent.declaration === node; | ||
node.type === 'ClassDeclaration' | ||
&& node.parent.type === 'ExportDefaultDeclaration' | ||
&& node.parent.declaration === node; | ||
// https://github.com/estree/estree/blob/master/stage3/class-features.md#propertydefinition | ||
const isPropertyDefinition = node => node.type === 'PropertyDefinition' || | ||
// Legacy node type | ||
node.type === 'ClassProperty'; | ||
const isPropertyDefinition = node => node.type === 'PropertyDefinition'; | ||
const isMethodDefinition = node => node.type === 'MethodDefinition'; | ||
@@ -39,7 +29,6 @@ | ||
decorators, | ||
key | ||
key, | ||
} = node; | ||
// Avoid matching unexpected node. For example: https://github.com/tc39/proposal-class-static-block | ||
/* istanbul ignore next */ | ||
if (!isPropertyDefinition(node) && !isMethodDefinition(node)) { | ||
@@ -49,3 +38,3 @@ return false; | ||
if (!isStatic || isPrivate) { | ||
if (!isStatic || isPrivate || key.type === 'PrivateIdentifier') { | ||
return false; | ||
@@ -56,7 +45,6 @@ } | ||
if ( | ||
isDeclare || | ||
isReadonly || | ||
typeof accessibility !== 'undefined' || | ||
(Array.isArray(decorators) && decorators.length > 0) || | ||
key.type === 'TSPrivateIdentifier' | ||
isDeclare | ||
|| isReadonly | ||
|| accessibility !== undefined | ||
|| (Array.isArray(decorators) && decorators.length > 0) | ||
) { | ||
@@ -72,8 +60,4 @@ return false; | ||
assertToken(staticToken, { | ||
expected: [ | ||
{type: 'Keyword', value: 'static'}, | ||
// `@babel/eslint-parser` use `{type: 'Identifier', value: 'static'}` | ||
{type: 'Identifier', value: 'static'} | ||
], | ||
ruleId: 'no-static-only-class' | ||
expected: {type: 'Keyword', value: 'static'}, | ||
ruleId: 'no-static-only-class', | ||
}); | ||
@@ -84,5 +68,5 @@ | ||
const maybeSemicolonToken = isPropertyDefinition(node) ? | ||
sourceCode.getLastToken(node) : | ||
sourceCode.getTokenAfter(node); | ||
const maybeSemicolonToken = isPropertyDefinition(node) | ||
? sourceCode.getLastToken(node) | ||
: sourceCode.getTokenAfter(node); | ||
const hasSemicolonToken = isSemicolonToken(maybeSemicolonToken); | ||
@@ -105,5 +89,5 @@ | ||
yield ( | ||
hasSemicolonToken ? | ||
fixer.replaceText(maybeSemicolonToken, ',') : | ||
fixer.insertTextAfter(node, ',') | ||
hasSemicolonToken | ||
? fixer.replaceText(maybeSemicolonToken, ',') | ||
: fixer.insertTextAfter(node, ',') | ||
); | ||
@@ -120,9 +104,9 @@ } | ||
implements: classImplements, | ||
parent | ||
parent, | ||
} = node; | ||
if ( | ||
isDeclare || | ||
isAbstract || | ||
(Array.isArray(classImplements) && classImplements.length > 0) | ||
isDeclare | ||
|| isAbstract | ||
|| (Array.isArray(classImplements) && classImplements.length > 0) | ||
) { | ||
@@ -144,7 +128,7 @@ return; | ||
if ( | ||
isPropertyDefinition(node) && | ||
( | ||
node.typeAnnotation || | ||
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')) | ||
|| (node.value && sourceCode.getText(node.value).includes('this')) | ||
) | ||
@@ -158,6 +142,6 @@ ) { | ||
const classToken = sourceCode.getFirstToken(node); | ||
/* istanbul ignore next */ | ||
/* c8 ignore next */ | ||
assertToken(classToken, { | ||
expected: {type: 'Keyword', value: 'class'}, | ||
ruleId: 'no-static-only-class' | ||
ruleId: 'no-static-only-class', | ||
}); | ||
@@ -179,6 +163,6 @@ | ||
if ( | ||
type === 'ClassExpression' && | ||
parent.type === 'ReturnStatement' && | ||
body.loc.start.line !== parent.loc.start.line && | ||
sourceCode.text.slice(classToken.range[1], body.range[0]).trim() | ||
type === 'ClassExpression' | ||
&& parent.type === 'ReturnStatement' | ||
&& body.loc.start.line !== parent.loc.start.line | ||
&& sourceCode.text.slice(classToken.range[1], body.range[0]).trim() | ||
) { | ||
@@ -217,20 +201,25 @@ yield fixer.replaceText(classToken, '{'); | ||
function create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
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 { | ||
[selector](node) { | ||
if (node.body.body.some(node => !isStaticMember(node))) { | ||
return; | ||
} | ||
const {sourceCode} = context; | ||
return { | ||
node, | ||
loc: getClassHeadLocation(node, sourceCode), | ||
messageId: MESSAGE_ID, | ||
fix: switchClassToObject(node, sourceCode) | ||
}; | ||
} | ||
}; | ||
return { | ||
node, | ||
loc: getClassHeadLocation(node, sourceCode), | ||
messageId: MESSAGE_ID, | ||
fix: switchClassToObject(node, sourceCode), | ||
}; | ||
}); | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -241,7 +230,7 @@ create, | ||
docs: { | ||
description: 'Forbid classes that only have static members.' | ||
description: 'Disallow classes that only have static members.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const MESSAGE_ID = 'no-this-assignment'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Do not assign `this` to `{{name}}`.' | ||
[MESSAGE_ID]: 'Do not assign `this` to `{{name}}`.', | ||
}; | ||
const variableDeclaratorSelector = [ | ||
'VariableDeclarator', | ||
'[init.type="ThisExpression"]', | ||
'[id.type="Identifier"]' | ||
].join(''); | ||
function getProblem(variableNode, valueNode) { | ||
if ( | ||
variableNode.type !== 'Identifier' | ||
|| valueNode?.type !== 'ThisExpression' | ||
) { | ||
return; | ||
} | ||
const assignmentExpressionSelector = [ | ||
'AssignmentExpression', | ||
'[right.type="ThisExpression"]', | ||
'[left.type="Identifier"]' | ||
].join(''); | ||
return { | ||
node: valueNode.parent, | ||
data: {name: variableNode.name}, | ||
messageId: MESSAGE_ID, | ||
}; | ||
} | ||
const selector = matches([variableDeclaratorSelector, assignmentExpressionSelector]); | ||
/** @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)); | ||
}; | ||
const create = () => ({ | ||
[selector](node) { | ||
const variable = node.type === 'AssignmentExpression' ? node.left : node.id; | ||
return { | ||
node, | ||
data: {name: variable.name}, | ||
messageId: MESSAGE_ID | ||
}; | ||
} | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -39,6 +34,6 @@ create, | ||
docs: { | ||
description: 'Disallow assigning `this` to a variable.' | ||
description: 'Disallow assigning `this` to a variable.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const MESSAGE_ID = 'no-unreadable-array-destructuring'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Array destructuring may not contain consecutive ignored values.' | ||
[MESSAGE_ID]: 'Array destructuring may not contain consecutive ignored values.', | ||
}; | ||
@@ -13,10 +14,13 @@ | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
'ArrayPattern[elements.length>=3]'(node) { | ||
ArrayPattern(node) { | ||
const {elements, parent} = node; | ||
if (!elements.some((element, index, elements) => isCommaFollowedWithComma(element, index, elements))) { | ||
if ( | ||
elements.length < 3 | ||
|| !elements.some((element, index, elements) => isCommaFollowedWithComma(element, index, elements))) { | ||
return; | ||
@@ -27,3 +31,3 @@ } | ||
node, | ||
messageId: MESSAGE_ID | ||
messageId: MESSAGE_ID, | ||
}; | ||
@@ -33,6 +37,6 @@ | ||
if ( | ||
parent.type === 'VariableDeclarator' && | ||
parent.id === node && | ||
parent.init !== null && | ||
nonNullElements.length === 1 | ||
parent.type === 'VariableDeclarator' | ||
&& parent.id === node | ||
&& parent.init !== null | ||
&& nonNullElements.length === 1 | ||
) { | ||
@@ -52,4 +56,4 @@ const [element] = nonNullElements; | ||
if ( | ||
!isParenthesized(array, sourceCode) && | ||
shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
!isParenthesized(array, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
@@ -61,2 +65,4 @@ yield fixer.insertTextBefore(array, '('); | ||
} | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}; | ||
@@ -67,6 +73,7 @@ } | ||
return problem; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -77,7 +84,7 @@ create, | ||
docs: { | ||
description: 'Disallow unreadable array destructuring.' | ||
description: 'Disallow unreadable array destructuring.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const getScopes = require('./utils/get-scopes.js'); | ||
const MESSAGE_ID = 'no-unused-properties'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Property `{{name}}` is defined but never used.' | ||
[MESSAGE_ID]: 'Property `{{name}}` is defined but never used.', | ||
}; | ||
const getDeclaratorOrPropertyValue = declaratorOrProperty => | ||
declaratorOrProperty.init || | ||
declaratorOrProperty.value; | ||
declaratorOrProperty.init | ||
|| declaratorOrProperty.value; | ||
const isMemberExpressionCall = memberExpression => | ||
memberExpression.parent && | ||
memberExpression.parent.type === 'CallExpression' && | ||
memberExpression.parent.callee === memberExpression; | ||
memberExpression.parent.type === 'CallExpression' | ||
&& memberExpression.parent.callee === memberExpression; | ||
const isMemberExpressionAssignment = memberExpression => | ||
memberExpression.parent && | ||
memberExpression.parent.type === 'AssignmentExpression'; | ||
const isMemberExpressionComputedBeyondPrediction = memberExpression => | ||
memberExpression.computed && | ||
memberExpression.property.type !== 'Literal'; | ||
memberExpression.computed | ||
&& memberExpression.property.type !== 'Literal'; | ||
const specialProtoPropertyKey = { | ||
type: 'Identifier', | ||
name: '__proto__' | ||
name: '__proto__', | ||
}; | ||
@@ -54,4 +53,4 @@ | ||
const objectPatternMatchesObjectExprPropertyKey = (pattern, key) => { | ||
return pattern.properties.some(property => { | ||
const objectPatternMatchesObjectExprPropertyKey = (pattern, key) => | ||
pattern.properties.some(property => { | ||
if (property.type === 'RestElement') { | ||
@@ -63,3 +62,2 @@ return true; | ||
}); | ||
}; | ||
@@ -85,3 +83,5 @@ const isLeafDeclaratorOrProperty = declaratorOrProperty => { | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {sourceCode} = context; | ||
const getPropertyDisplayName = property => { | ||
@@ -96,3 +96,3 @@ if (property.key.type === 'Identifier') { | ||
return context.getSource(property.key); | ||
return sourceCode.getText(property.key); | ||
}; | ||
@@ -106,4 +106,4 @@ | ||
data: { | ||
name: getPropertyDisplayName(property) | ||
} | ||
name: getPropertyDisplayName(property), | ||
}, | ||
}); | ||
@@ -136,5 +136,5 @@ return; | ||
if ( | ||
parent.type === 'VariableDeclarator' && | ||
parent.parent.type === 'VariableDeclaration' && | ||
parent.parent.parent.type === 'ExportNamedDeclaration' | ||
parent.type === 'VariableDeclarator' | ||
&& parent.parent.type === 'VariableDeclaration' | ||
&& parent.parent.parent.type === 'ExportNamedDeclaration' | ||
) { | ||
@@ -149,6 +149,6 @@ return {identifier: parent}; | ||
if ( | ||
isMemberExpressionAssignment(parent) || | ||
isMemberExpressionCall(parent) || | ||
isMemberExpressionComputedBeyondPrediction(parent) || | ||
propertyKeysEqual(parent.property, key) | ||
isMemberExpressionAssignment(parent) | ||
|| isMemberExpressionCall(parent) | ||
|| isMemberExpressionComputedBeyondPrediction(parent) | ||
|| propertyKeysEqual(parent.property, key) | ||
) { | ||
@@ -162,4 +162,4 @@ return {identifier: parent}; | ||
if ( | ||
parent.type === 'VariableDeclarator' && | ||
parent.id.type === 'ObjectPattern' | ||
parent.type === 'VariableDeclarator' | ||
&& parent.id.type === 'ObjectPattern' | ||
) { | ||
@@ -174,4 +174,4 @@ if (objectPatternMatchesObjectExprPropertyKey(parent.id, key)) { | ||
if ( | ||
parent.type === 'AssignmentExpression' && | ||
parent.left.type === 'ObjectPattern' | ||
parent.type === 'AssignmentExpression' | ||
&& parent.left.type === 'ObjectPattern' | ||
) { | ||
@@ -223,25 +223,17 @@ if (objectPatternMatchesObjectExprPropertyKey(parent.left, key)) { | ||
const checkChildScopes = scope => { | ||
for (const childScope of scope.childScopes) { | ||
checkScope(childScope); | ||
} | ||
}; | ||
return { | ||
'Program:exit'(program) { | ||
const scopes = getScopes(sourceCode.getScope(program)); | ||
for (const scope of scopes) { | ||
if (scope.type === 'global') { | ||
continue; | ||
} | ||
const checkScope = scope => { | ||
if (scope.type === 'global') { | ||
return checkChildScopes(scope); | ||
} | ||
checkVariables(scope); | ||
return checkChildScopes(scope); | ||
checkVariables(scope); | ||
} | ||
}, | ||
}; | ||
return { | ||
'Program:exit'() { | ||
checkScope(context.getScope()); | ||
} | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -252,6 +244,6 @@ create, | ||
docs: { | ||
description: 'Disallow unused object properties.' | ||
description: 'Disallow unused object properties.', | ||
}, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const replaceNodeOrTokenAndSpacesBefore = require('./utils/replace-node-or-token-and-spaces-before.js'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const {replaceNodeOrTokenAndSpacesBefore} = require('./fix/index.js'); | ||
const {isUndefined, isFunction} = require('./ast/index.js'); | ||
const messageId = 'no-useless-undefined'; | ||
const messages = { | ||
[messageId]: 'Do not use useless `undefined`.' | ||
[messageId]: 'Do not use useless `undefined`.', | ||
}; | ||
const getSelector = (parent, property) => | ||
`${parent} > Identifier.${property}[name="undefined"]`; | ||
// `return undefined` | ||
const returnSelector = getSelector('ReturnStatement', 'argument'); | ||
// `yield undefined` | ||
const yieldSelector = getSelector('YieldExpression[delegate!=true]', 'argument'); | ||
// `() => undefined` | ||
const arrowFunctionSelector = getSelector('ArrowFunctionExpression', 'body'); | ||
// `let foo = undefined` / `var foo = undefined` | ||
const variableInitSelector = getSelector( | ||
[ | ||
'VariableDeclaration', | ||
'[kind!="const"]', | ||
'>', | ||
'VariableDeclarator' | ||
].join(''), | ||
'init' | ||
); | ||
// `const {foo = undefined} = {}` | ||
const assignmentPatternSelector = getSelector('AssignmentPattern', 'right'); | ||
const isUndefined = node => node && node.type === 'Identifier' && node.name === 'undefined'; | ||
const compareFunctionNames = new Set([ | ||
@@ -57,3 +30,3 @@ 'is', | ||
'strictSame', | ||
'strictNotSame' | ||
'strictNotSame', | ||
]); | ||
@@ -66,6 +39,5 @@ const shouldIgnore = node => { | ||
} else if ( | ||
node.type === 'MemberExpression' && | ||
node.computed === false && | ||
node.property && | ||
node.property.type === 'Identifier' | ||
node.type === 'MemberExpression' | ||
&& node.computed === false | ||
&& node.property.type === 'Identifier' | ||
) { | ||
@@ -75,11 +47,23 @@ name = node.property.name; | ||
return compareFunctionNames.has(name) || | ||
return compareFunctionNames.has(name) | ||
// `array.push(undefined)` | ||
|| name === 'push' | ||
// `array.unshift(undefined)` | ||
|| name === 'unshift' | ||
// `array.includes(undefined)` | ||
|| name === 'includes' | ||
// `set.add(undefined)` | ||
name === 'add' || | ||
|| name === 'add' | ||
// `set.has(undefined)` | ||
|| name === 'has' | ||
// `map.set(foo, undefined)` | ||
name === 'set' || | ||
// `array.push(undefined)` | ||
name === 'push' || | ||
// `array.unshift(undefined)` | ||
name === 'unshift'; | ||
|| name === 'set' | ||
// `React.createContext(undefined)` | ||
|| name === 'createContext' | ||
// https://vuejs.org/api/reactivity-core.html#ref | ||
|| name === 'ref'; | ||
}; | ||
@@ -95,7 +79,20 @@ | ||
const isFunctionBindCall = node => | ||
!node.optional | ||
&& node.callee.type === 'MemberExpression' | ||
&& !node.callee.computed | ||
&& node.callee.property.type === 'Identifier' | ||
&& node.callee.property.name === 'bind'; | ||
const isTypeScriptFile = context => | ||
/\.(?:ts|mts|cts|tsx)$/i.test(context.physicalFilename); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const listener = (fix, checkFunctionReturnType) => node => { | ||
const {sourceCode} = context; | ||
const getProblem = (node, fix, checkFunctionReturnType) => { | ||
if (checkFunctionReturnType) { | ||
const functionNode = getFunction(context.getScope()); | ||
if (functionNode && functionNode.returnType) { | ||
const functionNode = getFunction(sourceCode.getScope(node)); | ||
if (functionNode?.returnType) { | ||
return; | ||
@@ -108,10 +105,9 @@ } | ||
messageId, | ||
fix: fixer => fix(node, fixer) | ||
fix, | ||
}; | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const options = { | ||
checkArguments: true, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
@@ -122,73 +118,156 @@ | ||
const listeners = { | ||
[returnSelector]: listener( | ||
removeNodeAndLeadingSpace, | ||
/* CheckFunctionReturnType */ true | ||
), | ||
[yieldSelector]: listener(removeNodeAndLeadingSpace), | ||
[arrowFunctionSelector]: listener( | ||
(node, fixer) => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode), | ||
/* CheckFunctionReturnType */ true | ||
), | ||
[variableInitSelector]: listener( | ||
(node, fixer) => fixer.removeRange([node.parent.id.range[1], node.range[1]]) | ||
), | ||
[assignmentPatternSelector]: listener( | ||
(node, fixer) => fixer.removeRange([node.parent.left.range[1], node.range[1]]) | ||
) | ||
}; | ||
// `return undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'ReturnStatement' | ||
&& node.parent.argument === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => removeNodeAndLeadingSpace(node, fixer), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
if (options.checkArguments) { | ||
listeners.CallExpression = node => { | ||
if (shouldIgnore(node.callee)) { | ||
return; | ||
} | ||
// `yield undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'YieldExpression' | ||
&& !node.parent.delegate | ||
&& node.parent.argument === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => removeNodeAndLeadingSpace(node, fixer), | ||
); | ||
} | ||
}); | ||
const argumentNodes = node.arguments; | ||
const undefinedArguments = []; | ||
for (let index = argumentNodes.length - 1; index >= 0; index--) { | ||
const node = argumentNodes[index]; | ||
if (isUndefined(node)) { | ||
undefinedArguments.unshift(node); | ||
} else { | ||
break; | ||
} | ||
} | ||
// `() => undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'ArrowFunctionExpression' | ||
&& node.parent.body === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
if (undefinedArguments.length === 0) { | ||
return; | ||
} | ||
// `let foo = undefined` / `var foo = undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'VariableDeclarator' | ||
&& node.parent.init === node | ||
&& node.parent.parent.type === 'VariableDeclaration' | ||
&& node.parent.parent.kind !== 'const' | ||
&& node.parent.parent.declarations.includes(node.parent) | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => fixer.removeRange([node.parent.id.range[1], node.range[1]]), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
const firstUndefined = undefinedArguments[0]; | ||
const lastUndefined = undefinedArguments[undefinedArguments.length - 1]; | ||
// `const {foo = undefined} = {}` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'AssignmentPattern' | ||
&& node.parent.right === node | ||
) { | ||
return getProblem( | ||
node, | ||
function * (fixer) { | ||
const assignmentPattern = node.parent; | ||
const {left} = assignmentPattern; | ||
return { | ||
messageId, | ||
loc: { | ||
start: firstUndefined.loc.start, | ||
end: lastUndefined.loc.end | ||
yield fixer.removeRange([left.range[1], node.range[1]]); | ||
if ( | ||
(left.typeAnnotation || isTypeScriptFile(context)) | ||
&& !left.optional | ||
&& isFunction(assignmentPattern.parent) | ||
&& assignmentPattern.parent.params.includes(assignmentPattern) | ||
) { | ||
yield ( | ||
left.typeAnnotation | ||
? fixer.insertTextBefore(left.typeAnnotation, '?') | ||
: fixer.insertTextAfter(left, '?') | ||
); | ||
} | ||
}, | ||
fix: fixer => { | ||
let start = firstUndefined.range[0]; | ||
let end = lastUndefined.range[1]; | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
const previousArgument = argumentNodes[argumentNodes.length - undefinedArguments.length - 1]; | ||
if (!options.checkArguments) { | ||
return; | ||
} | ||
if (previousArgument) { | ||
start = previousArgument.range[1]; | ||
} else { | ||
// If all arguments removed, and there is trailing comma, we need remove it. | ||
const tokenAfter = context.getTokenAfter(lastUndefined); | ||
if (isCommaToken(tokenAfter)) { | ||
end = tokenAfter.range[1]; | ||
} | ||
context.on('CallExpression', node => { | ||
if (shouldIgnore(node.callee)) { | ||
return; | ||
} | ||
const argumentNodes = node.arguments; | ||
// Ignore arguments in `Function#bind()`, but not `this` argument | ||
if (isFunctionBindCall(node) && argumentNodes.length !== 1) { | ||
return; | ||
} | ||
const undefinedArguments = []; | ||
for (let index = argumentNodes.length - 1; index >= 0; index--) { | ||
const node = argumentNodes[index]; | ||
if (isUndefined(node)) { | ||
undefinedArguments.unshift(node); | ||
} else { | ||
break; | ||
} | ||
} | ||
if (undefinedArguments.length === 0) { | ||
return; | ||
} | ||
const firstUndefined = undefinedArguments[0]; | ||
const lastUndefined = undefinedArguments.at(-1); | ||
return { | ||
messageId, | ||
loc: { | ||
start: firstUndefined.loc.start, | ||
end: lastUndefined.loc.end, | ||
}, | ||
fix(fixer) { | ||
let start = firstUndefined.range[0]; | ||
let end = lastUndefined.range[1]; | ||
const previousArgument = argumentNodes[argumentNodes.length - undefinedArguments.length - 1]; | ||
if (previousArgument) { | ||
start = previousArgument.range[1]; | ||
} else { | ||
// If all arguments removed, and there is trailing comma, we need remove it. | ||
const tokenAfter = sourceCode.getTokenAfter(lastUndefined); | ||
if (isCommaToken(tokenAfter)) { | ||
end = tokenAfter.range[1]; | ||
} | ||
} | ||
return fixer.removeRange([start, end]); | ||
} | ||
}; | ||
return fixer.removeRange([start, end]); | ||
}, | ||
}; | ||
} | ||
return listeners; | ||
}); | ||
}; | ||
@@ -199,11 +278,12 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkArguments: { | ||
type: 'boolean' | ||
} | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -214,8 +294,8 @@ create, | ||
docs: { | ||
description: 'Disallow useless `undefined`.' | ||
description: 'Disallow useless `undefined`.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {isNumber, isDecimalInteger} = require('./utils/numeric.js'); | ||
const {isDecimalInteger} = require('./utils/numeric.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isNumberLiteral} = require('./ast/index.js'); | ||
@@ -11,56 +13,58 @@ const MESSAGE_ZERO_FRACTION = 'zero-fraction'; | ||
[MESSAGE_ZERO_FRACTION]: 'Don\'t use a zero fraction in the number.', | ||
[MESSAGE_DANGLING_DOT]: 'Don\'t use a dangling dot in the number.' | ||
[MESSAGE_DANGLING_DOT]: 'Don\'t use a dangling dot in the number.', | ||
}; | ||
const create = context => { | ||
return { | ||
Literal: node => { | ||
if (!isNumber(node)) { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
Literal(node) { | ||
if (!isNumberLiteral(node)) { | ||
return; | ||
} | ||
// Legacy octal number `0777` and prefixed number `0o1234` cannot have a dot. | ||
const {raw} = node; | ||
const match = raw.match(/^(?<before>[\d_]*)(?<dotAndFractions>\.[\d_]*)(?<after>.*)$/); | ||
if (!match) { | ||
return; | ||
} | ||
// Legacy octal number `0777` and prefixed number `0o1234` cannot have a dot. | ||
const {raw} = node; | ||
const match = raw.match(/^(?<before>[\d_]*)(?<dotAndFractions>\.[\d_]*)(?<after>.*)$/); | ||
if (!match) { | ||
return; | ||
} | ||
const {before, dotAndFractions, after} = match.groups; | ||
const formatted = before + dotAndFractions.replace(/[.0_]+$/g, '') + after; | ||
const {before, dotAndFractions, after} = match.groups; | ||
const fixedDotAndFractions = dotAndFractions.replaceAll(/[.0_]+$/g, ''); | ||
const formatted = ((before + fixedDotAndFractions) || '0') + after; | ||
if (formatted === raw) { | ||
return; | ||
} | ||
if (formatted === raw) { | ||
return; | ||
} | ||
const isDanglingDot = dotAndFractions === '.'; | ||
// End of fractions | ||
const end = node.range[0] + before.length + dotAndFractions.length; | ||
const start = end - (raw.length - formatted.length); | ||
const sourceCode = context.getSourceCode(); | ||
return { | ||
loc: toLocation([start, end], sourceCode), | ||
messageId: isDanglingDot ? MESSAGE_DANGLING_DOT : MESSAGE_ZERO_FRACTION, | ||
fix: fixer => { | ||
let fixed = formatted; | ||
if ( | ||
node.parent.type === 'MemberExpression' && | ||
node.parent.object === node && | ||
isDecimalInteger(formatted) && | ||
!isParenthesized(node, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
const isDanglingDot = dotAndFractions === '.'; | ||
// End of fractions | ||
const end = node.range[0] + before.length + dotAndFractions.length; | ||
const start = end - (raw.length - formatted.length); | ||
const {sourceCode} = context; | ||
return { | ||
loc: toLocation([start, end], sourceCode), | ||
messageId: isDanglingDot ? MESSAGE_DANGLING_DOT : MESSAGE_ZERO_FRACTION, | ||
* fix(fixer) { | ||
let fixed = formatted; | ||
if ( | ||
node.parent.type === 'MemberExpression' | ||
&& node.parent.object === node | ||
&& isDecimalInteger(formatted) | ||
&& !isParenthesized(node, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
return fixer.replaceText(node, fixed); | ||
} | ||
}; | ||
} | ||
}; | ||
}; | ||
yield fixer.replaceText(node, fixed); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -71,7 +75,7 @@ create, | ||
docs: { | ||
description: 'Disallow number literals with zero fractions or dangling dots.' | ||
description: 'Disallow number literals with zero fractions or dangling dots.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isNumber, isBigInt} = require('./utils/numeric.js'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {isNumberLiteral, isBigIntLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'number-literal-case'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Invalid number literal casing.' | ||
[MESSAGE_ID]: 'Invalid number literal casing.', | ||
}; | ||
@@ -18,35 +19,35 @@ | ||
const create = () => { | ||
return { | ||
Literal: node => { | ||
const {raw} = node; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
Literal(node) { | ||
const {raw} = node; | ||
let fixed = raw; | ||
if (isNumber(node)) { | ||
fixed = fix(raw); | ||
} else if (isBigInt(node)) { | ||
fixed = fix(raw.slice(0, -1)) + 'n'; | ||
} | ||
let fixed = raw; | ||
if (isNumberLiteral(node)) { | ||
fixed = fix(raw); | ||
} else if (isBigIntLiteral(node)) { | ||
fixed = fix(raw.slice(0, -1)) + 'n'; | ||
} | ||
if (raw !== fixed) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.replaceText(node, fixed) | ||
}; | ||
} | ||
if (raw !== fixed) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.replaceText(node, fixed), | ||
}; | ||
} | ||
}; | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
create: checkVueTemplate(create), | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Enforce proper case for numeric literals.' | ||
description: 'Enforce proper case for numeric literals.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const numeric = require('./utils/numeric.js'); | ||
const {isBigIntLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'numeric-separators-style'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Invalid group length in numeric value.' | ||
[MESSAGE_ID]: 'Invalid group length in numeric value.', | ||
}; | ||
@@ -52,3 +53,3 @@ | ||
sign, | ||
power | ||
power, | ||
} = numeric.parseNumber(value); | ||
@@ -63,3 +64,3 @@ | ||
hexadecimal: {minimumDigits: 0, groupLength: 2}, | ||
number: {minimumDigits: 5, groupLength: 3} | ||
number: {minimumDigits: 5, groupLength: 3}, | ||
}; | ||
@@ -72,6 +73,6 @@ const create = context => { | ||
hexadecimal, | ||
number | ||
number, | ||
} = { | ||
onlyIfContainsSeparator: false, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
@@ -83,3 +84,3 @@ | ||
...defaultOptions.binary, | ||
...binary | ||
...binary, | ||
}, | ||
@@ -89,3 +90,3 @@ '0o': { | ||
...defaultOptions.octal, | ||
...octal | ||
...octal, | ||
}, | ||
@@ -95,3 +96,3 @@ '0x': { | ||
...defaultOptions.hexadecimal, | ||
...hexadecimal | ||
...hexadecimal, | ||
}, | ||
@@ -101,8 +102,8 @@ '': { | ||
...defaultOptions.number, | ||
...number | ||
} | ||
...number, | ||
}, | ||
}; | ||
return { | ||
Literal: node => { | ||
Literal(node) { | ||
if (!numeric.isNumeric(node) || numeric.isLegacyOctal(node)) { | ||
@@ -115,3 +116,3 @@ return; | ||
let suffix = ''; | ||
if (numeric.isBigInt(node)) { | ||
if (isBigIntLiteral(node)) { | ||
number = raw.slice(0, -1); | ||
@@ -121,3 +122,3 @@ suffix = 'n'; | ||
const strippedNumber = number.replace(/_/g, ''); | ||
const strippedNumber = number.replaceAll('_', ''); | ||
const {prefix, data} = numeric.getPrefix(strippedNumber); | ||
@@ -136,6 +137,6 @@ | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.replaceText(node, formatted) | ||
fix: fixer => fixer.replaceText(node, formatted), | ||
}; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -146,5 +147,6 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
onlyIfContainsSeparator: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
@@ -154,3 +156,3 @@ minimumDigits: { | ||
minimum: 0, | ||
default: minimumDigits | ||
default: minimumDigits, | ||
}, | ||
@@ -160,6 +162,5 @@ groupLength: { | ||
minimum: 1, | ||
default: groupLength | ||
} | ||
default: groupLength, | ||
}, | ||
}, | ||
additionalProperties: false | ||
}); | ||
@@ -169,14 +170,15 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
...Object.fromEntries( | ||
Object.entries(defaultOptions).map(([type, options]) => [type, formatOptionsSchema(options)]) | ||
Object.entries(defaultOptions).map(([type, options]) => [type, formatOptionsSchema(options)]), | ||
), | ||
onlyIfContainsSeparator: { | ||
type: 'boolean', | ||
default: false | ||
} | ||
default: false, | ||
}, | ||
}, | ||
additionalProperties: false | ||
}]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -187,8 +189,8 @@ create, | ||
docs: { | ||
description: 'Enforce the style of numeric separators by correctly grouping digits.' | ||
description: 'Enforce the style of numeric separators by correctly grouping digits.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const domEventsJson = require('./utils/dom-events.json'); | ||
const {STATIC_REQUIRE_SOURCE_SELECTOR} = require('./selectors/index.js'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const eventTypes = require('./shared/dom-events.js'); | ||
const {isUndefined, isNullLiteral, isStaticRequire} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-add-event-listener'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `{{replacement}}` over `{{method}}`.{{extra}}' | ||
[MESSAGE_ID]: 'Prefer `{{replacement}}` over `{{method}}`.{{extra}}', | ||
}; | ||
const extraMessages = { | ||
beforeunload: 'Use `event.preventDefault(); event.returnValue = \'foo\'` to trigger the prompt.', | ||
message: 'Note that there is difference between `SharedWorker#onmessage` and `SharedWorker#addEventListener(\'message\')`.' | ||
message: 'Note that there is difference between `SharedWorker#onmessage` and `SharedWorker#addEventListener(\'message\')`.', | ||
error: 'Note that there is difference between `{window,element}.onerror` and `{window,element}.addEventListener(\'error\')`.', | ||
}; | ||
const nestedEvents = Object.values(domEventsJson); | ||
const eventTypes = new Set(nestedEvents.flat()); | ||
const getEventMethodName = memberExpression => memberExpression.property.name; | ||
@@ -38,4 +37,4 @@ const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length); | ||
if ( | ||
assignedExpression.type !== 'ArrowFunctionExpression' && | ||
assignedExpression.type !== 'FunctionExpression' | ||
assignedExpression.type !== 'ArrowFunctionExpression' | ||
&& assignedExpression.type !== 'FunctionExpression' | ||
) { | ||
@@ -52,14 +51,5 @@ return false; | ||
const isClearing = node => { | ||
if (node.type === 'Literal') { | ||
return node.raw === 'null'; | ||
} | ||
const isClearing = node => isUndefined(node) || isNullLiteral(node); | ||
if (node.type === 'Identifier') { | ||
return node.name === 'undefined'; | ||
} | ||
return false; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -78,3 +68,3 @@ const options = context.options[0] || {}; | ||
upper: codePathInfo, | ||
returnsSomething: false | ||
returnsSomething: false, | ||
}; | ||
@@ -88,4 +78,8 @@ }, | ||
[STATIC_REQUIRE_SOURCE_SELECTOR](node) { | ||
if (!isDisabled && excludedPackages.has(node.value)) { | ||
CallExpression(node) { | ||
if (!isStaticRequire(node)) { | ||
return; | ||
} | ||
if (!isDisabled && excludedPackages.has(node.arguments[0].value)) { | ||
isDisabled = true; | ||
@@ -95,4 +89,4 @@ } | ||
'ImportDeclaration > Literal'(node) { | ||
if (!isDisabled && excludedPackages.has(node.value)) { | ||
Literal(node) { | ||
if (node.parent.type === 'ImportDeclaration' && !isDisabled && excludedPackages.has(node.value)) { | ||
isDisabled = true; | ||
@@ -111,7 +105,7 @@ } | ||
const {left: memberExpression, right: assignedExpression} = node; | ||
const {left: memberExpression, right: assignedExpression, operator} = node; | ||
if ( | ||
memberExpression.type !== 'MemberExpression' || | ||
memberExpression.computed | ||
memberExpression.type !== 'MemberExpression' | ||
|| memberExpression.computed | ||
) { | ||
@@ -140,4 +134,4 @@ return; | ||
} else if ( | ||
eventTypeName === 'beforeunload' && | ||
!shouldFixBeforeUnload(assignedExpression, nodeReturnsSomething) | ||
eventTypeName === 'beforeunload' | ||
&& !shouldFixBeforeUnload(assignedExpression, nodeReturnsSomething) | ||
) { | ||
@@ -148,4 +142,11 @@ extra = extraMessages.beforeunload; | ||
extra = extraMessages.message; | ||
} else { | ||
fix = fixer => fixCode(fixer, context.getSourceCode(), node, memberExpression); | ||
} else if (eventTypeName === 'error') { | ||
// Disable `onerror` fix, see #1493 | ||
extra = extraMessages.error; | ||
} else if ( | ||
operator === '=' | ||
&& node.parent.type === 'ExpressionStatement' | ||
&& node.parent.expression === node | ||
) { | ||
fix = fixer => fixCode(fixer, context.sourceCode, node, memberExpression); | ||
} | ||
@@ -159,7 +160,7 @@ | ||
method: eventMethodName, | ||
extra: extra ? ` ${extra}` : '' | ||
extra: extra ? ` ${extra}` : '', | ||
}, | ||
fix | ||
fix, | ||
}; | ||
} | ||
}, | ||
}; | ||
@@ -171,2 +172,3 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
@@ -176,11 +178,11 @@ excludedPackages: { | ||
items: { | ||
type: 'string' | ||
type: 'string', | ||
}, | ||
uniqueItems: true | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -191,8 +193,8 @@ create, | ||
docs: { | ||
description: 'Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions.' | ||
description: 'Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, findVariable} = require('eslint-utils'); | ||
const {isParenthesized, findVariable} = require('@eslint-community/eslint-utils'); | ||
const { | ||
not, | ||
methodCallSelector, | ||
notLeftHandSideSelector | ||
} = require('./selectors/index.js'); | ||
const getVariableIdentifiers = require('./utils/get-variable-identifiers.js'); | ||
const renameVariable = require('./utils/rename-variable.js'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const getChildScopesRecursive = require('./utils/get-child-scopes-recursive.js'); | ||
const singular = require('./utils/singular.js'); | ||
const extendFixRange = require('./utils/extend-fix-range.js'); | ||
const {removeMemberExpressionProperty, removeMethodCall} = require('./fix/index.js'); | ||
extendFixRange, | ||
removeMemberExpressionProperty, | ||
removeMethodCall, | ||
renameVariable, | ||
} = require('./fix/index.js'); | ||
const { | ||
isLeftHandSide, | ||
singular, | ||
getScopes, | ||
avoidCapture, | ||
getVariableIdentifiers, | ||
} = require('./utils/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const ERROR_ZERO_INDEX = 'error-zero-index'; | ||
const ERROR_SHIFT = 'error-shift'; | ||
const ERROR_POP = 'error-pop'; | ||
const ERROR_AT_MINUS_ONE = 'error-at-minus-one'; | ||
const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration'; | ||
@@ -27,2 +31,4 @@ const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment'; | ||
[ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.', | ||
[ERROR_POP]: 'Prefer `.findLast(…)` over `.filter(…).pop()`.', | ||
[ERROR_AT_MINUS_ONE]: 'Prefer `.findLast(…)` over `.filter(…).at(-1)`.', | ||
[ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.', | ||
@@ -32,69 +38,13 @@ // Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case | ||
[SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.', | ||
[SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.' | ||
[SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.', | ||
}; | ||
const filterMethodSelectorOptions = { | ||
name: 'filter', | ||
min: 1, | ||
max: 2 | ||
}; | ||
const isArrayFilterCall = node => isMethodCall(node, { | ||
method: 'filter', | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}); | ||
const filterVariableSelector = [ | ||
'VariableDeclaration', | ||
// Exclude `export const foo = [];` | ||
not('ExportNamedDeclaration > .declaration'), | ||
' > ', | ||
'VariableDeclarator.declarations', | ||
'[id.type="Identifier"]', | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'init' | ||
}) | ||
].join(''); | ||
const zeroIndexSelector = [ | ||
'MemberExpression', | ||
'[computed!=false]', | ||
'[property.type="Literal"]', | ||
'[property.raw="0"]', | ||
notLeftHandSideSelector(), | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'object' | ||
}) | ||
].join(''); | ||
const shiftSelector = [ | ||
methodCallSelector({ | ||
name: 'shift', | ||
length: 0 | ||
}), | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'callee.object' | ||
}) | ||
].join(''); | ||
const destructuringDeclaratorSelector = [ | ||
'VariableDeclarator', | ||
'[id.type="ArrayPattern"]', | ||
'[id.elements.length=1]', | ||
'[id.elements.0.type!="RestElement"]', | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'init' | ||
}) | ||
].join(''); | ||
const destructuringAssignmentSelector = [ | ||
'AssignmentExpression', | ||
'[left.type="ArrayPattern"]', | ||
'[left.elements.length=1]', | ||
'[left.elements.0.type!="RestElement"]', | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'right' | ||
}) | ||
].join(''); | ||
// Need add `()` to the `AssignmentExpression` | ||
@@ -119,3 +69,3 @@ // - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)` | ||
(node.type === 'LogicalExpression' && ( | ||
node.operator === operator || | ||
node.operator === operator | ||
// https://tc39.es/proposal-nullish-coalescing/ says | ||
@@ -125,15 +75,15 @@ // `??` has lower precedence than `||` | ||
// `??` has higher precedence than `||` | ||
(operator === '||' && node.operator === '??') || | ||
(operator === '??' && (node.operator === '||' || node.operator === '&&')) | ||
)) || | ||
node.type === 'ConditionalExpression' || | ||
|| (operator === '||' && node.operator === '??') | ||
|| (operator === '??' && (node.operator === '||' || node.operator === '&&')) | ||
)) | ||
|| node.type === 'ConditionalExpression' | ||
// Lower than `assignment`, should already parenthesized | ||
/* istanbul ignore next */ | ||
node.type === 'AssignmentExpression' || | ||
node.type === 'YieldExpression' || | ||
node.type === 'SequenceExpression' | ||
/* c8 ignore next */ | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'YieldExpression' | ||
|| node.type === 'SequenceExpression' | ||
); | ||
const getDestructuringLeftAndRight = node => { | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (!node) { | ||
@@ -192,3 +142,3 @@ return {}; | ||
{operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR}, | ||
{operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR} | ||
{operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR}, | ||
].map(({messageId, operator}) => ({ | ||
@@ -200,3 +150,3 @@ messageId, | ||
yield * fixDestructuring(node, sourceCode, fixer); | ||
} | ||
}, | ||
})); | ||
@@ -214,115 +164,255 @@ } else { | ||
const isAccessingZeroIndex = node => | ||
node.parent && | ||
node.parent.type === 'MemberExpression' && | ||
node.parent.computed === true && | ||
node.parent.object === node && | ||
node.parent.property && | ||
node.parent.property.type === 'Literal' && | ||
node.parent.property.raw === '0'; | ||
node.parent.type === 'MemberExpression' | ||
&& node.parent.computed === true | ||
&& node.parent.object === node | ||
&& node.parent.property.type === 'Literal' | ||
&& node.parent.property.raw === '0'; | ||
const isDestructuringFirstElement = node => { | ||
const {left, right} = getDestructuringLeftAndRight(node.parent); | ||
return left && | ||
right && | ||
right === node && | ||
left.type === 'ArrayPattern' && | ||
left.elements && | ||
left.elements.length === 1 && | ||
left.elements[0].type !== 'RestElement'; | ||
return left | ||
&& right | ||
&& right === node | ||
&& left.type === 'ArrayPattern' | ||
&& left.elements.length === 1 | ||
&& left.elements[0] | ||
&& left.elements[0].type !== 'RestElement'; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const { | ||
checkFromLast, | ||
} = { | ||
checkFromLast: false, | ||
...context.options[0], | ||
}; | ||
return { | ||
[zeroIndexSelector](node) { | ||
return { | ||
node: node.object.callee.property, | ||
messageId: ERROR_ZERO_INDEX, | ||
fix: fixer => [ | ||
fixer.replaceText(node.object.callee.property, 'find'), | ||
removeMemberExpressionProperty(fixer, node, sourceCode) | ||
] | ||
}; | ||
}, | ||
[shiftSelector](node) { | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_SHIFT, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'find'), | ||
...removeMethodCall(fixer, node, sourceCode) | ||
] | ||
}; | ||
}, | ||
[destructuringDeclaratorSelector](node) { | ||
return { | ||
node: node.init.callee.property, | ||
messageId: ERROR_DESTRUCTURING_DECLARATION, | ||
...fixDestructuringAndReplaceFilter(sourceCode, node) | ||
}; | ||
}, | ||
[destructuringAssignmentSelector](node) { | ||
return { | ||
node: node.right.callee.property, | ||
messageId: ERROR_DESTRUCTURING_ASSIGNMENT, | ||
...fixDestructuringAndReplaceFilter(sourceCode, node) | ||
}; | ||
}, | ||
[filterVariableSelector](node) { | ||
const scope = context.getScope(); | ||
const variable = findVariable(scope, node.id); | ||
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id); | ||
// Zero index access | ||
context.on('MemberExpression', node => { | ||
if (!( | ||
node.computed | ||
&& node.property.type === 'Literal' | ||
&& node.property.raw === '0' | ||
&& isArrayFilterCall(node.object) | ||
&& !isLeftHandSide(node) | ||
)) { | ||
return; | ||
} | ||
if (identifiers.length === 0) { | ||
return { | ||
node: node.object.callee.property, | ||
messageId: ERROR_ZERO_INDEX, | ||
fix: fixer => [ | ||
fixer.replaceText(node.object.callee.property, 'find'), | ||
removeMemberExpressionProperty(fixer, node, sourceCode), | ||
], | ||
}; | ||
}); | ||
// `array.filter().shift()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'shift', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayFilterCall(node.callee.object) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_SHIFT, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'find'), | ||
...removeMethodCall(fixer, node, sourceCode), | ||
], | ||
}; | ||
}); | ||
// `const [foo] = array.filter()` | ||
context.on('VariableDeclarator', node => { | ||
if (!( | ||
node.id.type === 'ArrayPattern' | ||
&& node.id.elements.length === 1 | ||
&& node.id.elements[0] | ||
&& node.id.elements[0].type !== 'RestElement' | ||
&& isArrayFilterCall(node.init) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.init.callee.property, | ||
messageId: ERROR_DESTRUCTURING_DECLARATION, | ||
...fixDestructuringAndReplaceFilter(sourceCode, node), | ||
}; | ||
}); | ||
// `[foo] = array.filter()` | ||
context.on('AssignmentExpression', node => { | ||
if (!( | ||
node.left.type === 'ArrayPattern' | ||
&& node.left.elements.length === 1 | ||
&& node.left.elements[0] | ||
&& node.left.elements[0].type !== 'RestElement' | ||
&& isArrayFilterCall(node.right) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.right.callee.property, | ||
messageId: ERROR_DESTRUCTURING_ASSIGNMENT, | ||
...fixDestructuringAndReplaceFilter(sourceCode, node), | ||
}; | ||
}); | ||
// `const foo = array.filter(); foo[0]; [bar] = foo` | ||
context.on('VariableDeclarator', node => { | ||
if (!( | ||
node.id.type === 'Identifier' | ||
&& isArrayFilterCall(node.init) | ||
&& node.parent.type === 'VariableDeclaration' | ||
&& node.parent.declarations.includes(node) | ||
// Exclude `export const foo = [];` | ||
&& !( | ||
node.parent.parent.type === 'ExportNamedDeclaration' | ||
&& node.parent.parent.declaration === node.parent | ||
) | ||
)) { | ||
return; | ||
} | ||
const scope = sourceCode.getScope(node); | ||
const variable = findVariable(scope, node.id); | ||
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id); | ||
if (identifiers.length === 0) { | ||
return; | ||
} | ||
const zeroIndexNodes = []; | ||
const destructuringNodes = []; | ||
for (const identifier of identifiers) { | ||
if (isAccessingZeroIndex(identifier)) { | ||
zeroIndexNodes.push(identifier.parent); | ||
} else if (isDestructuringFirstElement(identifier)) { | ||
destructuringNodes.push(identifier.parent); | ||
} else { | ||
return; | ||
} | ||
} | ||
const zeroIndexNodes = []; | ||
const destructuringNodes = []; | ||
for (const identifier of identifiers) { | ||
if (isAccessingZeroIndex(identifier)) { | ||
zeroIndexNodes.push(identifier.parent); | ||
} else if (isDestructuringFirstElement(identifier)) { | ||
destructuringNodes.push(identifier.parent); | ||
} else { | ||
return; | ||
const problem = { | ||
node: node.init.callee.property, | ||
messageId: ERROR_DECLARATION, | ||
}; | ||
// `const [foo = bar] = baz` is not fixable | ||
if (!destructuringNodes.some(node => hasDefaultValue(node))) { | ||
problem.fix = function * (fixer) { | ||
yield fixer.replaceText(node.init.callee.property, 'find'); | ||
const singularName = singular(node.id.name); | ||
if (singularName) { | ||
// Rename variable to be singularized now that it refers to a single item in the array instead of the entire array. | ||
const singularizedName = avoidCapture(singularName, getScopes(scope)); | ||
yield * renameVariable(variable, singularizedName, fixer); | ||
// Prevent possible variable conflicts | ||
yield * extendFixRange(fixer, sourceCode.ast.range); | ||
} | ||
} | ||
const problem = { | ||
node: node.init.callee.property, | ||
messageId: ERROR_DECLARATION | ||
for (const node of zeroIndexNodes) { | ||
yield removeMemberExpressionProperty(fixer, node, sourceCode); | ||
} | ||
for (const node of destructuringNodes) { | ||
yield * fixDestructuring(node, sourceCode, fixer); | ||
} | ||
}; | ||
} | ||
// `const [foo = bar] = baz` is not fixable | ||
if (!destructuringNodes.some(node => hasDefaultValue(node))) { | ||
problem.fix = function * (fixer) { | ||
yield fixer.replaceText(node.init.callee.property, 'find'); | ||
return problem; | ||
}); | ||
const singularName = singular(node.id.name); | ||
if (singularName) { | ||
// Rename variable to be singularized now that it refers to a single item in the array instead of the entire array. | ||
const singularizedName = avoidCapture(singularName, getChildScopesRecursive(scope), context.parserOptions.ecmaVersion); | ||
yield * renameVariable(variable, singularizedName, fixer); | ||
if (!checkFromLast) { | ||
return; | ||
} | ||
// Prevent possible variable conflicts | ||
yield * extendFixRange(fixer, sourceCode.ast.range); | ||
} | ||
// `array.filter().pop()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'pop', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayFilterCall(node.callee.object) | ||
)) { | ||
return; | ||
} | ||
for (const node of zeroIndexNodes) { | ||
yield removeMemberExpressionProperty(fixer, node, sourceCode); | ||
} | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_POP, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'findLast'), | ||
...removeMethodCall(fixer, node, sourceCode), | ||
], | ||
}; | ||
}); | ||
for (const node of destructuringNodes) { | ||
yield * fixDestructuring(node, sourceCode, fixer); | ||
} | ||
}; | ||
} | ||
// `array.filter().at(-1)` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'at', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.arguments[0].type === 'UnaryExpression' | ||
&& node.arguments[0].operator === '-' | ||
&& node.arguments[0].prefix | ||
&& node.arguments[0].argument.type === 'Literal' | ||
&& node.arguments[0].argument.raw === '1' | ||
&& isArrayFilterCall(node.callee.object) | ||
)) { | ||
return; | ||
} | ||
return problem; | ||
} | ||
}; | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_AT_MINUS_ONE, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'findLast'), | ||
...removeMethodCall(fixer, node, sourceCode), | ||
], | ||
}; | ||
}); | ||
}; | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkFromLast: { | ||
type: 'boolean', | ||
// TODO: Change default value to `true`, or remove the option when targeting Node.js 18. | ||
default: false, | ||
}, | ||
}, | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -333,8 +423,9 @@ create, | ||
docs: { | ||
description: 'Prefer `.find(…)` over the first element from `.filter(…)`.' | ||
description: 'Prefer `.find(…)` and `.findLast(…)` over the first or last element from `.filter(…)`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {methodCallSelector, matches} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {removeMethodCall} = require('./fix/index.js'); | ||
@@ -8,18 +8,34 @@ | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `.flatMap(…)` over `.map(…).flat()`.' | ||
[MESSAGE_ID]: 'Prefer `.flatMap(…)` over `.map(…).flat()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector('flat'), | ||
matches([ | ||
'[arguments.length=0]', | ||
'[arguments.length=1][arguments.0.type="Literal"][arguments.0.raw="1"]' | ||
]), | ||
methodCallSelector({path: 'callee.object', name: 'map'}) | ||
].join(''); | ||
const ignored = ['React.Children', 'Children']; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](flatCallExpression) { | ||
CallExpression(callExpression) { | ||
if (!( | ||
isMethodCall(callExpression, { | ||
method: 'flat', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& ( | ||
callExpression.arguments.length === 0 | ||
|| ( | ||
callExpression.arguments.length === 1 | ||
&& callExpression.arguments[0].type === 'Literal' | ||
&& callExpression.arguments[0].raw === '1' | ||
) | ||
) | ||
&& isMethodCall(callExpression.callee.object, { | ||
method: 'map', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
)) { | ||
return; | ||
} | ||
const flatCallExpression = callExpression; | ||
const mapCallExpression = flatCallExpression.callee.object; | ||
@@ -30,3 +46,3 @@ if (isNodeMatches(mapCallExpression.callee.object, ignored)) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const mapProperty = mapCallExpression.callee.property; | ||
@@ -52,7 +68,8 @@ | ||
yield fixer.replaceText(mapProperty, 'flatMap'); | ||
} | ||
}, | ||
}; | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -63,7 +80,7 @@ create, | ||
docs: { | ||
description: 'Prefer `.flatMap(…)` over `.map(…).flat()`.' | ||
description: 'Prefer `.flatMap(…)` over `.map(…).flat()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const { | ||
methodCallSelector, | ||
arrayPrototypeMethodSelector, | ||
emptyArraySelector, | ||
callExpressionSelector | ||
} = require('./selectors/index.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js'); | ||
const {getParenthesizedText, isParenthesized} = require('./utils/parentheses.js'); | ||
getParenthesizedText, | ||
isArrayPrototypeProperty, | ||
isNodeMatches, | ||
isNodeMatchesNameOrPath, | ||
isParenthesized, | ||
isSameIdentifier, | ||
needsSemicolon, | ||
shouldAddParenthesesToMemberExpressionObject, | ||
} = require('./utils/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const { | ||
isMethodCall, | ||
isCallExpression, | ||
} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-array-flat'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `Array#flat()` over `{{description}}` to flatten an array.' | ||
[MESSAGE_ID]: 'Prefer `Array#flat()` over `{{description}}` to flatten an array.', | ||
}; | ||
const isEmptyArrayExpression = node => | ||
node.type === 'ArrayExpression' | ||
&& node.elements.length === 0; | ||
// `array.flatMap(x => x)` | ||
const arrayFlatMap = { | ||
selector: [ | ||
methodCallSelector({ | ||
name: 'flatMap', | ||
length: 1 | ||
}), | ||
'[arguments.0.type="ArrowFunctionExpression"]', | ||
'[arguments.0.async!=true]', | ||
'[arguments.0.generator!=true]', | ||
'[arguments.0.params.length=1]', | ||
'[arguments.0.params.0.type="Identifier"]', | ||
'[arguments.0.body.type="Identifier"]' | ||
].join(''), | ||
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.name, | ||
testFunction(node) { | ||
if (!isMethodCall(node, { | ||
method: 'flatMap', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return false; | ||
} | ||
const [firstArgument] = node.arguments; | ||
return ( | ||
firstArgument.type === 'ArrowFunctionExpression' | ||
&& !firstArgument.async | ||
&& firstArgument.params.length === 1 | ||
&& isSameIdentifier(firstArgument.params[0], firstArgument.body) | ||
); | ||
}, | ||
getArrayNode: node => node.callee.object, | ||
description: 'Array#flatMap()' | ||
description: 'Array#flatMap()', | ||
}; | ||
// `array.reduce((a, b) => a.concat(b), [])` | ||
// `array.reduce((a, b) => [...a, ...b], [])` | ||
const arrayReduce = { | ||
selector: [ | ||
methodCallSelector({ | ||
name: 'reduce', | ||
length: 2 | ||
}), | ||
'[arguments.0.type="ArrowFunctionExpression"]', | ||
'[arguments.0.async!=true]', | ||
'[arguments.0.generator!=true]', | ||
'[arguments.0.params.length=2]', | ||
'[arguments.0.params.0.type="Identifier"]', | ||
'[arguments.0.params.1.type="Identifier"]', | ||
methodCallSelector({ | ||
name: 'concat', | ||
length: 1, | ||
path: 'arguments.0.body' | ||
}), | ||
'[arguments.0.body.callee.object.type="Identifier"]', | ||
'[arguments.0.body.arguments.0.type="Identifier"]', | ||
emptyArraySelector('arguments.1') | ||
].join(''), | ||
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.callee.object.name && | ||
node.arguments[0].params[1].name === node.arguments[0].body.arguments[0].name, | ||
getArrayNode: node => node.callee.object, | ||
description: 'Array#reduce()' | ||
}; | ||
testFunction(node) { | ||
if (!isMethodCall(node, { | ||
method: 'reduce', | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return false; | ||
} | ||
// `array.reduce((a, b) => [...a, ...b], [])` | ||
const arrayReduce2 = { | ||
selector: [ | ||
methodCallSelector({ | ||
name: 'reduce', | ||
length: 2 | ||
}), | ||
'[arguments.0.type="ArrowFunctionExpression"]', | ||
'[arguments.0.async!=true]', | ||
'[arguments.0.generator!=true]', | ||
'[arguments.0.params.length=2]', | ||
'[arguments.0.params.0.type="Identifier"]', | ||
'[arguments.0.params.1.type="Identifier"]', | ||
'[arguments.0.body.type="ArrayExpression"]', | ||
'[arguments.0.body.elements.length=2]', | ||
'[arguments.0.body.elements.0.type="SpreadElement"]', | ||
'[arguments.0.body.elements.0.argument.type="Identifier"]', | ||
'[arguments.0.body.elements.1.type="SpreadElement"]', | ||
'[arguments.0.body.elements.1.argument.type="Identifier"]', | ||
emptyArraySelector('arguments.1') | ||
].join(''), | ||
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.elements[0].argument.name && | ||
node.arguments[0].params[1].name === node.arguments[0].body.elements[1].argument.name, | ||
const [firstArgument, secondArgument] = node.arguments; | ||
if (!( | ||
firstArgument.type === 'ArrowFunctionExpression' | ||
&& !firstArgument.async | ||
&& firstArgument.params.length === 2 | ||
&& isEmptyArrayExpression(secondArgument) | ||
)) { | ||
return false; | ||
} | ||
const firstArgumentBody = firstArgument.body; | ||
const [firstParameter, secondParameter] = firstArgument.params; | ||
return ( | ||
// `(a, b) => a.concat(b)` | ||
( | ||
isMethodCall(firstArgumentBody, { | ||
method: 'concat', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isSameIdentifier(firstParameter, firstArgumentBody.callee.object) | ||
&& isSameIdentifier(secondParameter, firstArgumentBody.arguments[0]) | ||
) | ||
// `(a, b) => [...a, ...b]` | ||
|| ( | ||
firstArgumentBody.type === 'ArrayExpression' | ||
&& firstArgumentBody.elements.length === 2 | ||
&& firstArgumentBody.elements.every((node, index) => | ||
node?.type === 'SpreadElement' | ||
&& node.argument.type === 'Identifier' | ||
&& isSameIdentifier(firstArgument.params[index], node.argument), | ||
) | ||
) | ||
); | ||
}, | ||
getArrayNode: node => node.callee.object, | ||
description: 'Array#reduce()' | ||
description: 'Array#reduce()', | ||
}; | ||
// `[].concat(maybeArray)` and `[].concat(...array)` | ||
// `[].concat(maybeArray)` | ||
// `[].concat(...array)` | ||
const emptyArrayConcat = { | ||
selector: [ | ||
methodCallSelector({ | ||
name: 'concat', | ||
length: 1, | ||
allowSpreadElement: true | ||
}), | ||
emptyArraySelector('callee.object') | ||
].join(''), | ||
getArrayNode: node => { | ||
testFunction(node) { | ||
return isMethodCall(node, { | ||
method: 'concat', | ||
argumentsLength: 1, | ||
allowSpreadElement: true, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isEmptyArrayExpression(node.callee.object); | ||
}, | ||
getArrayNode(node) { | ||
const argumentNode = node.arguments[0]; | ||
@@ -107,3 +122,3 @@ return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode; | ||
description: '[].concat()', | ||
shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement' | ||
shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement', | ||
}; | ||
@@ -115,16 +130,26 @@ | ||
const arrayPrototypeConcat = { | ||
selector: [ | ||
methodCallSelector({ | ||
names: ['apply', 'call'], | ||
length: 2, | ||
allowSpreadElement: true | ||
}), | ||
emptyArraySelector('arguments.0'), | ||
arrayPrototypeMethodSelector({ | ||
path: 'callee.object', | ||
name: 'concat' | ||
}) | ||
].join(''), | ||
testFunction: node => node.arguments[1].type !== 'SpreadElement' || node.callee.property.name === 'call', | ||
getArrayNode: node => { | ||
testFunction(node) { | ||
if (!( | ||
isMethodCall(node, { | ||
methods: ['apply', 'call'], | ||
argumentsLength: 2, | ||
allowSpreadElement: true, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayPrototypeProperty(node.callee.object, { | ||
property: 'concat', | ||
}) | ||
)) { | ||
return false; | ||
} | ||
const [firstArgument, secondArgument] = node.arguments; | ||
return isEmptyArrayExpression(firstArgument) | ||
&& ( | ||
node.callee.property.name === 'call' | ||
|| secondArgument.type !== 'SpreadElement' | ||
); | ||
}, | ||
getArrayNode(node) { | ||
const argumentNode = node.arguments[1]; | ||
@@ -134,3 +159,3 @@ return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode; | ||
description: 'Array.prototype.concat()', | ||
shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call' | ||
shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call', | ||
}; | ||
@@ -141,8 +166,4 @@ | ||
'lodash.flatten', | ||
'underscore.flatten' | ||
'underscore.flatten', | ||
]; | ||
const anyCall = { | ||
selector: callExpressionSelector({length: 1}), | ||
getArrayNode: node => node.arguments[0] | ||
}; | ||
@@ -154,3 +175,3 @@ function fix(node, array, sourceCode, shouldSwitchToArray) { | ||
return fixer => { | ||
return function * (fixer) { | ||
let fixed = getParenthesizedText(array, sourceCode); | ||
@@ -162,4 +183,4 @@ if (shouldSwitchToArray) { | ||
} else if ( | ||
!isParenthesized(array, sourceCode) && | ||
shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
!isParenthesized(array, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
@@ -176,3 +197,5 @@ fixed = `(${fixed})`; | ||
return fixer.replaceText(node, fixed); | ||
yield fixer.replaceText(node, fixed); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}; | ||
@@ -184,7 +207,5 @@ } | ||
functions: [], | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
const functions = [...configFunctions, ...lodashFlattenFunctions]; | ||
const sourceCode = context.getSourceCode(); | ||
const listeners = {}; | ||
@@ -194,43 +215,47 @@ const cases = [ | ||
arrayReduce, | ||
arrayReduce2, | ||
emptyArrayConcat, | ||
arrayPrototypeConcat, | ||
{ | ||
...anyCall, | ||
testFunction: node => isNodeMatches(node.callee, functions), | ||
description: node => `${functions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)).trim()}()` | ||
} | ||
testFunction: node => isCallExpression(node, { | ||
argumentsLength: 1, | ||
optional: false, | ||
}) && isNodeMatches(node.callee, functions), | ||
getArrayNode: node => node.arguments[0], | ||
description: node => `${functions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)).trim()}()`, | ||
}, | ||
]; | ||
for (const {selector, testFunction, description, getArrayNode, shouldSwitchToArray} of cases) { | ||
listeners[selector] = function (node) { | ||
if (testFunction && !testFunction(node)) { | ||
return; | ||
} | ||
return { | ||
* CallExpression(node) { | ||
for (const {testFunction, description, getArrayNode, shouldSwitchToArray} of cases) { | ||
if (!testFunction(node)) { | ||
continue; | ||
} | ||
const array = getArrayNode(node); | ||
const array = getArrayNode(node); | ||
const data = { | ||
description: typeof description === 'string' ? description : description(node) | ||
}; | ||
const data = { | ||
description: typeof description === 'string' ? description : description(node), | ||
}; | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data | ||
}; | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data, | ||
}; | ||
// Don't fix if it has comments. | ||
if ( | ||
sourceCode.getCommentsInside(node).length === | ||
sourceCode.getCommentsInside(array).length | ||
) { | ||
problem.fix = fix(node, array, sourceCode, shouldSwitchToArray); | ||
} | ||
const {sourceCode} = context; | ||
return problem; | ||
}; | ||
} | ||
// Don't fix if it has comments. | ||
if ( | ||
sourceCode.getCommentsInside(node).length | ||
=== sourceCode.getCommentsInside(array).length | ||
) { | ||
problem.fix = fix(node, array, sourceCode, shouldSwitchToArray); | ||
} | ||
return listeners; | ||
yield problem; | ||
} | ||
}, | ||
}; | ||
} | ||
@@ -241,12 +266,13 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
functions: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -257,8 +283,8 @@ create, | ||
docs: { | ||
description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.' | ||
description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const simpleArraySearchRule = require('./shared/simple-array-search-rule.js'); | ||
const {messages, createListeners} = simpleArraySearchRule({ | ||
const indexOfOverFindIndexRule = simpleArraySearchRule({ | ||
method: 'findIndex', | ||
replacement: 'indexOf' | ||
replacement: 'indexOf', | ||
}); | ||
const lastIndexOfOverFindLastIndexRule = simpleArraySearchRule({ | ||
method: 'findLastIndex', | ||
replacement: 'lastIndexOf', | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create: context => createListeners(context), | ||
create(context) { | ||
indexOfOverFindIndexRule.listen(context); | ||
lastIndexOfOverFindLastIndexRule.listen(context); | ||
}, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Prefer `Array#indexOf()` over `Array#findIndex()` when looking for the index of an item.' | ||
description: 'Prefer `Array#{indexOf,lastIndexOf}()` over `Array#{findIndex,findLastIndex}()` when looking for the index of an item.', | ||
}, | ||
fixable: 'code', | ||
messages, | ||
hasSuggestions: true | ||
} | ||
hasSuggestions: true, | ||
messages: { | ||
...indexOfOverFindIndexRule.messages, | ||
...lastIndexOfOverFindLastIndexRule.messages, | ||
}, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {isBooleanNode} = require('./utils/boolean.js'); | ||
const {getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const { | ||
isBooleanNode, | ||
getParenthesizedRange, | ||
isNodeValueNotFunction, | ||
} = require('./utils/index.js'); | ||
const {removeMemberExpressionProperty} = require('./fix/index.js'); | ||
const {isLiteral, isUndefined, isMethodCall, isMemberExpression} = require('./ast/index.js'); | ||
@@ -12,83 +15,133 @@ const ERROR_ID_ARRAY_SOME = 'some'; | ||
const messages = { | ||
[ERROR_ID_ARRAY_SOME]: 'Prefer `.some(…)` over `.find(…)`.', | ||
[SUGGESTION_ID_ARRAY_SOME]: 'Replace `.find(…)` with `.some(…)`.', | ||
[ERROR_ID_ARRAY_FILTER]: 'Prefer `.some(…)` over non-zero length check from `.filter(…)`.' | ||
[ERROR_ID_ARRAY_SOME]: 'Prefer `.some(…)` over `.{{method}}(…)`.', | ||
[SUGGESTION_ID_ARRAY_SOME]: 'Replace `.{{method}}(…)` with `.some(…)`.', | ||
[ERROR_ID_ARRAY_FILTER]: 'Prefer `.some(…)` over non-zero length check from `.filter(…)`.', | ||
}; | ||
const arrayFindCallSelector = methodCallSelector({ | ||
name: 'find', | ||
min: 1, | ||
max: 2 | ||
}); | ||
const isCheckingUndefined = node => | ||
node.parent.type === 'BinaryExpression' | ||
// Not checking yoda expression `null != foo.find()` and `undefined !== foo.find() | ||
&& node.parent.left === node | ||
&& ( | ||
( | ||
( | ||
node.parent.operator === '!=' | ||
|| node.parent.operator === '==' | ||
|| node.parent.operator === '===' | ||
|| node.parent.operator === '!==' | ||
) | ||
&& isUndefined(node.parent.right) | ||
) | ||
|| ( | ||
( | ||
node.parent.operator === '!=' | ||
|| node.parent.operator === '==' | ||
) | ||
// eslint-disable-next-line unicorn/no-null | ||
&& isLiteral(node.parent.right, null) | ||
) | ||
); | ||
const arrayFilterCallSelector = [ | ||
'BinaryExpression', | ||
'[right.type="Literal"]', | ||
// We assume the user already follows `unicorn/explicit-length-check`, these are allowed in that rule | ||
matches([ | ||
'[operator=">"][right.raw="0"]', | ||
'[operator="!=="][right.raw="0"]', | ||
'[operator=">="][right.raw="1"]' | ||
]), | ||
' > ', | ||
`${memberExpressionSelector('length')}.left`, | ||
' > ', | ||
`${methodCallSelector('filter')}.object` | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
methods: ['find', 'findLast'], | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const create = context => { | ||
return { | ||
[arrayFindCallSelector](findCall) { | ||
if (!isBooleanNode(findCall)) { | ||
return; | ||
} | ||
const isCompare = isCheckingUndefined(callExpression); | ||
if (!isCompare && !isBooleanNode(callExpression)) { | ||
return; | ||
} | ||
const findProperty = findCall.callee.property; | ||
return { | ||
node: findProperty, | ||
messageId: ERROR_ID_ARRAY_SOME, | ||
suggest: [ | ||
{ | ||
messageId: SUGGESTION_ID_ARRAY_SOME, | ||
fix: fixer => fixer.replaceText(findProperty, 'some') | ||
} | ||
] | ||
}; | ||
}, | ||
[arrayFilterCallSelector](filterCall) { | ||
const filterProperty = filterCall.callee.property; | ||
return { | ||
node: filterProperty, | ||
messageId: ERROR_ID_ARRAY_FILTER, | ||
* fix(fixer) { | ||
// `.filter` to `.some` | ||
yield fixer.replaceText(filterProperty, 'some'); | ||
const methodNode = callExpression.callee.property; | ||
return { | ||
node: methodNode, | ||
messageId: ERROR_ID_ARRAY_SOME, | ||
data: {method: methodNode.name}, | ||
suggest: [ | ||
{ | ||
messageId: SUGGESTION_ID_ARRAY_SOME, | ||
* fix(fixer) { | ||
yield fixer.replaceText(methodNode, 'some'); | ||
const sourceCode = context.getSourceCode(); | ||
const lengthNode = filterCall.parent; | ||
/* | ||
Remove `.length` | ||
`(( (( array.filter() )).length )) > (( 0 ))` | ||
------------------------^^^^^^^ | ||
*/ | ||
yield removeMemberExpressionProperty(fixer, lengthNode, sourceCode); | ||
if (!isCompare) { | ||
return; | ||
} | ||
const compareNode = lengthNode.parent; | ||
/* | ||
Remove `> 0` | ||
`(( (( array.filter() )).length )) > (( 0 ))` | ||
----------------------------------^^^^^^^^^^ | ||
*/ | ||
yield fixer.removeRange([ | ||
getParenthesizedRange(lengthNode, sourceCode)[1], | ||
compareNode.range[1] | ||
]); | ||
const parenthesizedRange = getParenthesizedRange(callExpression, context.sourceCode); | ||
yield fixer.replaceTextRange([parenthesizedRange[1], callExpression.parent.range[1]], ''); | ||
// The `BinaryExpression` always ends with a number or `)`, no need check for ASI | ||
} | ||
}; | ||
if (callExpression.parent.operator === '!=' || callExpression.parent.operator === '!==') { | ||
return; | ||
} | ||
yield fixer.insertTextBeforeRange(parenthesizedRange, '!'); | ||
}, | ||
}, | ||
], | ||
}; | ||
}, | ||
BinaryExpression(binaryExpression) { | ||
if (!( | ||
// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule. | ||
(binaryExpression.operator === '>' || binaryExpression.operator === '!==') | ||
&& binaryExpression.right.type === 'Literal' | ||
&& binaryExpression.right.raw === '0' | ||
&& isMemberExpression(binaryExpression.left, {property: 'length', optional: false}) | ||
&& isMethodCall(binaryExpression.left.object, { | ||
method: 'filter', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
)) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const filterCall = binaryExpression.left.object; | ||
const [firstArgument] = filterCall.arguments; | ||
if (!firstArgument || isNodeValueNotFunction(firstArgument)) { | ||
return; | ||
} | ||
const filterProperty = filterCall.callee.property; | ||
return { | ||
node: filterProperty, | ||
messageId: ERROR_ID_ARRAY_FILTER, | ||
* fix(fixer) { | ||
// `.filter` to `.some` | ||
yield fixer.replaceText(filterProperty, 'some'); | ||
const {sourceCode} = context; | ||
const lengthNode = binaryExpression.left; | ||
/* | ||
Remove `.length` | ||
`(( (( array.filter() )).length )) > (( 0 ))` | ||
------------------------^^^^^^^ | ||
*/ | ||
yield removeMemberExpressionProperty(fixer, lengthNode, sourceCode); | ||
/* | ||
Remove `> 0` | ||
`(( (( array.filter() )).length )) > (( 0 ))` | ||
----------------------------------^^^^^^^^^^ | ||
*/ | ||
yield fixer.removeRange([ | ||
getParenthesizedRange(lengthNode, sourceCode)[1], | ||
binaryExpression.range[1], | ||
]); | ||
// The `BinaryExpression` always ends with a number or `)`, no need check for ASI | ||
}, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -99,8 +152,8 @@ create: checkVueTemplate(create), | ||
docs: { | ||
description: 'Prefer `.some(…)` over `.filter(…).length` check and `.find(…)`.' | ||
description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`.', | ||
}, | ||
fixable: 'code', | ||
messages, | ||
hasSuggestions: true | ||
} | ||
hasSuggestions: true, | ||
}, | ||
}; |
'use strict'; | ||
const {isOpeningBracketToken, isClosingBracketToken, getStaticValue} = require('eslint-utils'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const {isOpeningBracketToken, isClosingBracketToken, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const { | ||
isParenthesized, | ||
getParenthesizedRange, | ||
getParenthesizedText | ||
} = require('./utils/parentheses.js'); | ||
const {isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const isLeftHandSide = require('./utils/is-left-hand-side.js'); | ||
getParenthesizedText, | ||
isNodeMatchesNameOrPath, | ||
needsSemicolon, | ||
shouldAddParenthesesToMemberExpressionObject, | ||
isLeftHandSide, | ||
} = require('./utils/index.js'); | ||
const { | ||
getNegativeIndexLengthNode, | ||
removeLengthNode | ||
removeLengthNode, | ||
} = require('./shared/negative-index.js'); | ||
const {methodCallSelector, callExpressionSelector, notLeftHandSideSelector} = require('./selectors/index.js'); | ||
const {removeMemberExpressionProperty, removeMethodCall} = require('./fix/index.js'); | ||
const {isLiteral, isCallExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -34,28 +33,21 @@ const MESSAGE_ID_NEGATIVE_INDEX = 'negative-index'; | ||
[MESSAGE_ID_GET_LAST_FUNCTION]: 'Prefer `.at(-1)` over `{{description}}(…)` to get the last element.', | ||
[SUGGESTION_ID]: 'Use `.at(…)`.' | ||
[SUGGESTION_ID]: 'Use `.at(…)`.', | ||
}; | ||
const indexAccess = [ | ||
'MemberExpression', | ||
'[optional!=true]', | ||
'[computed!=false]', | ||
notLeftHandSideSelector() | ||
].join(''); | ||
const sliceCall = methodCallSelector({name: 'slice', min: 1, max: 2}); | ||
const stringCharAt = methodCallSelector({name: 'charAt', length: 1}); | ||
const isArguments = node => node.type === 'Identifier' && node.name === 'arguments'; | ||
const isLiteralNegativeInteger = node => | ||
node.type === 'UnaryExpression' && | ||
node.prefix && | ||
node.operator === '-' && | ||
node.argument.type === 'Literal' && | ||
Number.isInteger(node.argument.value) && | ||
node.argument.value > 0; | ||
node.type === 'UnaryExpression' | ||
&& node.prefix | ||
&& node.operator === '-' | ||
&& node.argument.type === 'Literal' | ||
&& Number.isInteger(node.argument.value) | ||
&& node.argument.value > 0; | ||
const isZeroIndexAccess = node => { | ||
const {parent} = node; | ||
return parent.type === 'MemberExpression' && | ||
!parent.optional && | ||
parent.computed && | ||
parent.object === node && | ||
isLiteralValue(parent.property, 0); | ||
return parent.type === 'MemberExpression' | ||
&& !parent.optional | ||
&& parent.computed | ||
&& parent.object === node | ||
&& isLiteral(parent.property, 0); | ||
}; | ||
@@ -65,12 +57,12 @@ | ||
const {parent} = node; | ||
return parent.type === 'MemberExpression' && | ||
!parent.optional && | ||
!parent.computed && | ||
parent.object === node && | ||
parent.property.type === 'Identifier' && | ||
parent.property.name === method && | ||
parent.parent.type === 'CallExpression' && | ||
parent.parent.callee === parent && | ||
!parent.parent.optional && | ||
parent.parent.arguments.length === 0; | ||
return parent.type === 'MemberExpression' | ||
&& !parent.optional | ||
&& !parent.computed | ||
&& parent.object === node | ||
&& parent.property.type === 'Identifier' | ||
&& parent.property.name === method | ||
&& parent.parent.type === 'CallExpression' | ||
&& parent.parent.callee === parent | ||
&& !parent.parent.optional | ||
&& parent.parent.arguments.length === 0; | ||
}; | ||
@@ -109,5 +101,5 @@ | ||
if ( | ||
firstElementGetMethod === 'zero-index' || | ||
firstElementGetMethod === 'shift' || | ||
(startIndex === -1 && firstElementGetMethod === 'pop') | ||
firstElementGetMethod === 'zero-index' | ||
|| firstElementGetMethod === 'shift' | ||
|| (startIndex === -1 && firstElementGetMethod === 'pop') | ||
) { | ||
@@ -121,4 +113,4 @@ return {safeToFix: true, firstElementGetMethod}; | ||
if ( | ||
isLiteralNegativeInteger(endIndexNode) && | ||
-endIndexNode.argument.value === startIndex + 1 | ||
isLiteralNegativeInteger(endIndexNode) | ||
&& -endIndexNode.argument.value === startIndex + 1 | ||
) { | ||
@@ -138,3 +130,3 @@ return {safeToFix: true, firstElementGetMethod}; | ||
'lodash.last', | ||
'underscore.last' | ||
'underscore.last', | ||
]; | ||
@@ -146,31 +138,103 @@ | ||
getLastElementFunctions, | ||
checkAllIndexAccess | ||
checkAllIndexAccess, | ||
} = { | ||
getLastElementFunctions: [], | ||
checkAllIndexAccess: false, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
const getLastFunctions = [...getLastElementFunctions, ...lodashLastFunctions]; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[indexAccess](node) { | ||
const indexNode = node.property; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.object); | ||
// Index access | ||
context.on('MemberExpression', node => { | ||
if ( | ||
node.optional | ||
|| !node.computed | ||
|| isLeftHandSide(node) | ||
) { | ||
return; | ||
} | ||
if (!lengthNode) { | ||
if (!checkAllIndexAccess) { | ||
return; | ||
} | ||
const indexNode = node.property; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.object); | ||
// Only if we are sure it's an positive integer | ||
const staticValue = getStaticValue(indexNode, context.getScope()); | ||
if (!staticValue || !Number.isInteger(staticValue.value) || staticValue.value < 0) { | ||
return; | ||
if (!lengthNode) { | ||
if (!checkAllIndexAccess) { | ||
return; | ||
} | ||
// Only if we are sure it's an positive integer | ||
const staticValue = getStaticValue(indexNode, sourceCode.getScope(indexNode)); | ||
if (!staticValue || !Number.isInteger(staticValue.value) || staticValue.value < 0) { | ||
return; | ||
} | ||
} | ||
const problem = { | ||
node: indexNode, | ||
messageId: lengthNode ? MESSAGE_ID_NEGATIVE_INDEX : MESSAGE_ID_INDEX, | ||
}; | ||
if (isArguments(node.object)) { | ||
return problem; | ||
} | ||
problem.fix = function * (fixer) { | ||
if (lengthNode) { | ||
yield removeLengthNode(lengthNode, fixer, sourceCode); | ||
} | ||
// Only remove space for `foo[foo.length - 1]` | ||
if ( | ||
indexNode.type === 'BinaryExpression' | ||
&& indexNode.operator === '-' | ||
&& indexNode.left === lengthNode | ||
&& indexNode.right.type === 'Literal' | ||
&& /^\d+$/.test(indexNode.right.raw) | ||
) { | ||
const numberNode = indexNode.right; | ||
const tokenBefore = sourceCode.getTokenBefore(numberNode); | ||
if ( | ||
tokenBefore.type === 'Punctuator' | ||
&& tokenBefore.value === '-' | ||
&& /^\s+$/.test(sourceCode.text.slice(tokenBefore.range[1], numberNode.range[0])) | ||
) { | ||
yield fixer.removeRange([tokenBefore.range[1], numberNode.range[0]]); | ||
} | ||
} | ||
return { | ||
node: indexNode, | ||
messageId: lengthNode ? MESSAGE_ID_NEGATIVE_INDEX : MESSAGE_ID_INDEX, | ||
const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken); | ||
yield fixer.replaceText(openingBracketToken, '.at('); | ||
const closingBracketToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken); | ||
yield fixer.replaceText(closingBracketToken, ')'); | ||
}; | ||
return problem; | ||
}); | ||
// `string.charAt` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'charAt', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const [indexNode] = node.arguments; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.callee.object); | ||
// `String#charAt` don't care about index value, we assume it's always number | ||
if (!lengthNode && !checkAllIndexAccess) { | ||
return; | ||
} | ||
return { | ||
node: indexNode, | ||
messageId: lengthNode ? MESSAGE_ID_STRING_CHAR_AT_NEGATIVE : MESSAGE_ID_STRING_CHAR_AT, | ||
suggest: [{ | ||
messageId: SUGGESTION_ID, | ||
* fix(fixer) { | ||
@@ -181,109 +245,105 @@ if (lengthNode) { | ||
const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken); | ||
yield fixer.replaceText(openingBracketToken, '.at('); | ||
yield fixer.replaceText(node.callee.property, 'at'); | ||
}, | ||
}], | ||
}; | ||
}); | ||
const isClosingBraceToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken); | ||
yield fixer.replaceText(isClosingBraceToken, ')'); | ||
} | ||
}; | ||
}, | ||
[stringCharAt](node) { | ||
const [indexNode] = node.arguments; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.callee.object); | ||
// `.slice()` | ||
context.on('CallExpression', sliceCall => { | ||
if (!isMethodCall(sliceCall, { | ||
method: 'slice', | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
// `String#charAt` don't care about index value, we assume it's always number | ||
if (!lengthNode && !checkAllIndexAccess) { | ||
return; | ||
} | ||
const result = checkSliceCall(sliceCall); | ||
if (!result) { | ||
return; | ||
} | ||
return { | ||
node: indexNode, | ||
messageId: lengthNode ? MESSAGE_ID_STRING_CHAR_AT_NEGATIVE : MESSAGE_ID_STRING_CHAR_AT, | ||
suggest: [{ | ||
messageId: SUGGESTION_ID, | ||
* fix(fixer) { | ||
if (lengthNode) { | ||
yield removeLengthNode(lengthNode, fixer, sourceCode); | ||
} | ||
const {safeToFix, firstElementGetMethod} = result; | ||
yield fixer.replaceText(node.callee.property, 'at'); | ||
} | ||
}] | ||
}; | ||
}, | ||
[sliceCall](sliceCall) { | ||
const result = checkSliceCall(sliceCall); | ||
if (!result) { | ||
return; | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
function * fix(fixer) { | ||
// `.slice` to `.at` | ||
yield fixer.replaceText(sliceCall.callee.property, 'at'); | ||
// Remove extra arguments | ||
if (sliceCall.arguments.length !== 1) { | ||
const [, start] = getParenthesizedRange(sliceCall.arguments[0], sourceCode); | ||
const [end] = sourceCode.getLastToken(sliceCall).range; | ||
yield fixer.removeRange([start, end]); | ||
} | ||
const {safeToFix, firstElementGetMethod} = result; | ||
// Remove `[0]`, `.shift()`, or `.pop()` | ||
if (firstElementGetMethod === 'zero-index') { | ||
yield removeMemberExpressionProperty(fixer, sliceCall.parent, sourceCode); | ||
} else { | ||
yield * removeMethodCall(fixer, sliceCall.parent.parent, sourceCode); | ||
} | ||
} | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
function * fix(fixer) { | ||
// `.slice` to `.at` | ||
yield fixer.replaceText(sliceCall.callee.property, 'at'); | ||
const problem = { | ||
node: sliceCall.callee.property, | ||
messageId: MESSAGE_ID_SLICE, | ||
}; | ||
// Remove extra arguments | ||
if (sliceCall.arguments.length !== 1) { | ||
const [, start] = getParenthesizedRange(sliceCall.arguments[0], sourceCode); | ||
const [end] = sourceCode.getLastToken(sliceCall).range; | ||
yield fixer.removeRange([start, end]); | ||
} | ||
if (safeToFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_ID, fix}]; | ||
} | ||
// Remove `[0]`, `.shift()`, or `.pop()` | ||
if (firstElementGetMethod === 'zero-index') { | ||
yield removeMemberExpressionProperty(fixer, sliceCall.parent, sourceCode); | ||
} else { | ||
yield * removeMethodCall(fixer, sliceCall.parent.parent, sourceCode); | ||
} | ||
} | ||
return problem; | ||
}); | ||
const problem = { | ||
node: sliceCall.callee.property, | ||
messageId: MESSAGE_ID_SLICE | ||
}; | ||
context.on('CallExpression', node => { | ||
if (!isCallExpression(node, {argumentsLength: 1, optional: false})) { | ||
return; | ||
} | ||
if (safeToFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_ID, fix}]; | ||
} | ||
const matchedFunction = getLastFunctions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)); | ||
if (!matchedFunction) { | ||
return; | ||
} | ||
const problem = { | ||
node: node.callee, | ||
messageId: MESSAGE_ID_GET_LAST_FUNCTION, | ||
data: {description: matchedFunction.trim()}, | ||
}; | ||
const [array] = node.arguments; | ||
if (isArguments(array)) { | ||
return problem; | ||
}, | ||
[callExpressionSelector({length: 1})](node) { | ||
const matchedFunction = getLastFunctions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)); | ||
if (!matchedFunction) { | ||
return; | ||
} | ||
} | ||
return { | ||
node: node.callee, | ||
messageId: MESSAGE_ID_GET_LAST_FUNCTION, | ||
data: {description: matchedFunction.trim()}, | ||
fix(fixer) { | ||
const [array] = node.arguments; | ||
problem.fix = function (fixer) { | ||
let fixed = getParenthesizedText(array, sourceCode); | ||
let fixed = getParenthesizedText(array, sourceCode); | ||
if ( | ||
!isParenthesized(array, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
} | ||
if ( | ||
!isParenthesized(array, sourceCode) && | ||
shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
} | ||
fixed = `${fixed}.at(-1)`; | ||
fixed = `${fixed}.at(-1)`; | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
if (needsSemicolon(tokenBefore, sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
if (needsSemicolon(tokenBefore, sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
return fixer.replaceText(node, fixed); | ||
}; | ||
return fixer.replaceText(node, fixed); | ||
} | ||
}; | ||
} | ||
}; | ||
return problem; | ||
}); | ||
} | ||
@@ -294,16 +354,17 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
getLastElementFunctions: { | ||
type: 'array', | ||
uniqueItems: true | ||
uniqueItems: true, | ||
}, | ||
checkAllIndexAccess: { | ||
type: 'boolean', | ||
default: false | ||
} | ||
default: false, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -314,9 +375,9 @@ create, | ||
docs: { | ||
description: 'Prefer `.at()` method for index access and `String#charAt()`.' | ||
description: 'Prefer `.at()` method for index access and `String#charAt()`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const { | ||
matches, | ||
methodCallSelector, | ||
newExpressionSelector, | ||
callExpressionSelector | ||
} = require('./selectors/index.js'); | ||
isMethodCall, | ||
isCallExpression, | ||
isNewExpression, | ||
} = require('./ast/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
@@ -15,81 +15,112 @@ const MESSAGE_ID_DEFAULT = 'prefer-date'; | ||
[MESSAGE_ID_METHOD]: 'Prefer `Date.now()` over `Date#{{method}}()`.', | ||
[MESSAGE_ID_NUMBER]: 'Prefer `Date.now()` over `Number(new Date())`.' | ||
[MESSAGE_ID_NUMBER]: 'Prefer `Date.now()` over `Number(new Date())`.', | ||
}; | ||
const createNewDateSelector = path => newExpressionSelector({path, name: 'Date', length: 0}); | ||
const operatorsSelector = (...operators) => matches(operators.map(operator => `[operator="${operator}"]`)); | ||
// `new Date()` | ||
const newDateSelector = createNewDateSelector(); | ||
// `new Date().{getTime,valueOf}()` | ||
const methodsSelector = [ | ||
methodCallSelector({ | ||
names: ['getTime', 'valueOf'], | ||
length: 0 | ||
}), | ||
createNewDateSelector('callee.object') | ||
].join(''); | ||
// `{Number,BigInt}(new Date())` | ||
const builtinObjectSelector = [ | ||
callExpressionSelector({names: ['Number', 'BigInt'], length: 1}), | ||
createNewDateSelector('arguments.0') | ||
].join(''); | ||
// https://github.com/estree/estree/blob/master/es5.md#unaryoperator | ||
const unaryExpressionsSelector = [ | ||
'UnaryExpression', | ||
operatorsSelector('+', '-'), | ||
createNewDateSelector('argument') | ||
].join(''); | ||
const assignmentExpressionSelector = [ | ||
'AssignmentExpression', | ||
operatorsSelector('-=', '*=', '/=', '%=', '**='), | ||
'>', | ||
`${newDateSelector}.right` | ||
].join(''); | ||
const binaryExpressionSelector = [ | ||
'BinaryExpression', | ||
operatorsSelector('-', '*', '/', '%', '**'), | ||
// Both `left` and `right` properties | ||
'>', | ||
newDateSelector | ||
].join(''); | ||
const isNewDate = node => isNewExpression(node, {name: 'Date', argumentsLength: 0}); | ||
const getProblem = (node, problem) => ({ | ||
const getProblem = (node, problem, sourceCode) => ({ | ||
node, | ||
messageId: MESSAGE_ID_DEFAULT, | ||
fix: fixer => fixer.replaceText(node, 'Date.now()'), | ||
...problem | ||
* fix(fixer) { | ||
yield fixer.replaceText(node, 'Date.now()'); | ||
if (node.type === 'UnaryExpression') { | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
} | ||
}, | ||
...problem, | ||
}); | ||
const create = () => { | ||
return { | ||
[methodsSelector](node) { | ||
const method = node.callee.property; | ||
return getProblem(node, { | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(callExpression) { | ||
// `new Date().{getTime,valueOf}()` | ||
if ( | ||
isMethodCall(callExpression, { | ||
methods: ['getTime', 'valueOf'], | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isNewDate(callExpression.callee.object) | ||
) { | ||
const method = callExpression.callee.property; | ||
return getProblem(callExpression, { | ||
node: method, | ||
messageId: MESSAGE_ID_METHOD, | ||
data: {method: method.name} | ||
data: {method: method.name}, | ||
}); | ||
}, | ||
[builtinObjectSelector](node) { | ||
const {name} = node.callee; | ||
} | ||
// `{Number,BigInt}(new Date())` | ||
if ( | ||
isCallExpression(callExpression, { | ||
names: ['Number', 'BigInt'], | ||
argumentsLength: 1, | ||
optional: false, | ||
}) | ||
&& isNewDate(callExpression.arguments[0]) | ||
) { | ||
const {name} = callExpression.callee; | ||
if (name === 'Number') { | ||
return getProblem(node, { | ||
messageId: MESSAGE_ID_NUMBER | ||
return getProblem(callExpression, { | ||
messageId: MESSAGE_ID_NUMBER, | ||
}); | ||
} | ||
return getProblem(node.arguments[0]); | ||
}, | ||
[unaryExpressionsSelector](node) { | ||
return getProblem(node.operator === '-' ? node.argument : node); | ||
}, | ||
[assignmentExpressionSelector](node) { | ||
return getProblem(node); | ||
}, | ||
[binaryExpressionSelector](node) { | ||
return getProblem(node); | ||
return getProblem(callExpression.arguments[0]); | ||
} | ||
}; | ||
}; | ||
}, | ||
UnaryExpression(unaryExpression) { | ||
// https://github.com/estree/estree/blob/master/es5.md#unaryoperator | ||
if ( | ||
unaryExpression.operator !== '+' | ||
&& unaryExpression.operator !== '-' | ||
) { | ||
return; | ||
} | ||
if (isNewDate(unaryExpression.argument)) { | ||
return getProblem( | ||
unaryExpression.operator === '-' ? unaryExpression.argument : unaryExpression, | ||
{}, | ||
context.sourceCode, | ||
); | ||
} | ||
}, | ||
AssignmentExpression(assignmentExpression) { | ||
if ( | ||
assignmentExpression.operator !== '-=' | ||
&& assignmentExpression.operator !== '*=' | ||
&& assignmentExpression.operator !== '/=' | ||
&& assignmentExpression.operator !== '%=' | ||
&& assignmentExpression.operator !== '**=' | ||
) { | ||
return; | ||
} | ||
if (isNewDate(assignmentExpression.right)) { | ||
return getProblem(assignmentExpression.right); | ||
} | ||
}, | ||
* BinaryExpression(binaryExpression) { | ||
if ( | ||
binaryExpression.operator !== '-' | ||
&& binaryExpression.operator !== '*' | ||
&& binaryExpression.operator !== '/' | ||
&& binaryExpression.operator !== '%' | ||
&& binaryExpression.operator !== '**' | ||
) { | ||
return; | ||
} | ||
for (const node of [binaryExpression.left, binaryExpression.right]) { | ||
if (isNewDate(node)) { | ||
yield getProblem(node); | ||
} | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -100,7 +131,7 @@ create, | ||
docs: { | ||
description: 'Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch.' | ||
description: 'Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const {functionTypes} = require('./ast/index.js'); | ||
@@ -7,20 +8,10 @@ const MESSAGE_ID = 'preferDefaultParameters'; | ||
const assignmentSelector = [ | ||
'ExpressionStatement', | ||
'[expression.type="AssignmentExpression"]' | ||
].join(''); | ||
const declarationSelector = [ | ||
'VariableDeclaration', | ||
'[declarations.0.type="VariableDeclarator"]' | ||
].join(''); | ||
const isDefaultExpression = (left, right) => | ||
left && | ||
right && | ||
left.type === 'Identifier' && | ||
right.type === 'LogicalExpression' && | ||
(right.operator === '||' || right.operator === '??') && | ||
right.left.type === 'Identifier' && | ||
right.right.type === 'Literal'; | ||
left | ||
&& right | ||
&& left.type === 'Identifier' | ||
&& right.type === 'LogicalExpression' | ||
&& (right.operator === '||' || right.operator === '??') | ||
&& right.left.type === 'Identifier' | ||
&& right.right.type === 'Literal'; | ||
@@ -85,3 +76,3 @@ const containsCallExpression = (sourceCode, node) => { | ||
const isLastParameter = (parameters, parameter) => { | ||
const lastParameter = parameters[parameters.length - 1]; | ||
const lastParameter = parameters.at(-1); | ||
@@ -116,3 +107,3 @@ // See 'default-param-last' rule | ||
sourceCode.getIndexFromLoc({line, column: 0}), | ||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}) | ||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}), | ||
]); | ||
@@ -124,3 +115,3 @@ } | ||
node.range[0], | ||
node.range[1] + 1 | ||
node.range[1] + 1, | ||
]); | ||
@@ -132,8 +123,9 @@ } | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const functionStack = []; | ||
const checkExpression = (node, left, right, assignment) => { | ||
const currentFunction = functionStack[functionStack.length - 1]; | ||
const currentFunction = functionStack.at(-1); | ||
@@ -147,3 +139,3 @@ if (!currentFunction || !isDefaultExpression(left, right)) { | ||
left: {name: secondId}, | ||
right: {raw: literal} | ||
right: {raw: literal}, | ||
} = right; | ||
@@ -156,7 +148,7 @@ | ||
const variable = findVariable(context.getScope(), secondId); | ||
const variable = findVariable(sourceCode.getScope(node), secondId); | ||
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1122 | ||
// But can't reproduce, just ignore this case | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (!variable) { | ||
@@ -169,10 +161,10 @@ return; | ||
const parameter = params.find(parameter => | ||
parameter.type === 'Identifier' && | ||
parameter.name === secondId | ||
parameter.type === 'Identifier' | ||
&& parameter.name === secondId, | ||
); | ||
if ( | ||
hasSideEffects(sourceCode, currentFunction, node) || | ||
hasExtraReferences(assignment, references, left) || | ||
!isLastParameter(params, parameter) | ||
hasSideEffects(sourceCode, currentFunction, node) | ||
|| hasExtraReferences(assignment, references, left) | ||
|| !isLastParameter(params, parameter) | ||
) { | ||
@@ -182,5 +174,5 @@ return; | ||
const replacement = needsParentheses(sourceCode, currentFunction) ? | ||
`(${firstId} = ${literal})` : | ||
`${firstId} = ${literal}`; | ||
const replacement = needsParentheses(sourceCode, currentFunction) | ||
? `(${firstId} = ${literal})` | ||
: `${firstId} = ${literal}`; | ||
@@ -194,28 +186,30 @@ return { | ||
fixer.replaceText(parameter, replacement), | ||
fixDefaultExpression(fixer, sourceCode, node) | ||
] | ||
}] | ||
fixDefaultExpression(fixer, sourceCode, node), | ||
], | ||
}], | ||
}; | ||
}; | ||
return { | ||
':function': node => { | ||
functionStack.push(node); | ||
}, | ||
':function:exit': () => { | ||
functionStack.pop(); | ||
}, | ||
[assignmentSelector]: node => { | ||
const {left, right} = node.expression; | ||
context.on(functionTypes, node => { | ||
functionStack.push(node); | ||
}); | ||
return checkExpression(node, left, right, true); | ||
}, | ||
[declarationSelector]: node => { | ||
const {id, init} = node.declarations[0]; | ||
context.onExit(functionTypes, () => { | ||
functionStack.pop(); | ||
}); | ||
return checkExpression(node, id, init, false); | ||
context.on('AssignmentExpression', node => { | ||
if (node.parent.type === 'ExpressionStatement' && node.parent.expression === node) { | ||
return checkExpression(node.parent, node.left, node.right, true); | ||
} | ||
}; | ||
}); | ||
context.on('VariableDeclarator', node => { | ||
if (node.parent.type === 'VariableDeclaration' && node.parent.declarations[0] === node) { | ||
return checkExpression(node.parent, node.id, node.init, false); | ||
} | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -226,11 +220,11 @@ create, | ||
docs: { | ||
description: 'Prefer default parameters over reassignment.' | ||
description: 'Prefer default parameters over reassignment.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages: { | ||
[MESSAGE_ID]: 'Prefer default parameters over reassignment.', | ||
[MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.' | ||
[MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.', | ||
}, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const isValueNotUsable = require('./utils/is-value-not-usable.js'); | ||
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isNodeValueNotDomNode, isValueNotUsable} = require('./utils/index.js'); | ||
const MESSAGE_ID = 'prefer-dom-node-append'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `Node#append()` over `Node#appendChild()`.' | ||
[MESSAGE_ID]: 'Prefer `Node#append()` over `Node#appendChild()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
name: 'appendChild', | ||
length: 1 | ||
}), | ||
notDomNodeSelector('callee.object'), | ||
notDomNodeSelector('arguments.0') | ||
].join(''); | ||
const create = () => { | ||
return { | ||
[selector](node) { | ||
const fix = isValueNotUsable(node) ? | ||
fixer => fixer.replaceText(node.callee.property, 'append') : | ||
undefined; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'appendChild', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
}) | ||
|| isNodeValueNotDomNode(node.callee.object) | ||
|| isNodeValueNotDomNode(node.arguments[0]) | ||
) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const fix = isValueNotUsable(node) | ||
? fixer => fixer.replaceText(node.callee.property, 'append') | ||
: undefined; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -39,7 +43,7 @@ create, | ||
docs: { | ||
description: 'Prefer `Node#append()` over `Node#appendChild()`.' | ||
description: 'Prefer `Node#append()` over `Node#appendChild()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isValidVariableName = require('./utils/is-valid-variable-name.js'); | ||
const quoteString = require('./utils/quote-string.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isIdentifierName} = require('@babel/helper-validator-identifier'); | ||
const { | ||
escapeString, | ||
hasOptionalChainElement, | ||
isValueNotUsable, | ||
} = require('./utils/index.js'); | ||
const {isMethodCall, isStringLiteral, isExpressionStatement} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-dom-node-dataset'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `.dataset` over `setAttribute(…)`.' | ||
[MESSAGE_ID]: 'Prefer `.dataset` over `{{method}}(…)`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
name: 'setAttribute', | ||
length: 2 | ||
}), | ||
'[arguments.0.type="Literal"]' | ||
].join(''); | ||
const dashToCamelCase = string => string.replaceAll(/-[a-z]/g, s => s[1].toUpperCase()); | ||
const parseNodeText = (context, argument) => context.getSourceCode().getText(argument); | ||
function getFix(callExpression, context) { | ||
const method = callExpression.callee.property.name; | ||
const dashToCamelCase = string => string.replace(/-[a-z]/g, s => s[1].toUpperCase()); | ||
// `foo?.bar = ''` is invalid | ||
// TODO: Remove this restriction if https://github.com/nicolo-ribaudo/ecma262/pull/4 get merged | ||
if (method === 'setAttribute' && hasOptionalChainElement(callExpression.callee)) { | ||
return; | ||
} | ||
const fix = (context, node, fixer) => { | ||
let [name, value] = node.arguments; | ||
const calleeObject = parseNodeText(context, node.callee.object); | ||
// `element.setAttribute(…)` returns `undefined`, but `AssignmentExpression` returns value of RHS | ||
if (method === 'setAttribute' && !isValueNotUsable(callExpression)) { | ||
return; | ||
} | ||
name = dashToCamelCase(name.value.slice(5)); | ||
value = parseNodeText(context, value); | ||
if (method === 'removeAttribute' && !isExpressionStatement(callExpression.parent)) { | ||
return; | ||
} | ||
const replacement = `${calleeObject}.dataset${ | ||
isValidVariableName(name) ? | ||
`.${name}` : | ||
`[${quoteString(name)}]` | ||
} = ${value}`; | ||
return fixer => { | ||
const [nameNode] = callExpression.arguments; | ||
const name = dashToCamelCase(nameNode.value.toLowerCase().slice(5)); | ||
const {sourceCode} = context; | ||
let text = ''; | ||
const datasetText = `${sourceCode.getText(callExpression.callee.object)}.dataset`; | ||
switch (method) { | ||
case 'setAttribute': | ||
case 'getAttribute': | ||
case 'removeAttribute': { | ||
text = isIdentifierName(name) ? `.${name}` : `[${escapeString(name, nameNode.raw.charAt(0))}]`; | ||
text = `${datasetText}${text}`; | ||
if (method === 'setAttribute') { | ||
text += ` = ${sourceCode.getText(callExpression.arguments[1])}`; | ||
} else if (method === 'removeAttribute') { | ||
text = `delete ${text}`; | ||
} | ||
return fixer.replaceText(node, replacement); | ||
}; | ||
/* | ||
For non-exists attribute, `element.getAttribute('data-foo')` returns `null`, | ||
but `element.dataset.foo` returns `undefined`, switch to suggestions if necessary | ||
*/ | ||
break; | ||
} | ||
const create = context => { | ||
return { | ||
[selector](node) { | ||
const name = node.arguments[0].value; | ||
if (typeof name !== 'string' || !name.startsWith('data-') || name === 'data-') { | ||
return; | ||
case 'hasAttribute': { | ||
text = `Object.hasOwn(${datasetText}, ${escapeString(name, nameNode.raw.charAt(0))})`; | ||
break; | ||
} | ||
// No default | ||
} | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fix(context, node, fixer) | ||
}; | ||
} | ||
return fixer.replaceText(callExpression, text); | ||
}; | ||
}; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(callExpression) { | ||
if (!( | ||
( | ||
isMethodCall(callExpression, { | ||
method: 'setAttribute', | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isMethodCall(callExpression, { | ||
methods: ['getAttribute', 'removeAttribute', 'hasAttribute'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
&& isStringLiteral(callExpression.arguments[0]) | ||
)) { | ||
return; | ||
} | ||
const attributeName = callExpression.arguments[0].value.toLowerCase(); | ||
if (!attributeName.startsWith('data-')) { | ||
return; | ||
} | ||
return { | ||
node: callExpression, | ||
messageId: MESSAGE_ID, | ||
data: {method: callExpression.callee.property.name}, | ||
fix: getFix(callExpression, context), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -62,7 +115,7 @@ create, | ||
docs: { | ||
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)`.' | ||
description: 'Prefer using `.dataset` on DOM elements over calling attribute methods.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, hasSideEffect} = require('eslint-utils'); | ||
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const isValueNotUsable = require('./utils/is-value-not-usable.js'); | ||
const {getParenthesizedText} = require('./utils/parentheses.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {isParenthesized, hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const { | ||
getParenthesizedText, | ||
isNodeValueNotDomNode, | ||
isValueNotUsable, | ||
needsSemicolon, | ||
shouldAddParenthesesToMemberExpressionObject, | ||
} = require('./utils/index.js'); | ||
@@ -13,19 +16,32 @@ const ERROR_MESSAGE_ID = 'error'; | ||
[ERROR_MESSAGE_ID]: 'Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.', | ||
[SUGGESTION_MESSAGE_ID]: 'Replace `parentNode.removeChild(childNode)` with `childNode.remove()`.' | ||
[SUGGESTION_MESSAGE_ID]: 'Replace `parentNode.removeChild(childNode)` with `childNode{{dotOrQuestionDot}}remove()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
name: 'removeChild', | ||
length: 1 | ||
}), | ||
notDomNodeSelector('callee.object'), | ||
notDomNodeSelector('arguments.0') | ||
].join(''); | ||
// TODO: Don't check node.type twice | ||
const isMemberExpressionOptionalObject = node => | ||
node.parent.type === 'MemberExpression' | ||
&& node.parent.object === node | ||
&& ( | ||
node.parent.optional | ||
|| (node.type === 'MemberExpression' && isMemberExpressionOptionalObject(node.object)) | ||
); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[selector](node) { | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'removeChild', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
}) | ||
|| isNodeValueNotDomNode(node.callee.object) | ||
|| isNodeValueNotDomNode(node.arguments[0]) | ||
) { | ||
return; | ||
} | ||
const parentNode = node.callee.object; | ||
@@ -36,10 +52,12 @@ const childNode = node.arguments[0]; | ||
node, | ||
messageId: ERROR_MESSAGE_ID | ||
messageId: ERROR_MESSAGE_ID, | ||
}; | ||
const fix = fixer => { | ||
const isOptionalParentNode = isMemberExpressionOptionalObject(parentNode); | ||
const createFix = (optional = false) => fixer => { | ||
let childNodeText = getParenthesizedText(childNode, sourceCode); | ||
if ( | ||
!isParenthesized(childNode, sourceCode) && | ||
shouldAddParenthesesToMemberExpressionObject(childNode, sourceCode) | ||
!isParenthesized(childNode, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(childNode, sourceCode) | ||
) { | ||
@@ -53,21 +71,44 @@ childNodeText = `(${childNodeText})`; | ||
return fixer.replaceText(node, `${childNodeText}.remove()`); | ||
return fixer.replaceText(node, `${childNodeText}${optional ? '?' : ''}.remove()`); | ||
}; | ||
if (!hasSideEffect(parentNode, sourceCode) && isValueNotUsable(node)) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
fix | ||
} | ||
]; | ||
if (!isOptionalParentNode) { | ||
problem.fix = createFix(false); | ||
return problem; | ||
} | ||
// The most common case `foo?.parentNode.remove(foo)` | ||
// TODO: Allow case like `foo.bar?.parentNode.remove(foo.bar)` | ||
if ( | ||
node.callee.type === 'MemberExpression' | ||
&& !node.callee.optional | ||
&& parentNode.type === 'MemberExpression' | ||
&& parentNode.optional | ||
&& !parentNode.computed | ||
&& parentNode.property.type === 'Identifier' | ||
&& parentNode.property.name === 'parentNode' | ||
&& parentNode.object.type === 'Identifier' | ||
&& childNode.type === 'Identifier' | ||
&& parentNode.object.name === childNode.name | ||
) { | ||
problem.fix = createFix(true); | ||
return problem; | ||
} | ||
} | ||
problem.suggest = ( | ||
isOptionalParentNode ? [true, false] : [false] | ||
).map(optional => ({ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
data: {dotOrQuestionDot: optional ? '?.' : '.'}, | ||
fix: createFix(optional), | ||
})); | ||
return problem; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -78,8 +119,8 @@ create, | ||
docs: { | ||
description: 'Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.' | ||
description: 'Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {memberExpressionSelector} = require('./selectors/index.js'); | ||
const {isMemberExpression} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-dom-node-text-content'; | ||
const ERROR = 'error'; | ||
const SUGGESTION = 'suggestion'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `.textContent` over `.innerText`.' | ||
[ERROR]: 'Prefer `.textContent` over `.innerText`.', | ||
[SUGGESTION]: 'Switch to `.textContent`.', | ||
}; | ||
const selector = `${memberExpressionSelector('innerText')} > .property`; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
MemberExpression(memberExpression) { | ||
if ( | ||
!isMemberExpression(memberExpression, { | ||
property: 'innerText', | ||
}) | ||
) { | ||
return; | ||
} | ||
const create = () => { | ||
return { | ||
[selector](node) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.replaceText(node, 'textContent') | ||
}; | ||
const node = memberExpression.property; | ||
return { | ||
node, | ||
messageId: ERROR, | ||
suggest: [ | ||
{ | ||
messageId: SUGGESTION, | ||
fix: fixer => fixer.replaceText(node, 'textContent'), | ||
}, | ||
], | ||
}; | ||
}, | ||
Identifier(node) { | ||
if (!( | ||
node.name === 'innerText' | ||
&& node.parent.type === 'Property' | ||
&& node.parent.key === node | ||
&& !node.parent.computed | ||
&& node.parent.kind === 'init' | ||
&& node.parent.parent.type === 'ObjectPattern' | ||
&& node.parent.parent.properties.includes(node.parent) | ||
)) { | ||
return; | ||
} | ||
}; | ||
}; | ||
return { | ||
node, | ||
messageId: ERROR, | ||
suggest: [ | ||
{ | ||
messageId: SUGGESTION, | ||
fix: fixer => fixer.replaceText( | ||
node, | ||
node.parent.shorthand ? 'textContent: innerText' : 'textContent', | ||
), | ||
}, | ||
], | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -28,7 +70,7 @@ create, | ||
docs: { | ||
description: 'Prefer `.textContent` over `.innerText`.' | ||
description: 'Prefer `.textContent` over `.innerText`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
hasSuggestions: true, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isMethodNamed = require('./utils/is-method-named.js'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const simpleArraySearchRule = require('./shared/simple-array-search-rule.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-includes'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use `.includes()`, rather than `.indexOf()`, when checking for existence.' | ||
[MESSAGE_ID]: 'Use `.includes()`, rather than `.indexOf()`, when checking for existence.', | ||
}; | ||
@@ -14,7 +14,7 @@ // Ignore {_,lodash,underscore}.indexOf | ||
const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1; | ||
const isLiteralZero = node => isLiteralValue(node, 0); | ||
const isLiteralZero = node => isLiteral(node, 0); | ||
const isNegativeResult = node => ['===', '==', '<'].includes(node.operator); | ||
const getProblem = (context, node, target, argumentsNodes) => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const memberExpressionNode = target.parent; | ||
@@ -34,6 +34,6 @@ const dotToken = sourceCode.getTokenBefore(memberExpressionNode.property); | ||
messageId: MESSAGE_ID, | ||
fix: fixer => { | ||
fix(fixer) { | ||
const replacement = `${isNegativeResult(node) ? '!' : ''}${targetSource}.includes(${argumentsSource.join(', ')})`; | ||
return fixer.replaceText(node, replacement); | ||
} | ||
}, | ||
}; | ||
@@ -44,7 +44,10 @@ }; | ||
method: 'some', | ||
replacement: 'includes' | ||
replacement: 'includes', | ||
}); | ||
const create = context => ({ | ||
BinaryExpression: node => { | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
includesOverSomeRule.listen(context); | ||
context.on('BinaryExpression', node => { | ||
const {left, right, operator} = node; | ||
@@ -70,4 +73,4 @@ | ||
if ( | ||
(['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right)) || | ||
(['>=', '<'].includes(operator) && isLiteralZero(right)) | ||
(['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right)) | ||
|| (['>=', '<'].includes(operator) && isLiteralZero(right)) | ||
) { | ||
@@ -78,9 +81,9 @@ return getProblem( | ||
target, | ||
argumentsNodes | ||
argumentsNodes, | ||
); | ||
} | ||
}, | ||
...includesOverSomeRule.createListeners(context) | ||
}); | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -91,11 +94,11 @@ create, | ||
docs: { | ||
description: 'Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence.' | ||
description: 'Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages: { | ||
...messages, | ||
...includesOverSomeRule.messages | ||
...includesOverSomeRule.messages, | ||
}, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const quoteString = require('./utils/quote-string.js'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const translateToKey = require('./shared/event-keys.js'); | ||
const {isNumberLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-keyboard-event-key'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Use `.key` instead of `.{{name}}`.' | ||
[MESSAGE_ID]: 'Use `.key` instead of `.{{name}}`.', | ||
}; | ||
@@ -13,29 +14,27 @@ | ||
'charCode', | ||
'which' | ||
'which', | ||
]); | ||
const isPropertyNamedAddEventListener = node => | ||
node && | ||
node.type === 'CallExpression' && | ||
node.callee && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.property && | ||
node.callee.property.name === 'addEventListener'; | ||
node?.type === 'CallExpression' | ||
&& node.callee.type === 'MemberExpression' | ||
&& node.callee.property.name === 'addEventListener'; | ||
const getEventNodeAndReferences = (context, node) => { | ||
const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener); | ||
const callback = eventListener && eventListener.arguments && eventListener.arguments[1]; | ||
switch (callback && callback.type) { | ||
const callback = eventListener?.arguments[1]; | ||
switch (callback?.type) { | ||
case 'ArrowFunctionExpression': | ||
case 'FunctionExpression': { | ||
const eventVariable = context.getDeclaredVariables(callback)[0]; | ||
const references = eventVariable && eventVariable.references; | ||
const eventVariable = context.sourceCode.getDeclaredVariables(callback)[0]; | ||
const references = eventVariable?.references; | ||
return { | ||
event: callback.params && callback.params[0], | ||
references | ||
event: callback.params[0], | ||
references, | ||
}; | ||
} | ||
default: | ||
default: { | ||
return {}; | ||
} | ||
} | ||
@@ -45,7 +44,4 @@ }; | ||
const isPropertyOf = (node, eventNode) => | ||
node && | ||
node.parent && | ||
node.parent.type === 'MemberExpression' && | ||
node.parent.object && | ||
node.parent.object === eventNode; | ||
node?.parent?.type === 'MemberExpression' | ||
&& node.parent.object === eventNode; | ||
@@ -72,3 +68,3 @@ // The third argument is a condition function, as one passed to `Array#filter()` | ||
/* istanbul ignore else */ | ||
/* c8 ignore next 3 */ | ||
if (level === 0) { | ||
@@ -86,9 +82,17 @@ return current; | ||
const {right = {}, operator} = nearestIf.test; | ||
const isTestingEquality = operator === '==' || operator === '==='; | ||
const isRightValid = isTestingEquality && right.type === 'Literal' && typeof right.value === 'number'; | ||
const {type, operator, right} = nearestIf.test; | ||
if ( | ||
!( | ||
type === 'BinaryExpression' | ||
&& (operator === '==' || operator === '===') | ||
&& isNumberLiteral(right) | ||
) | ||
) { | ||
return; | ||
} | ||
// Either a meta key or a printable character | ||
const keyCode = translateToKey[right.value] || String.fromCharCode(right.value); | ||
const key = translateToKey[right.value] || String.fromCodePoint(right.value); | ||
// And if we recognize the `.keyCode` | ||
if (!isRightValid || !keyCode) { | ||
if (!key) { | ||
return; | ||
@@ -100,3 +104,3 @@ } | ||
fixer.replaceText(node, 'key'), | ||
fixer.replaceText(right, quoteString(keyCode)) | ||
fixer.replaceText(right, escapeString(key)), | ||
]; | ||
@@ -109,65 +113,70 @@ }; | ||
node, | ||
fix: fix(node) | ||
fix: fix(node), | ||
}); | ||
const create = context => { | ||
return { | ||
'Identifier:matches([name="keyCode"], [name="charCode"], [name="which"])'(node) { | ||
// Normal case when usage is direct -> `event.keyCode` | ||
const {event, references} = getEventNodeAndReferences(context, node); | ||
if (!event) { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
Identifier(node) { | ||
if ( | ||
node.name !== 'keyCode' | ||
&& node.name !== 'charCode' | ||
&& node.name !== 'which' | ||
) { | ||
return; | ||
} | ||
if ( | ||
references && | ||
references.some(reference => isPropertyOf(node, reference.identifier)) | ||
) { | ||
return getProblem(node); | ||
} | ||
}, | ||
// Normal case when usage is direct -> `event.keyCode` | ||
const {event, references} = getEventNodeAndReferences(context, node); | ||
if (!event) { | ||
return; | ||
} | ||
Property(node) { | ||
// Destructured case | ||
const propertyName = node.value && node.value.name; | ||
if (!keys.has(propertyName)) { | ||
return; | ||
} | ||
if ( | ||
references | ||
&& references.some(reference => isPropertyOf(node, reference.identifier)) | ||
) { | ||
return getProblem(node); | ||
} | ||
}, | ||
const {event, references} = getEventNodeAndReferences(context, node); | ||
if (!event) { | ||
return; | ||
} | ||
Property(node) { | ||
// Destructured case | ||
const propertyName = node.value.name; | ||
if (!keys.has(propertyName)) { | ||
return; | ||
} | ||
const nearestVariableDeclarator = getMatchingAncestorOfType( | ||
node, | ||
'VariableDeclarator' | ||
); | ||
const initObject = | ||
nearestVariableDeclarator && | ||
nearestVariableDeclarator.init && | ||
nearestVariableDeclarator.init; | ||
const {event, references} = getEventNodeAndReferences(context, node); | ||
if (!event) { | ||
return; | ||
} | ||
// Make sure initObject is a reference of eventVariable | ||
if ( | ||
references && | ||
references.some(reference => reference.identifier === initObject) | ||
) { | ||
return getProblem(node.value); | ||
} | ||
const nearestVariableDeclarator = getMatchingAncestorOfType( | ||
node, | ||
'VariableDeclarator', | ||
); | ||
const initObject = nearestVariableDeclarator?.init; | ||
// When the event parameter itself is destructured directly | ||
const isEventParameterDestructured = event.type === 'ObjectPattern'; | ||
if (isEventParameterDestructured) { | ||
// Check for properties | ||
for (const property of event.properties) { | ||
if (property === node) { | ||
return getProblem(node.value); | ||
} | ||
// Make sure initObject is a reference of eventVariable | ||
if ( | ||
references | ||
&& references.some(reference => reference.identifier === initObject) | ||
) { | ||
return getProblem(node.value); | ||
} | ||
// When the event parameter itself is destructured directly | ||
const isEventParameterDestructured = event.type === 'ObjectPattern'; | ||
if (isEventParameterDestructured) { | ||
// Check for properties | ||
for (const property of event.properties) { | ||
if (property === node) { | ||
return getProblem(node.value); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -178,7 +187,7 @@ create, | ||
docs: { | ||
description: 'Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`.' | ||
description: 'Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {hasSideEffect} = require('eslint-utils'); | ||
const {hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
@@ -10,25 +12,14 @@ const ERROR_BITWISE = 'error-bitwise'; | ||
[ERROR_BITWISE_NOT]: 'Use `Math.trunc` instead of `~~`.', | ||
[SUGGESTION_BITWISE]: 'Replace `{{operator}} {{value}}` with `Math.trunc`.' | ||
[SUGGESTION_BITWISE]: 'Replace `{{operator}} {{value}}` with `Math.trunc`.', | ||
}; | ||
const createBitwiseNotSelector = (level, isNegative) => { | ||
const prefix = 'argument.'.repeat(level); | ||
const selector = [ | ||
`[${prefix}type="UnaryExpression"]`, | ||
`[${prefix}operator="~"]` | ||
].join(''); | ||
return isNegative ? `:not(${selector})` : selector; | ||
}; | ||
// Bitwise operators | ||
const bitwiseOperators = new Set(['|', '>>', '<<', '^']); | ||
// Unary Expression Selector: Inner-most 2 bitwise NOT | ||
const bitwiseNotUnaryExpressionSelector = [ | ||
createBitwiseNotSelector(0), | ||
createBitwiseNotSelector(1), | ||
createBitwiseNotSelector(2, true) | ||
].join(''); | ||
const isBitwiseNot = node => | ||
node.type === 'UnaryExpression' | ||
&& node.operator === '~'; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -41,60 +32,69 @@ const mathTruncFunctionCall = node => { | ||
return { | ||
':matches(BinaryExpression, AssignmentExpression)[right.type="Literal"]': node => { | ||
const {type, operator, right, left} = node; | ||
const isAssignment = type === 'AssignmentExpression'; | ||
if ( | ||
right.value !== 0 || | ||
!bitwiseOperators.has(isAssignment ? operator.slice(0, -1) : operator) | ||
) { | ||
return; | ||
} | ||
context.on(['BinaryExpression', 'AssignmentExpression'], node => { | ||
const {type, operator, right, left} = node; | ||
const isAssignment = type === 'AssignmentExpression'; | ||
if ( | ||
!isLiteral(right, 0) | ||
|| !bitwiseOperators.has(isAssignment ? operator.slice(0, -1) : operator) | ||
) { | ||
return; | ||
} | ||
const problem = { | ||
node, | ||
messageId: ERROR_BITWISE, | ||
data: { | ||
operator, | ||
value: right.raw | ||
const problem = { | ||
node, | ||
messageId: ERROR_BITWISE, | ||
data: { | ||
operator, | ||
value: right.raw, | ||
}, | ||
}; | ||
if (!isAssignment || !hasSideEffect(left, sourceCode)) { | ||
const fix = function * (fixer) { | ||
const fixed = mathTruncFunctionCall(left); | ||
if (isAssignment) { | ||
const operatorToken = sourceCode.getTokenAfter(left, token => token.type === 'Punctuator' && token.value === operator); | ||
yield fixer.replaceText(operatorToken, '='); | ||
yield fixer.replaceText(right, fixed); | ||
} else { | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
yield fixer.replaceText(node, fixed); | ||
} | ||
}; | ||
if (!isAssignment || !hasSideEffect(left, sourceCode)) { | ||
const fix = fixer => { | ||
let fixed = mathTruncFunctionCall(left); | ||
if (isAssignment) { | ||
fixed = `${sourceCode.getText(left)} = ${fixed}`; | ||
} | ||
if (operator === '|') { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_BITWISE, | ||
fix, | ||
}, | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
} | ||
} | ||
return fixer.replaceText(node, fixed); | ||
}; | ||
return problem; | ||
}); | ||
if (operator === '|') { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_BITWISE, | ||
data: { | ||
operator, | ||
value: right.raw | ||
}, | ||
fix | ||
} | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
} | ||
} | ||
return problem; | ||
}, | ||
[bitwiseNotUnaryExpressionSelector]: node => { | ||
// Unary Expression Selector: Inner-most 2 bitwise NOT | ||
context.on('UnaryExpression', node => { | ||
if ( | ||
isBitwiseNot(node) | ||
&& isBitwiseNot(node.argument) | ||
&& !isBitwiseNot(node.argument.argument) | ||
) { | ||
return { | ||
node, | ||
messageId: ERROR_BITWISE_NOT, | ||
fix: fixer => fixer.replaceText(node, mathTruncFunctionCall(node.argument.argument)) | ||
* fix(fixer) { | ||
yield fixer.replaceText(node, mathTruncFunctionCall(node.argument.argument)); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}, | ||
}; | ||
} | ||
}; | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -105,8 +105,8 @@ create, | ||
docs: { | ||
description: 'Enforce the use of `Math.trunc` instead of bitwise operators.' | ||
description: 'Enforce the use of `Math.trunc` instead of bitwise operators.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const isValueNotUsable = require('./utils/is-value-not-usable.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isValueNotUsable} = require('./utils/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -9,22 +9,8 @@ const messages = { | ||
insertAdjacentTextOrInsertAdjacentElement: | ||
'Prefer `{{reference}}.{{preferredMethod}}({{content}})` over `{{reference}}.{{method}}({{position}}, {{content}})`.' | ||
'Prefer `{{reference}}.{{preferredMethod}}({{content}})` over `{{reference}}.{{method}}({{position}}, {{content}})`.', | ||
}; | ||
const replaceChildOrInsertBeforeSelector = [ | ||
methodCallSelector({ | ||
names: ['replaceChild', 'insertBefore'], | ||
length: 2 | ||
}), | ||
// We only allow Identifier for now | ||
'[arguments.0.type="Identifier"]', | ||
'[arguments.0.name!="undefined"]', | ||
'[arguments.1.type="Identifier"]', | ||
'[arguments.1.name!="undefined"]', | ||
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets reported | ||
'[callee.object.type="Identifier"]' | ||
].join(''); | ||
const forbiddenMethods = new Map([ | ||
const disallowedMethods = new Map([ | ||
['replaceChild', 'replaceWith'], | ||
['insertBefore', 'before'] | ||
['insertBefore', 'before'], | ||
]); | ||
@@ -36,10 +22,10 @@ | ||
const [newChildNode, oldChildNode] = node.arguments.map(({name}) => name); | ||
const preferredMethod = forbiddenMethods.get(method); | ||
const preferredMethod = disallowedMethods.get(method); | ||
const fix = isValueNotUsable(node) ? | ||
fixer => fixer.replaceText( | ||
const fix = isValueNotUsable(node) | ||
? fixer => fixer.replaceText( | ||
node, | ||
`${oldChildNode}.${preferredMethod}(${newChildNode})` | ||
) : | ||
undefined; | ||
`${oldChildNode}.${preferredMethod}(${newChildNode})`, | ||
) | ||
: undefined; | ||
@@ -54,21 +40,8 @@ return { | ||
newChildNode, | ||
oldChildNode | ||
oldChildNode, | ||
}, | ||
fix | ||
fix, | ||
}; | ||
}; | ||
const insertAdjacentTextOrInsertAdjacentElementSelector = [ | ||
methodCallSelector({ | ||
names: ['insertAdjacentText', 'insertAdjacentElement'], | ||
length: 2 | ||
}), | ||
// Position argument should be `string` | ||
'[arguments.0.type="Literal"]', | ||
// TODO: remove this limits on second argument | ||
':matches([arguments.1.type="Literal"], [arguments.1.type="Identifier"])', | ||
// TODO: remove this limits on callee | ||
'[callee.object.type="Identifier"]' | ||
].join(''); | ||
const positionReplacers = new Map([ | ||
@@ -78,3 +51,3 @@ ['beforebegin', 'before'], | ||
['beforeend', 'append'], | ||
['afterend', 'after'] | ||
['afterend', 'after'], | ||
]); | ||
@@ -93,11 +66,12 @@ | ||
const preferredMethod = positionReplacers.get(position); | ||
const content = context.getSource(contentNode); | ||
const reference = context.getSource(node.callee.object); | ||
const {sourceCode} = context; | ||
const content = sourceCode.getText(contentNode); | ||
const reference = sourceCode.getText(node.callee.object); | ||
const fix = method === 'insertAdjacentElement' && !isValueNotUsable(node) ? | ||
undefined : | ||
const fix = method === 'insertAdjacentElement' && !isValueNotUsable(node) | ||
? undefined | ||
// TODO: make a better fix, don't touch reference | ||
fixer => fixer.replaceText( | ||
: fixer => fixer.replaceText( | ||
node, | ||
`${reference}.${preferredMethod}(${content})` | ||
`${reference}.${preferredMethod}(${content})`, | ||
); | ||
@@ -112,20 +86,52 @@ | ||
preferredMethod, | ||
position: context.getSource(positionNode), | ||
content | ||
position: sourceCode.getText(positionNode), | ||
content, | ||
}, | ||
fix | ||
fix, | ||
}; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
return { | ||
[replaceChildOrInsertBeforeSelector](node) { | ||
context.on('CallExpression', node => { | ||
if ( | ||
isMethodCall(node, { | ||
methods: ['replaceChild', 'insertBefore'], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// We only allow Identifier for now | ||
&& node.arguments.every(node => node.type === 'Identifier' && node.name !== 'undefined') | ||
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets reported | ||
&& node.callee.object.type === 'Identifier' | ||
) { | ||
return checkForReplaceChildOrInsertBefore(context, node); | ||
}, | ||
[insertAdjacentTextOrInsertAdjacentElementSelector](node) { | ||
} | ||
}); | ||
context.on('CallExpression', node => { | ||
if ( | ||
isMethodCall(node, { | ||
methods: ['insertAdjacentText', 'insertAdjacentElement'], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// Position argument should be `string` | ||
&& node.arguments[0].type === 'Literal' | ||
// TODO: remove this limits on second argument | ||
&& ( | ||
node.arguments[1].type === 'Literal' | ||
|| node.arguments[1].type === 'Identifier' | ||
) | ||
// TODO: remove this limits on callee | ||
&& node.callee.object.type === 'Identifier' | ||
) { | ||
return checkForInsertAdjacentTextOrInsertAdjacentElement(context, node); | ||
} | ||
}; | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -136,7 +142,7 @@ create, | ||
docs: { | ||
description: 'Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`.' | ||
description: 'Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isOpeningParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken} = require('@eslint-community/eslint-utils'); | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const removeSpacesAfter = require('./utils/remove-spaces-after.js'); | ||
const isStaticRequire = require('./utils/is-static-require.js'); | ||
const replaceReferenceIdentifier = require('./utils/replace-reference-identifier.js'); | ||
const {getParentheses} = require('./utils/parentheses.js'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
const {referenceIdentifierSelector} = require('./selectors/index.js'); | ||
const {isStaticRequire, isReferenceIdentifier, isFunction} = require('./ast/index.js'); | ||
const { | ||
removeParentheses, | ||
replaceReferenceIdentifier, | ||
removeSpacesAfter, | ||
} = require('./fix/index.js'); | ||
@@ -14,2 +15,3 @@ const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive'; | ||
const ERROR_IDENTIFIER = 'error/identifier'; | ||
const SUGGESTION_USE_STRICT_DIRECTIVE = 'suggestion/use-strict-directive'; | ||
const SUGGESTION_DIRNAME = 'suggestion/dirname'; | ||
@@ -23,25 +25,9 @@ const SUGGESTION_FILENAME = 'suggestion/filename'; | ||
[ERROR_IDENTIFIER]: 'Do not use "{{name}}".', | ||
[SUGGESTION_USE_STRICT_DIRECTIVE]: 'Remove "use strict" directive.', | ||
[SUGGESTION_DIRNAME]: 'Replace "__dirname" with `"…(import.meta.url)"`.', | ||
[SUGGESTION_FILENAME]: 'Replace "__filename" with `"…(import.meta.url)"`.', | ||
[SUGGESTION_IMPORT]: 'Switch to `import`.', | ||
[SUGGESTION_EXPORT]: 'Switch to `export`.' | ||
[SUGGESTION_EXPORT]: 'Switch to `export`.', | ||
}; | ||
const identifierSelector = referenceIdentifierSelector([ | ||
'exports', | ||
'require', | ||
'module', | ||
'__filename', | ||
'__dirname' | ||
]); | ||
function * removeParentheses(nodeOrNodes, fixer, sourceCode) { | ||
for (const node of Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes]) { | ||
const parentheses = getParentheses(node, sourceCode); | ||
for (const token of parentheses) { | ||
yield fixer.remove(token); | ||
} | ||
} | ||
} | ||
function fixRequireCall(node, sourceCode) { | ||
@@ -56,3 +42,3 @@ if (!isStaticRequire(node.parent) || node.parent.callee !== node) { | ||
callee, | ||
arguments: [source] | ||
arguments: [source], | ||
} = requireCall; | ||
@@ -66,3 +52,3 @@ | ||
callee, | ||
isOpeningParenToken | ||
isOpeningParenToken, | ||
); | ||
@@ -72,3 +58,6 @@ yield fixer.replaceText(openingParenthesisToken, ' '); | ||
yield fixer.remove(closingParenthesisToken); | ||
yield * removeParentheses([callee, requireCall, source], fixer, sourceCode); | ||
for (const node of [callee, requireCall, source]) { | ||
yield * removeParentheses(node, fixer, sourceCode); | ||
} | ||
}; | ||
@@ -80,22 +69,22 @@ } | ||
if ( | ||
parent.type === 'VariableDeclarator' && | ||
parent.init === requireCall && | ||
( | ||
parent.id.type === 'Identifier' || | ||
( | ||
parent.id.type === 'ObjectPattern' && | ||
parent.id.properties.every( | ||
parent.type === 'VariableDeclarator' | ||
&& parent.init === requireCall | ||
&& ( | ||
parent.id.type === 'Identifier' | ||
|| ( | ||
parent.id.type === 'ObjectPattern' | ||
&& parent.id.properties.every( | ||
({type, key, value, computed}) => | ||
type === 'Property' && | ||
!computed && | ||
value.type === 'Identifier' && | ||
key.type === 'Identifier' | ||
type === 'Property' | ||
&& !computed | ||
&& value.type === 'Identifier' | ||
&& key.type === 'Identifier', | ||
) | ||
) | ||
) && | ||
parent.parent.type === 'VariableDeclaration' && | ||
parent.parent.kind === 'const' && | ||
parent.parent.declarations.length === 1 && | ||
parent.parent.declarations[0] === parent && | ||
parent.parent.parent.type === 'Program' | ||
) | ||
&& parent.parent.type === 'VariableDeclaration' | ||
&& parent.parent.kind === 'const' | ||
&& parent.parent.declarations.length === 1 | ||
&& parent.parent.declarations[0] === parent | ||
&& parent.parent.parent.type === 'Program' | ||
) { | ||
@@ -110,3 +99,3 @@ const declarator = parent; | ||
expected: {type: 'Keyword', value: 'const'}, | ||
ruleId: 'prefer-module' | ||
ruleId: 'prefer-module', | ||
}); | ||
@@ -118,3 +107,3 @@ yield fixer.replaceText(constToken, 'import'); | ||
expected: {type: 'Punctuator', value: '='}, | ||
ruleId: 'prefer-module' | ||
ruleId: 'prefer-module', | ||
}); | ||
@@ -128,3 +117,3 @@ yield removeSpacesAfter(id, sourceCode, fixer); | ||
callee, | ||
isOpeningParenToken | ||
isOpeningParenToken, | ||
); | ||
@@ -135,3 +124,5 @@ yield fixer.remove(openingParenthesisToken); | ||
yield * removeParentheses([callee, requireCall, source], fixer, sourceCode); | ||
for (const node of [callee, requireCall, source]) { | ||
yield * removeParentheses(node, fixer, sourceCode); | ||
} | ||
@@ -150,3 +141,3 @@ if (id.type === 'Identifier') { | ||
expected: {type: 'Punctuator', value: ':'}, | ||
ruleId: 'prefer-module' | ||
ruleId: 'prefer-module', | ||
}); | ||
@@ -163,23 +154,32 @@ yield removeSpacesAfter(key, sourceCode, fixer); | ||
const isTopLevelAssignment = node => | ||
node.parent.type === 'AssignmentExpression' && | ||
node.parent.operator === '=' && | ||
node.parent.left === node && | ||
node.parent.parent.type === 'ExpressionStatement' && | ||
node.parent.parent.parent.type === 'Program'; | ||
node.parent.type === 'AssignmentExpression' | ||
&& node.parent.operator === '=' | ||
&& node.parent.left === node | ||
&& node.parent.parent.type === 'ExpressionStatement' | ||
&& node.parent.parent.parent.type === 'Program'; | ||
const isNamedExport = node => | ||
node.parent.type === 'MemberExpression' && | ||
!node.parent.optional && | ||
!node.parent.computed && | ||
node.parent.object === node && | ||
node.parent.property.type === 'Identifier' && | ||
isTopLevelAssignment(node.parent) && | ||
node.parent.parent.right.type === 'Identifier'; | ||
node.parent.type === 'MemberExpression' | ||
&& !node.parent.optional | ||
&& !node.parent.computed | ||
&& node.parent.object === node | ||
&& node.parent.property.type === 'Identifier' | ||
&& isTopLevelAssignment(node.parent) | ||
&& node.parent.parent.right.type === 'Identifier'; | ||
const isModuleExports = node => | ||
node.parent.type === 'MemberExpression' && | ||
!node.parent.optional && | ||
!node.parent.computed && | ||
node.parent.object === node && | ||
node.parent.property.type === 'Identifier' && | ||
node.parent.property.name === 'exports'; | ||
node.parent.type === 'MemberExpression' | ||
&& !node.parent.optional | ||
&& !node.parent.computed | ||
&& node.parent.object === node | ||
&& node.parent.property.type === 'Identifier' | ||
&& node.parent.property.name === 'exports'; | ||
const isTopLevelReturnStatement = node => { | ||
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) { | ||
if (isFunction(ancestor)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
function fixDefaultExport(node, sourceCode) { | ||
@@ -194,3 +194,5 @@ return function * (fixer) { | ||
yield * removeParentheses([node.parent, node], fixer, sourceCode); | ||
for (const currentNode of [node.parent, node]) { | ||
yield * removeParentheses(currentNode, fixer, sourceCode); | ||
} | ||
}; | ||
@@ -229,50 +231,81 @@ } | ||
function create(context) { | ||
const filename = context.getPhysicalFilename(); | ||
const filename = context.filename.toLowerCase(); | ||
if (filename.toLowerCase().endsWith('.cjs')) { | ||
return {}; | ||
if (filename.endsWith('.cjs')) { | ||
return; | ||
} | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
'ExpressionStatement[directive="use strict"]'(node) { | ||
context.on('ExpressionStatement', node => { | ||
if (node.directive !== 'use strict') { | ||
return; | ||
} | ||
const problem = {node, messageId: ERROR_USE_STRICT_DIRECTIVE}; | ||
const fix = function * (fixer) { | ||
yield fixer.remove(node); | ||
yield removeSpacesAfter(node, sourceCode, fixer); | ||
}; | ||
if (filename.endsWith('.mjs')) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_USE_STRICT_DIRECTIVE, fix}]; | ||
} | ||
return problem; | ||
}); | ||
context.on('ReturnStatement', node => { | ||
if (isTopLevelReturnStatement(node)) { | ||
return { | ||
node, | ||
messageId: ERROR_USE_STRICT_DIRECTIVE, | ||
* fix(fixer) { | ||
yield fixer.remove(node); | ||
yield removeSpacesAfter(node, sourceCode, fixer); | ||
} | ||
}; | ||
}, | ||
'ReturnStatement:not(:function ReturnStatement)'(node) { | ||
return { | ||
node: sourceCode.getFirstToken(node), | ||
messageId: ERROR_GLOBAL_RETURN | ||
messageId: ERROR_GLOBAL_RETURN, | ||
}; | ||
}, | ||
[identifierSelector](node) { | ||
if (isShadowed(context.getScope(), node)) { | ||
return; | ||
} | ||
} | ||
}); | ||
const {name} = node; | ||
context.on('Identifier', node => { | ||
if ( | ||
!isReferenceIdentifier(node, [ | ||
'exports', | ||
'require', | ||
'module', | ||
'__filename', | ||
'__dirname', | ||
]) | ||
|| isShadowed(sourceCode.getScope(node), node) | ||
) { | ||
return; | ||
} | ||
const problem = { | ||
node, | ||
messageId: ERROR_IDENTIFIER, | ||
data: {name} | ||
}; | ||
const {name} = node; | ||
switch (name) { | ||
case '__filename': | ||
case '__dirname': { | ||
const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME; | ||
const replacement = node.name === '__dirname' ? | ||
'path.dirname(url.fileURLToPath(import.meta.url))' : | ||
'url.fileURLToPath(import.meta.url)'; | ||
const problem = { | ||
node, | ||
messageId: ERROR_IDENTIFIER, | ||
data: {name}, | ||
}; | ||
switch (name) { | ||
case '__filename': | ||
case '__dirname': { | ||
const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME; | ||
const replacement = node.name === '__dirname' | ||
? 'path.dirname(url.fileURLToPath(import.meta.url))' | ||
: 'url.fileURLToPath(import.meta.url)'; | ||
problem.suggest = [{ | ||
messageId, | ||
fix: fixer => replaceReferenceIdentifier(node, replacement, fixer), | ||
}]; | ||
return problem; | ||
} | ||
case 'require': { | ||
const fix = fixRequireCall(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId, | ||
fix: fixer => replaceReferenceIdentifier(node, replacement, fixer) | ||
messageId: SUGGESTION_IMPORT, | ||
fix, | ||
}]; | ||
@@ -282,49 +315,39 @@ return problem; | ||
case 'require': { | ||
const fix = fixRequireCall(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId: SUGGESTION_IMPORT, | ||
fix | ||
}]; | ||
return problem; | ||
} | ||
break; | ||
} | ||
break; | ||
case 'exports': { | ||
const fix = fixExports(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId: SUGGESTION_EXPORT, | ||
fix, | ||
}]; | ||
return problem; | ||
} | ||
case 'exports': { | ||
const fix = fixExports(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId: SUGGESTION_EXPORT, | ||
fix | ||
}]; | ||
return problem; | ||
} | ||
break; | ||
} | ||
break; | ||
case 'module': { | ||
const fix = fixModuleExports(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId: SUGGESTION_EXPORT, | ||
fix, | ||
}]; | ||
return problem; | ||
} | ||
case 'module': { | ||
const fix = fixModuleExports(node, sourceCode); | ||
if (fix) { | ||
problem.suggest = [{ | ||
messageId: SUGGESTION_EXPORT, | ||
fix | ||
}]; | ||
return problem; | ||
} | ||
break; | ||
} | ||
default: | ||
break; | ||
} | ||
return problem; | ||
default: | ||
} | ||
}; | ||
return problem; | ||
}); | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -335,8 +358,8 @@ create, | ||
docs: { | ||
description: 'Prefer JavaScript modules (ESM) over CommonJS.' | ||
description: 'Prefer JavaScript modules (ESM) over CommonJS.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const { | ||
getNegativeIndexLengthNode, | ||
removeLengthNode | ||
removeLengthNode, | ||
} = require('./shared/negative-index.js'); | ||
const typedArray = require('./shared/typed-array.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-negative-index'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer negative index over length minus index for `{{method}}`.' | ||
[MESSAGE_ID]: 'Prefer negative index over length minus index for `{{method}}`.', | ||
}; | ||
@@ -22,18 +23,8 @@ | ||
'ArrayBuffer', | ||
'Int8Array', | ||
'Uint8Array', | ||
'Uint8ClampedArray', | ||
'Int16Array', | ||
'Uint16Array', | ||
'Int32Array', | ||
'Uint32Array', | ||
'Float32Array', | ||
'Float64Array', | ||
'BigInt64Array', | ||
'BigUint64Array' | ||
...typedArray, | ||
// `{Blob,File}#slice()` are not generally used | ||
// 'Blob' | ||
// 'File' | ||
]) | ||
} | ||
]), | ||
}, | ||
], | ||
@@ -45,7 +36,16 @@ [ | ||
supportObjects: new Set([ | ||
'Array' | ||
]) | ||
} | ||
'Array', | ||
]), | ||
}, | ||
], | ||
[ | ||
'toSpliced', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
]), | ||
}, | ||
], | ||
[ | ||
'at', | ||
@@ -55,6 +55,18 @@ { | ||
supportObjects: new Set([ | ||
'Array' | ||
]) | ||
} | ||
] | ||
'Array', | ||
'String', | ||
...typedArray, | ||
]), | ||
}, | ||
], | ||
[ | ||
'with', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
...typedArray, | ||
]), | ||
}, | ||
], | ||
]); | ||
@@ -66,5 +78,4 @@ | ||
if ( | ||
type === 'MemberExpression' && | ||
property && | ||
property.type === 'Identifier' | ||
type === 'MemberExpression' | ||
&& property.type === 'Identifier' | ||
) { | ||
@@ -86,3 +97,3 @@ return property.name; | ||
target, | ||
argumentsNodes | ||
argumentsNodes, | ||
}; | ||
@@ -108,18 +119,18 @@ } | ||
if ( | ||
// [].{slice,splice} | ||
// `[].{slice,splice,toSpliced,at,with}` | ||
( | ||
parentCallee.type === 'ArrayExpression' && | ||
parentCallee.elements.length === 0 | ||
) || | ||
// ''.slice | ||
( | ||
method === 'slice' && | ||
isLiteralValue(parentCallee, '') | ||
) || | ||
parentCallee.type === 'ArrayExpression' | ||
&& parentCallee.elements.length === 0 | ||
) | ||
// `''.slice` | ||
|| ( | ||
method === 'slice' | ||
&& isLiteral(parentCallee, '') | ||
) | ||
// {Array,String...}.prototype.slice | ||
// Array.prototype.splice | ||
( | ||
getMemberName(parentCallee) === 'prototype' && | ||
parentCallee.object.type === 'Identifier' && | ||
supportObjects.has(parentCallee.object.name) | ||
|| ( | ||
getMemberName(parentCallee) === 'prototype' | ||
&& parentCallee.object.type === 'Identifier' | ||
&& supportObjects.has(parentCallee.object.name) | ||
) | ||
@@ -143,3 +154,3 @@ ) { | ||
target, | ||
argumentsNodes | ||
argumentsNodes, | ||
}; | ||
@@ -149,4 +160,9 @@ } | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
'CallExpression[callee.type="MemberExpression"]': node => { | ||
CallExpression(node) { | ||
if (node.callee.type !== 'MemberExpression') { | ||
return; | ||
} | ||
const parsed = parse(node); | ||
@@ -161,3 +177,3 @@ | ||
target, | ||
argumentsNodes | ||
argumentsNodes, | ||
} = parsed; | ||
@@ -179,11 +195,12 @@ | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
for (const node of removableNodes) { | ||
yield removeLengthNode(node, fixer, sourceCode); | ||
} | ||
} | ||
}, | ||
}; | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -194,7 +211,7 @@ create, | ||
docs: { | ||
description: 'Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`.' | ||
description: 'Prefer negative index over `.length - index` when possible.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isBuiltinModule = require('is-builtin-module'); | ||
const {matches, STATIC_REQUIRE_SOURCE_SELECTOR} = require('./selectors/index.js'); | ||
const {replaceStringLiteral} = require('./fix/index.js'); | ||
const isStaticRequire = require('./ast/is-static-require.js'); | ||
const MESSAGE_ID = 'prefer-node-protocol'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.' | ||
[MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.', | ||
}; | ||
const importExportSourceSelector = [ | ||
':matches(ImportDeclaration, ExportNamedDeclaration, ImportExpression)', | ||
' > ', | ||
'Literal.source' | ||
].join(''); | ||
const create = () => ({ | ||
Literal(node) { | ||
if (!( | ||
( | ||
( | ||
node.parent.type === 'ImportDeclaration' | ||
|| node.parent.type === 'ExportNamedDeclaration' | ||
|| node.parent.type === 'ImportExpression' | ||
) | ||
&& node.parent.source === node | ||
) | ||
|| ( | ||
isStaticRequire(node.parent) | ||
&& node.parent.arguments[0] === node | ||
) | ||
)) { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {checkRequire} = { | ||
checkRequire: false, | ||
...context.options[0] | ||
}; | ||
const selectors = [importExportSourceSelector]; | ||
if (checkRequire) { | ||
selectors.push(STATIC_REQUIRE_SOURCE_SELECTOR); | ||
} | ||
const {value} = node; | ||
return { | ||
[matches(selectors)](node) { | ||
const {value} = node; | ||
if ( | ||
typeof value !== 'string' || | ||
value.startsWith('node:') || | ||
!isBuiltinModule(value) | ||
) { | ||
return; | ||
} | ||
const firstCharacterIndex = node.range[0] + 1; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {moduleName: value}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => fixer.insertTextBeforeRange([firstCharacterIndex, firstCharacterIndex], 'node:') | ||
}; | ||
if ( | ||
typeof value !== 'string' | ||
|| value.startsWith('node:') | ||
|| !isBuiltinModule(value) | ||
) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
checkRequire: { | ||
type: 'boolean', | ||
default: false | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
]; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {moduleName: value}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => replaceStringLiteral(fixer, node, 'node:', 0, 0), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -68,8 +56,7 @@ create, | ||
docs: { | ||
description: 'Prefer using the `node:` protocol when importing Node.js builtin modules.' | ||
description: 'Prefer using the `node:` protocol when importing Node.js builtin modules.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const replaceReferenceIdentifier = require('./utils/replace-reference-identifier.js'); | ||
const { | ||
referenceIdentifierSelector, | ||
callExpressionSelector | ||
} = require('./selectors/index.js'); | ||
const {GlobalReferenceTracker} = require('./utils/global-reference-tracker.js'); | ||
const {replaceReferenceIdentifier} = require('./fix/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const isLeftHandSide = require('./utils/is-left-hand-side.js'); | ||
const METHOD_ERROR_MESSAGE_ID = 'method-error'; | ||
const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion'; | ||
const PROPERTY_ERROR_MESSAGE_ID = 'property-error'; | ||
const MESSAGE_ID_ERROR = 'error'; | ||
const MESSAGE_ID_SUGGESTION = 'suggestion'; | ||
const messages = { | ||
[METHOD_ERROR_MESSAGE_ID]: 'Prefer `Number.{{name}}()` over `{{name}}()`.', | ||
[METHOD_SUGGESTION_MESSAGE_ID]: 'Replace `{{name}}()` with `Number.{{name}}()`.', | ||
[PROPERTY_ERROR_MESSAGE_ID]: 'Prefer `Number.{{property}}` over `{{identifier}}`.' | ||
[MESSAGE_ID_ERROR]: 'Prefer `Number.{{property}}` over `{{description}}`.', | ||
[MESSAGE_ID_SUGGESTION]: 'Replace `{{description}}` with `Number.{{property}}`.', | ||
}; | ||
const methods = { | ||
// Safe | ||
const globalObjects = { | ||
// Safe to replace with `Number` properties | ||
parseInt: true, | ||
parseFloat: true, | ||
// Unsafe | ||
NaN: true, | ||
Infinity: true, | ||
// Unsafe to replace with `Number` properties | ||
isNaN: false, | ||
isFinite: false | ||
isFinite: false, | ||
}; | ||
const methodsSelector = [ | ||
callExpressionSelector(Object.keys(methods)), | ||
' > ', | ||
'.callee' | ||
].join(''); | ||
const propertiesSelector = referenceIdentifierSelector(['NaN', 'Infinity']); | ||
const isNegative = node => { | ||
const {parent} = node; | ||
return parent && parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node; | ||
return parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node; | ||
}; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const options = { | ||
checkInfinity: true, | ||
...context.options[0] | ||
}; | ||
function checkProperty({node, path: [name]}, sourceCode) { | ||
const {parent} = node; | ||
// Cache `NaN` and `Infinity` in `foo = {NaN, Infinity}` | ||
const reported = new WeakSet(); | ||
let property = name; | ||
if (name === 'Infinity') { | ||
property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY'; | ||
} | ||
return { | ||
[methodsSelector]: node => { | ||
if (isShadowed(context.getScope(), node)) { | ||
return; | ||
} | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_ERROR, | ||
data: { | ||
description: name, | ||
property, | ||
}, | ||
}; | ||
const {name} = node; | ||
const isSafe = methods[name]; | ||
if (property === 'NEGATIVE_INFINITY') { | ||
problem.node = parent; | ||
problem.data.description = '-Infinity'; | ||
problem.fix = function * (fixer) { | ||
yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); | ||
yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); | ||
}; | ||
const problem = { | ||
node, | ||
messageId: METHOD_ERROR_MESSAGE_ID, | ||
data: { | ||
name | ||
} | ||
}; | ||
return problem; | ||
} | ||
const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode); | ||
const fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); | ||
const isSafeToFix = globalObjects[name]; | ||
if (isSafe) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: METHOD_SUGGESTION_MESSAGE_ID, | ||
data: { | ||
name | ||
}, | ||
fix | ||
} | ||
]; | ||
} | ||
if (isSafeToFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
fix, | ||
}, | ||
]; | ||
} | ||
return problem; | ||
}, | ||
[propertiesSelector]: node => { | ||
if (reported.has(node) || isShadowed(context.getScope(), node)) { | ||
return; | ||
} | ||
return problem; | ||
} | ||
const {name, parent} = node; | ||
if (name === 'Infinity' && !options.checkInfinity) { | ||
return; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const { | ||
checkInfinity, | ||
} = { | ||
checkInfinity: true, | ||
...context.options[0], | ||
}; | ||
const {sourceCode} = context; | ||
let property = name; | ||
if (name === 'Infinity') { | ||
property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY'; | ||
} | ||
let objects = Object.keys(globalObjects); | ||
if (!checkInfinity) { | ||
objects = objects.filter(name => name !== 'Infinity'); | ||
} | ||
const problem = { | ||
node, | ||
messageId: PROPERTY_ERROR_MESSAGE_ID, | ||
data: { | ||
identifier: name, | ||
property | ||
} | ||
}; | ||
const tracker = new GlobalReferenceTracker({ | ||
objects, | ||
handle: reference => checkProperty(reference, sourceCode), | ||
filter: ({node}) => !isLeftHandSide(node), | ||
}); | ||
if (property === 'NEGATIVE_INFINITY') { | ||
problem.node = parent; | ||
problem.data.identifier = '-Infinity'; | ||
problem.fix = fixer => fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); | ||
} else { | ||
problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); | ||
} | ||
reported.add(node); | ||
return problem; | ||
} | ||
}; | ||
return tracker.createListeners(context); | ||
}; | ||
@@ -126,12 +103,13 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkInfinity: { | ||
type: 'boolean', | ||
default: true | ||
} | ||
default: true, | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -142,9 +120,9 @@ create, | ||
docs: { | ||
description: 'Prefer `Number` static properties over global ones.' | ||
description: 'Prefer `Number` static properties over global ones.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
@@ -9,58 +9,58 @@ | ||
[MESSAGE_ID_WITH_NAME]: 'Remove unused catch binding `{{name}}`.', | ||
[MESSAGE_ID_WITHOUT_NAME]: 'Remove unused catch binding.' | ||
[MESSAGE_ID_WITHOUT_NAME]: 'Remove unused catch binding.', | ||
}; | ||
const selector = [ | ||
'CatchClause', | ||
' > ', | ||
'.param' | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CatchClause(catchClause) { | ||
const node = catchClause.param; | ||
if (!node) { | ||
return; | ||
} | ||
const create = context => { | ||
return { | ||
[selector]: node => { | ||
const variables = context.getDeclaredVariables(node.parent); | ||
const {sourceCode} = context; | ||
const variables = sourceCode.getDeclaredVariables(node.parent); | ||
if (variables.some(variable => variable.references.length > 0)) { | ||
return; | ||
} | ||
if (variables.some(variable => variable.references.length > 0)) { | ||
return; | ||
} | ||
const {type, name, parent} = node; | ||
const {type, name, parent} = node; | ||
return { | ||
node, | ||
messageId: type === 'Identifier' ? MESSAGE_ID_WITH_NAME : MESSAGE_ID_WITHOUT_NAME, | ||
data: {name}, | ||
* fix(fixer) { | ||
const tokenBefore = context.getTokenBefore(node); | ||
assertToken(tokenBefore, { | ||
test: isOpeningParenToken, | ||
expected: '(', | ||
ruleId: 'prefer-optional-catch-binding' | ||
}); | ||
return { | ||
node, | ||
messageId: type === 'Identifier' ? MESSAGE_ID_WITH_NAME : MESSAGE_ID_WITHOUT_NAME, | ||
data: {name}, | ||
* fix(fixer) { | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
assertToken(tokenBefore, { | ||
test: isOpeningParenToken, | ||
expected: '(', | ||
ruleId: 'prefer-optional-catch-binding', | ||
}); | ||
const tokenAfter = context.getTokenAfter(node); | ||
assertToken(tokenAfter, { | ||
test: isClosingParenToken, | ||
expected: ')', | ||
ruleId: 'prefer-optional-catch-binding' | ||
}); | ||
const tokenAfter = sourceCode.getTokenAfter(node); | ||
assertToken(tokenAfter, { | ||
test: isClosingParenToken, | ||
expected: ')', | ||
ruleId: 'prefer-optional-catch-binding', | ||
}); | ||
yield fixer.remove(tokenBefore); | ||
yield fixer.remove(node); | ||
yield fixer.remove(tokenAfter); | ||
yield fixer.remove(tokenBefore); | ||
yield fixer.remove(node); | ||
yield fixer.remove(tokenAfter); | ||
const [, endOfClosingParenthesis] = tokenAfter.range; | ||
const [startOfCatchClauseBody] = parent.body.range; | ||
const text = context.getSourceCode().text.slice(endOfClosingParenthesis, startOfCatchClauseBody); | ||
const leadingSpacesLength = text.length - text.trimStart().length; | ||
if (leadingSpacesLength !== 0) { | ||
yield fixer.removeRange([endOfClosingParenthesis, endOfClosingParenthesis + leadingSpacesLength]); | ||
} | ||
const [, endOfClosingParenthesis] = tokenAfter.range; | ||
const [startOfCatchClauseBody] = parent.body.range; | ||
const text = sourceCode.text.slice(endOfClosingParenthesis, startOfCatchClauseBody); | ||
const leadingSpacesLength = text.length - text.trimStart().length; | ||
if (leadingSpacesLength !== 0) { | ||
yield fixer.removeRange([endOfClosingParenthesis, endOfClosingParenthesis + leadingSpacesLength]); | ||
} | ||
}; | ||
} | ||
}; | ||
}; | ||
}, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -71,7 +71,7 @@ create, | ||
docs: { | ||
description: 'Prefer omitting the `catch` binding parameter.' | ||
description: 'Prefer omitting the `catch` binding parameter.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const { | ||
methodCallSelector, | ||
emptyObjectSelector, | ||
emptyArraySelector, | ||
matches | ||
} = require('./selectors/index.js'); | ||
const getPropertyName = require('./utils/get-property-name.js'); | ||
const {getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isMemberExpression, isMethodCall} = require('./ast/index.js'); | ||
const messages = { | ||
'known-method': 'Prefer using `{{constructorName}}.prototype.{{methodName}}`.', | ||
'unknown-method': 'Prefer using method from `{{constructorName}}.prototype`.' | ||
'unknown-method': 'Prefer using method from `{{constructorName}}.prototype`.', | ||
}; | ||
const emptyObjectOrArrayMethodSelector = [ | ||
'MemberExpression', | ||
matches([emptyObjectSelector('object'), emptyArraySelector('object')]) | ||
].join(''); | ||
const selector = matches([ | ||
// `[].foo.{apply,bind,call}(…)` | ||
// `({}).foo.{apply,bind,call}(…)` | ||
[ | ||
methodCallSelector(['apply', 'bind', 'call']), | ||
' > ', | ||
'.callee', | ||
' > ', | ||
`${emptyObjectOrArrayMethodSelector}.object` | ||
].join(''), | ||
// `Reflect.apply([].foo, …)` | ||
// `Reflect.apply({}.foo, …)` | ||
[ | ||
methodCallSelector({object: 'Reflect', name: 'apply', min: 1}), | ||
' > ', | ||
`${emptyObjectOrArrayMethodSelector}.arguments:first-child` | ||
].join('') | ||
]); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
function create(context) { | ||
return { | ||
[selector](node) { | ||
const constructorName = node.object.type === 'ArrayExpression' ? 'Array' : 'Object'; | ||
const methodName = getPropertyName(node, context.getScope()); | ||
CallExpression(callExpression) { | ||
let methodNode; | ||
if ( | ||
// `Reflect.apply([].foo, …)` | ||
// `Reflect.apply({}.foo, …)` | ||
isMethodCall(callExpression, { | ||
object: 'Reflect', | ||
method: 'apply', | ||
minimumArguments: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) { | ||
methodNode = callExpression.arguments[0]; | ||
} else if ( | ||
// `[].foo.{apply,bind,call}(…)` | ||
// `({}).foo.{apply,bind,call}(…)` | ||
isMethodCall(callExpression, { | ||
names: ['apply', 'bind', 'call'], | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) { | ||
methodNode = callExpression.callee.object; | ||
} | ||
if (!methodNode || !isMemberExpression(methodNode, {optional: false})) { | ||
return; | ||
} | ||
const objectNode = methodNode.object; | ||
if (!( | ||
(objectNode.type === 'ArrayExpression' && objectNode.elements.length === 0) | ||
|| (objectNode.type === 'ObjectExpression' && objectNode.properties.length === 0) | ||
)) { | ||
return; | ||
} | ||
const constructorName = objectNode.type === 'ArrayExpression' ? 'Array' : 'Object'; | ||
const {sourceCode} = context; | ||
const methodName = getPropertyName(methodNode, sourceCode.getScope(methodNode)); | ||
return { | ||
node, | ||
node: methodNode, | ||
messageId: methodName ? 'known-method' : 'unknown-method', | ||
data: {constructorName, methodName: String(methodName)}, | ||
fix: fixer => fixer.replaceText(node.object, `${constructorName}.prototype`) | ||
data: {constructorName, methodName}, | ||
* fix(fixer) { | ||
yield fixer.replaceText(objectNode, `${constructorName}.prototype`); | ||
if ( | ||
objectNode.type === 'ArrayExpression' | ||
|| objectNode.type === 'ObjectExpression' | ||
) { | ||
yield * fixSpaceAroundKeyword(fixer, callExpression, sourceCode); | ||
} | ||
}, | ||
}; | ||
} | ||
}, | ||
}; | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -60,7 +83,7 @@ create, | ||
docs: { | ||
description: 'Prefer borrowing methods from the prototype instead of the instance.' | ||
description: 'Prefer borrowing methods from the prototype instead of the instance.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js'); | ||
const {isNodeValueNotDomNode} = require('./utils/index.js'); | ||
const {isMethodCall, isStringLiteral, isNullLiteral} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-query-selector'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `.{{replacement}}()` over `.{{method}}()`.' | ||
[MESSAGE_ID]: 'Prefer `.{{replacement}}()` over `.{{method}}()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
names: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'], | ||
length: 1 | ||
}), | ||
notDomNodeSelector('callee.object') | ||
].join(''); | ||
const forbiddenIdentifierNames = new Map([ | ||
const disallowedIdentifierNames = new Map([ | ||
['getElementById', 'querySelector'], | ||
['getElementsByClassName', 'querySelectorAll'], | ||
['getElementsByTagName', 'querySelectorAll'] | ||
['getElementsByTagName', 'querySelectorAll'], | ||
]); | ||
@@ -28,3 +21,3 @@ | ||
const leftQuote = node.raw.charAt(0); | ||
const rightQuote = node.raw.charAt(node.raw.length - 1); | ||
const rightQuote = node.raw.at(-1); | ||
return `${leftQuote}${value}${rightQuote}`; | ||
@@ -54,3 +47,3 @@ }; | ||
templateElement, | ||
getReplacementForId(templateElement.value.cooked) | ||
getReplacementForId(templateElement.value.cooked), | ||
); | ||
@@ -62,3 +55,3 @@ } | ||
templateElement, | ||
getReplacementForClass(templateElement.value.cooked) | ||
getReplacementForClass(templateElement.value.cooked), | ||
); | ||
@@ -69,17 +62,11 @@ } | ||
const canBeFixed = node => { | ||
if (node.type === 'Literal') { | ||
return node.raw === 'null' || (typeof node.value === 'string' && Boolean(node.value.trim())); | ||
} | ||
const canBeFixed = node => | ||
isNullLiteral(node) | ||
|| (isStringLiteral(node) && Boolean(node.value.trim())) | ||
|| ( | ||
node.type === 'TemplateLiteral' | ||
&& node.expressions.length === 0 | ||
&& node.quasis.some(templateElement => templateElement.value.cooked.trim()) | ||
); | ||
if (node.type === 'TemplateLiteral') { | ||
return ( | ||
node.expressions.length === 0 && | ||
node.quasis.some(templateElement => templateElement.value.cooked.trim()) | ||
); | ||
} | ||
return false; | ||
}; | ||
const hasValue = node => { | ||
@@ -106,26 +93,38 @@ if (node.type === 'Literal') { | ||
const create = () => { | ||
return { | ||
[selector](node) { | ||
const method = node.callee.property.name; | ||
const preferredSelector = forbiddenIdentifierNames.get(method); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isNodeValueNotDomNode(node.callee.object) | ||
) { | ||
return; | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: MESSAGE_ID, | ||
data: { | ||
replacement: preferredSelector, | ||
method | ||
} | ||
}; | ||
const method = node.callee.property.name; | ||
const preferredSelector = disallowedIdentifierNames.get(method); | ||
if (canBeFixed(node.arguments[0])) { | ||
problem.fix = fix(node, method, preferredSelector); | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: MESSAGE_ID, | ||
data: { | ||
replacement: preferredSelector, | ||
method, | ||
}, | ||
}; | ||
return problem; | ||
if (canBeFixed(node.arguments[0])) { | ||
problem.fix = fix(node, method, preferredSelector); | ||
} | ||
}; | ||
}; | ||
return problem; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -136,7 +135,7 @@ create, | ||
docs: { | ||
description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`.' | ||
description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const getPropertyName = require('./utils/get-property-name.js'); | ||
const {not, methodCallSelector} = require('./selectors/index.js'); | ||
const {getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {isNullLiteral, isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-reflect-apply'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `Reflect.apply()` over `Function#apply()`.' | ||
[MESSAGE_ID]: 'Prefer `Reflect.apply()` over `Function#apply()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({allowComputed: true}), | ||
not(['Literal', 'ArrayExpression', 'ObjectExpression'].map(type => `[callee.object.type=${type}]`)) | ||
].join(''); | ||
const isApplySignature = (argument1, argument2) => ( | ||
( | ||
// eslint-disable-next-line unicorn/no-null | ||
isLiteralValue(argument1, null) || | ||
argument1.type === 'ThisExpression' | ||
) && | ||
( | ||
argument2.type === 'ArrayExpression' || | ||
(argument2.type === 'Identifier' && argument2.name === 'arguments') | ||
isNullLiteral(argument1) | ||
|| argument1.type === 'ThisExpression' | ||
) | ||
&& ( | ||
argument2.type === 'ArrayExpression' | ||
|| (argument2.type === 'Identifier' && argument2.name === 'arguments') | ||
) | ||
); | ||
@@ -34,5 +27,5 @@ | ||
if ( | ||
getPropertyName(node.callee) === 'apply' && | ||
node.arguments.length === 2 && | ||
isApplySignature(node.arguments[0], node.arguments[1]) | ||
getPropertyName(node.callee) === 'apply' | ||
&& node.arguments.length === 2 | ||
&& isApplySignature(node.arguments[0], node.arguments[1]) | ||
) { | ||
@@ -42,3 +35,3 @@ return fixer => ( | ||
node, | ||
getReflectApplyCall(sourceCode, node.callee.object, node.arguments[0], node.arguments[1]) | ||
getReflectApplyCall(sourceCode, node.callee.object, node.arguments[0], node.arguments[1]), | ||
) | ||
@@ -51,10 +44,9 @@ ); | ||
if ( | ||
getPropertyName(node.callee) === 'call' && | ||
getPropertyName(node.callee.object) === 'apply' && | ||
getPropertyName(node.callee.object.object) === 'prototype' && | ||
node.callee.object.object.object && | ||
node.callee.object.object.object.type === 'Identifier' && | ||
node.callee.object.object.object.name === 'Function' && | ||
node.arguments.length === 3 && | ||
isApplySignature(node.arguments[1], node.arguments[2]) | ||
getPropertyName(node.callee) === 'call' | ||
&& getPropertyName(node.callee.object) === 'apply' | ||
&& getPropertyName(node.callee.object.object) === 'prototype' | ||
&& node.callee.object.object.object?.type === 'Identifier' | ||
&& node.callee.object.object.object.name === 'Function' | ||
&& node.arguments.length === 3 | ||
&& isApplySignature(node.arguments[1], node.arguments[2]) | ||
) { | ||
@@ -64,3 +56,3 @@ return fixer => ( | ||
node, | ||
getReflectApplyCall(sourceCode, node.arguments[0], node.arguments[1], node.arguments[2]) | ||
getReflectApplyCall(sourceCode, node.arguments[0], node.arguments[1], node.arguments[2]), | ||
) | ||
@@ -71,18 +63,30 @@ ); | ||
const create = context => { | ||
return { | ||
[selector]: node => { | ||
const sourceCode = context.getSourceCode(); | ||
const fix = fixDirectApplyCall(node, sourceCode) || fixFunctionPrototypeCall(node, sourceCode); | ||
if (fix) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix | ||
}; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| node.callee.object.type === 'Literal' | ||
|| node.callee.object.type === 'ArrayExpression' | ||
|| node.callee.object.type === 'ObjectExpression' | ||
) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const {sourceCode} = context; | ||
const fix = fixDirectApplyCall(node, sourceCode) || fixFunctionPrototypeCall(node, sourceCode); | ||
if (fix) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix, | ||
}; | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -93,7 +97,7 @@ create, | ||
docs: { | ||
description: 'Prefer `Reflect.apply()` over `Function#apply()`.' | ||
description: 'Prefer `Reflect.apply()` over `Function#apply()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('eslint-utils'); | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isBooleanNode} = require('./utils/boolean.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {isRegexLiteral, isNewExpression, isMethodCall} = require('./ast/index.js'); | ||
const { | ||
isBooleanNode, | ||
shouldAddParenthesesToMemberExpressionObject, | ||
} = require('./utils/index.js'); | ||
const REGEXP_EXEC = 'regexp-exec'; | ||
const STRING_MATCH = 'string-match'; | ||
const SUGGESTION = 'suggestion'; | ||
const messages = { | ||
[REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.', | ||
[STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.' | ||
[STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.', | ||
[SUGGESTION]: 'Switch to `RegExp#test(…)`.', | ||
}; | ||
@@ -18,5 +22,7 @@ | ||
type: REGEXP_EXEC, | ||
selector: methodCallSelector({ | ||
name: 'exec', | ||
length: 1 | ||
test: node => isMethodCall(node, { | ||
method: 'exec', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -26,11 +32,13 @@ getNodes: node => ({ | ||
methodNode: node.callee.property, | ||
regexpNode: node.callee.object | ||
regexpNode: node.callee.object, | ||
}), | ||
fix: (fixer, {methodNode}) => fixer.replaceText(methodNode, 'test') | ||
fix: (fixer, {methodNode}) => fixer.replaceText(methodNode, 'test'), | ||
}, | ||
{ | ||
type: STRING_MATCH, | ||
selector: methodCallSelector({ | ||
name: 'match', | ||
length: 1 | ||
test: node => isMethodCall(node, { | ||
method: 'match', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -40,3 +48,3 @@ getNodes: node => ({ | ||
methodNode: node.callee.property, | ||
regexpNode: node.arguments[0] | ||
regexpNode: node.arguments[0], | ||
}), | ||
@@ -48,5 +56,5 @@ * fix(fixer, {stringNode, methodNode, regexpNode}, sourceCode) { | ||
if ( | ||
!isParenthesized(regexpNode, sourceCode) && | ||
!isParenthesized(regexpNode, sourceCode) | ||
// Only `SequenceExpression` need add parentheses | ||
stringNode.type === 'SequenceExpression' | ||
&& stringNode.type === 'SequenceExpression' | ||
) { | ||
@@ -60,4 +68,4 @@ stringText = `(${stringText})`; | ||
if ( | ||
!isParenthesized(stringNode, sourceCode) && | ||
shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode) | ||
!isParenthesized(stringNode, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode) | ||
) { | ||
@@ -70,31 +78,39 @@ regexpText = `(${regexpText})`; | ||
yield fixer.replaceText(stringNode, regexpText); | ||
} | ||
} | ||
}, | ||
}, | ||
]; | ||
const isRegExpNode = node => { | ||
if (node.type === 'Literal' && node.regex) { | ||
return true; | ||
const isRegExpNode = node => isRegexLiteral(node) || isNewExpression(node, {name: 'RegExp'}); | ||
const isRegExpWithoutGlobalFlag = (node, scope) => { | ||
if (isRegexLiteral(node)) { | ||
return !node.regex.flags.includes('g'); | ||
} | ||
if ( | ||
node.type === 'NewExpression' && | ||
node.callee.type === 'Identifier' && | ||
node.callee.name === 'RegExp' | ||
) { | ||
return true; | ||
const staticResult = getStaticValue(node, scope); | ||
// Don't know if there is `g` flag | ||
if (!staticResult) { | ||
return false; | ||
} | ||
return false; | ||
const {value} = staticResult; | ||
return ( | ||
Object.prototype.toString.call(value) === '[object RegExp]' | ||
&& !value.global | ||
); | ||
}; | ||
const create = context => Object.fromEntries( | ||
cases.map(checkCase => [ | ||
checkCase.selector, | ||
node => { | ||
if (!isBooleanNode(node)) { | ||
return; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
* CallExpression(node) { | ||
if (!isBooleanNode(node)) { | ||
return; | ||
} | ||
for (const {type, test, getNodes, fix} of cases) { | ||
if (!test(node)) { | ||
continue; | ||
} | ||
const {type, getNodes, fix} = checkCase; | ||
const nodes = getNodes(node); | ||
@@ -104,3 +120,3 @@ const {methodNode, regexpNode} = nodes; | ||
if (regexpNode.type === 'Literal' && !regexpNode.regex) { | ||
return; | ||
continue; | ||
} | ||
@@ -110,24 +126,28 @@ | ||
node: type === REGEXP_EXEC ? methodNode : node, | ||
messageId: type | ||
messageId: type, | ||
}; | ||
if (!isRegExpNode(regexpNode)) { | ||
const staticResult = getStaticValue(regexpNode, context.getScope()); | ||
if (staticResult) { | ||
const {value} = staticResult; | ||
if ( | ||
Object.prototype.toString.call(value) !== '[object RegExp]' || | ||
value.flags.includes('g') | ||
) { | ||
return problem; | ||
} | ||
} | ||
const {sourceCode} = context; | ||
const fixFunction = fixer => fix(fixer, nodes, sourceCode); | ||
if ( | ||
isRegExpNode(regexpNode) | ||
|| isRegExpWithoutGlobalFlag(regexpNode, sourceCode.getScope(regexpNode)) | ||
) { | ||
problem.fix = fixFunction; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION, | ||
fix: fixFunction, | ||
}, | ||
]; | ||
} | ||
problem.fix = fixer => fix(fixer, nodes, context.getSourceCode()); | ||
return problem; | ||
yield problem; | ||
} | ||
]) | ||
); | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -138,7 +158,8 @@ create: checkVueTemplate(create), | ||
docs: { | ||
description: 'Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`.' | ||
description: 'Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
hasSuggestions: true, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const getVariableIdentifiers = require('./utils/get-variable-identifiers.js'); | ||
const { | ||
matches, | ||
not, | ||
methodCallSelector, | ||
callOrNewExpressionSelector | ||
} = require('./selectors/index.js'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const {getVariableIdentifiers} = require('./utils/index.js'); | ||
const {isCallOrNewExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -15,83 +10,36 @@ const MESSAGE_ID_ERROR = 'error'; | ||
[MESSAGE_ID_ERROR]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.', | ||
[MESSAGE_ID_SUGGESTION]: 'Switch `{{name}}` to `Set`.' | ||
[MESSAGE_ID_SUGGESTION]: 'Switch `{{name}}` to `Set`.', | ||
}; | ||
// `[]` | ||
const arrayExpressionSelector = [ | ||
'[init.type="ArrayExpression"]' | ||
].join(''); | ||
const arrayMethodsReturnsArray = [ | ||
'concat', | ||
'copyWithin', | ||
'fill', | ||
'filter', | ||
'flat', | ||
'flatMap', | ||
'map', | ||
'reverse', | ||
'slice', | ||
'sort', | ||
'splice', | ||
'toReversed', | ||
'toSorted', | ||
'toSpliced', | ||
'with', | ||
]; | ||
// `Array()` and `new Array()` | ||
const newArraySelector = callOrNewExpressionSelector({name: 'Array', path: 'init'}); | ||
// `Array.from()` and `Array.of()` | ||
const arrayStaticMethodSelector = methodCallSelector({ | ||
object: 'Array', | ||
names: ['from', 'of'], | ||
path: 'init' | ||
}); | ||
// `array.concat()` | ||
// `array.copyWithin()` | ||
// `array.fill()` | ||
// `array.filter()` | ||
// `array.flat()` | ||
// `array.flatMap()` | ||
// `array.map()` | ||
// `array.reverse()` | ||
// `array.slice()` | ||
// `array.sort()` | ||
// `array.splice()` | ||
const arrayMethodSelector = methodCallSelector({ | ||
names: [ | ||
'concat', | ||
'copyWithin', | ||
'fill', | ||
'filter', | ||
'flat', | ||
'flatMap', | ||
'map', | ||
'reverse', | ||
'slice', | ||
'sort', | ||
'splice' | ||
], | ||
path: 'init' | ||
}); | ||
const selector = [ | ||
'VariableDeclaration', | ||
// Exclude `export const foo = [];` | ||
not('ExportNamedDeclaration > .declaration'), | ||
' > ', | ||
'VariableDeclarator.declarations', | ||
matches([ | ||
arrayExpressionSelector, | ||
newArraySelector, | ||
arrayStaticMethodSelector, | ||
arrayMethodSelector | ||
]), | ||
' > ', | ||
'Identifier.id' | ||
].join(''); | ||
const isIncludesCall = node => { | ||
/* istanbul ignore next */ | ||
if (!node.parent || !node.parent.parent) { | ||
return false; | ||
} | ||
const {type, optional, callee, arguments: includesArguments} = node.parent.parent; | ||
const {type, optional, callee, arguments: includesArguments} = node.parent.parent ?? {}; | ||
return ( | ||
type === 'CallExpression' && | ||
!optional && | ||
callee && | ||
callee.type === 'MemberExpression' && | ||
!callee.computed && | ||
!callee.optional && | ||
callee.object === node && | ||
callee.property.type === 'Identifier' && | ||
callee.property.name === 'includes' && | ||
includesArguments.length === 1 && | ||
includesArguments[0].type !== 'SpreadElement' | ||
type === 'CallExpression' | ||
&& !optional | ||
&& callee.type === 'MemberExpression' | ||
&& !callee.computed | ||
&& !callee.optional | ||
&& callee.object === node | ||
&& callee.property.type === 'Identifier' | ||
&& callee.property.name === 'includes' | ||
&& includesArguments.length === 1 | ||
&& includesArguments[0].type !== 'SpreadElement' | ||
); | ||
@@ -108,3 +56,3 @@ }; | ||
'FunctionExpression', | ||
'ArrowFunctionExpression' | ||
'ArrowFunctionExpression', | ||
]); | ||
@@ -116,4 +64,4 @@ | ||
while ( | ||
parent && | ||
parent !== root | ||
parent | ||
&& parent !== root | ||
) { | ||
@@ -130,66 +78,102 @@ if (multipleCallNodeTypes.has(parent.type)) { | ||
const create = context => { | ||
return { | ||
[selector]: node => { | ||
const variable = findVariable(context.getScope(), node); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
Identifier(node) { | ||
const {parent} = node; | ||
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342 | ||
// But can't reproduce, just ignore this case | ||
/* istanbul ignore next */ | ||
if (!variable) { | ||
return; | ||
} | ||
if (!( | ||
parent.type === 'VariableDeclarator' | ||
&& parent.id === node | ||
&& Boolean(parent.init) | ||
&& parent.parent.type === 'VariableDeclaration' | ||
&& parent.parent.declarations.includes(parent) | ||
// Exclude `export const foo = [];` | ||
&& !( | ||
parent.parent.parent.type === 'ExportNamedDeclaration' | ||
&& parent.parent.parent.declaration === parent.parent | ||
) | ||
&& ( | ||
// `[]` | ||
parent.init.type === 'ArrayExpression' | ||
// `Array()` and `new Array()` | ||
|| isCallOrNewExpression(parent.init, { | ||
name: 'Array', | ||
optional: false, | ||
}) | ||
// `Array.from()` and `Array.of()` | ||
|| isMethodCall(parent.init, { | ||
object: 'Array', | ||
methods: ['from', 'of'], | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// Array methods that return an array | ||
|| isMethodCall(parent.init, { | ||
methods: arrayMethodsReturnsArray, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
)) { | ||
return; | ||
} | ||
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node); | ||
const variable = findVariable(context.sourceCode.getScope(node), node); | ||
if ( | ||
identifiers.length === 0 || | ||
identifiers.some(identifier => !isIncludesCall(identifier)) | ||
) { | ||
return; | ||
} | ||
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342 | ||
// But can't reproduce, just ignore this case | ||
/* c8 ignore next 3 */ | ||
if (!variable) { | ||
return; | ||
} | ||
if ( | ||
identifiers.length === 1 && | ||
identifiers.every(identifier => !isMultipleCall(identifier, node)) | ||
) { | ||
return; | ||
} | ||
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node); | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_ERROR, | ||
data: { | ||
name: node.name | ||
} | ||
}; | ||
if ( | ||
identifiers.length === 0 | ||
|| identifiers.some(identifier => !isIncludesCall(identifier)) | ||
) { | ||
return; | ||
} | ||
const fix = function * (fixer) { | ||
yield fixer.insertTextBefore(node.parent.init, 'new Set('); | ||
yield fixer.insertTextAfter(node.parent.init, ')'); | ||
if ( | ||
identifiers.length === 1 | ||
&& identifiers.every(identifier => !isMultipleCall(identifier, node)) | ||
) { | ||
return; | ||
} | ||
for (const identifier of identifiers) { | ||
yield fixer.replaceText(identifier.parent.property, 'has'); | ||
} | ||
}; | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_ERROR, | ||
data: { | ||
name: node.name, | ||
}, | ||
}; | ||
if (node.typeAnnotation) { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: { | ||
name: node.name | ||
}, | ||
fix | ||
} | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
const fix = function * (fixer) { | ||
yield fixer.insertTextBefore(node.parent.init, 'new Set('); | ||
yield fixer.insertTextAfter(node.parent.init, ')'); | ||
for (const identifier of identifiers) { | ||
yield fixer.replaceText(identifier.parent.property, 'has'); | ||
} | ||
}; | ||
return problem; | ||
if (node.typeAnnotation) { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
fix, | ||
}, | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
} | ||
}; | ||
}; | ||
return problem; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -200,8 +184,8 @@ create, | ||
docs: { | ||
description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.' | ||
description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {getParenthesizedRange, getParenthesizedText} = require('./utils/parentheses.js'); | ||
const shouldAddParenthesesToSpreadElementArgument = require('./utils/should-add-parentheses-to-spread-element-argument.js'); | ||
const replaceNodeOrTokenAndSpacesBefore = require('./utils/replace-node-or-token-and-spaces-before.js'); | ||
const removeSpacesAfter = require('./utils/remove-spaces-after.js'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const { | ||
getParenthesizedRange, | ||
getParenthesizedText, | ||
needsSemicolon, | ||
shouldAddParenthesesToSpreadElementArgument, | ||
isNodeMatches, | ||
isMethodNamed, | ||
} = require('./utils/index.js'); | ||
const {removeMethodCall} = require('./fix/index.js'); | ||
const {isLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -15,2 +17,4 @@ const ERROR_ARRAY_FROM = 'array-from'; | ||
const ERROR_ARRAY_SLICE = 'array-slice'; | ||
const ERROR_ARRAY_TO_SPLICED = 'array-to-spliced'; | ||
const ERROR_STRING_SPLIT = 'string-split'; | ||
const SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE = 'argument-is-spreadable'; | ||
@@ -20,2 +24,3 @@ const SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE = 'argument-is-not-spreadable'; | ||
const SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS = 'spread-all-arguments'; | ||
const SUGGESTION_USE_SPREAD = 'use-spread'; | ||
const messages = { | ||
@@ -25,42 +30,11 @@ [ERROR_ARRAY_FROM]: 'Prefer the spread operator over `Array.from(…)`.', | ||
[ERROR_ARRAY_SLICE]: 'Prefer the spread operator over `Array#slice()`.', | ||
[ERROR_ARRAY_TO_SPLICED]: 'Prefer the spread operator over `Array#toSpliced()`.', | ||
[ERROR_STRING_SPLIT]: 'Prefer the spread operator over `String#split(\'\')`.', | ||
[SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE]: 'First argument is an `array`.', | ||
[SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE]: 'First argument is not an `array`.', | ||
[SUGGESTION_CONCAT_TEST_ARGUMENT]: 'Test first argument with `Array.isArray(…)`.', | ||
[SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS]: 'Spread all unknown arguments`.' | ||
[SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS]: 'Spread all unknown arguments`.', | ||
[SUGGESTION_USE_SPREAD]: 'Use `...` operator.', | ||
}; | ||
const arrayFromCallSelector = [ | ||
methodCallSelector({ | ||
object: 'Array', | ||
name: 'from', | ||
min: 1, | ||
max: 3 | ||
}), | ||
// Allow `Array.from({length})` | ||
'[arguments.0.type!="ObjectExpression"]' | ||
].join(''); | ||
const arrayConcatCallSelector = [ | ||
methodCallSelector('concat'), | ||
`:not(${ | ||
[ | ||
...[ | ||
'Literal', | ||
'TemplateLiteral' | ||
].map(type => `[callee.object.type="${type}"]`), | ||
// Most likely it's a static method of a class | ||
'[callee.object.name=/^[A-Z]/]' | ||
].join(', ') | ||
})` | ||
].join(''); | ||
const arraySliceCallSelector = [ | ||
methodCallSelector({ | ||
name: 'slice', | ||
min: 0, | ||
max: 1 | ||
}), | ||
'[callee.object.type!="ArrayExpression"]' | ||
].join(''); | ||
const ignoredSliceCallee = [ | ||
@@ -71,3 +45,3 @@ 'arrayBuffer', | ||
'file', | ||
'this' | ||
'this', | ||
]; | ||
@@ -84,11 +58,2 @@ | ||
const getRangeAfterCalleeObject = (node, sourceCode) => { | ||
const {object} = node.callee; | ||
const parenthesizedRange = getParenthesizedRange(object, sourceCode); | ||
const [, start] = parenthesizedRange; | ||
const [, end] = node.range; | ||
return [start, end]; | ||
}; | ||
function fixConcat(node, sourceCode, fixableArguments) { | ||
@@ -103,4 +68,4 @@ const array = node.callee.object; | ||
if ( | ||
!keepTrailingComma && | ||
isArrayLiteralHasTrailingComma(node, sourceCode) | ||
!keepTrailingComma | ||
&& isArrayLiteralHasTrailingComma(node, sourceCode) | ||
) { | ||
@@ -118,3 +83,3 @@ const start = node.range[0] + 1; | ||
.filter(({node, isArrayLiteral}) => (!isArrayLiteral || node.elements.length > 0)); | ||
const lastArgument = nonEmptyArguments[nonEmptyArguments.length - 1]; | ||
const lastArgument = nonEmptyArguments.at(-1); | ||
@@ -135,4 +100,4 @@ let text = nonEmptyArguments | ||
if ( | ||
!isParenthesized(node, sourceCode) && | ||
shouldAddParenthesesToSpreadElementArgument(node) | ||
!isParenthesized(node, sourceCode) | ||
&& shouldAddParenthesesToSpreadElementArgument(node) | ||
) { | ||
@@ -162,4 +127,4 @@ text = `(${text})`; | ||
if ( | ||
arrayHasTrailingComma && | ||
(!lastArgument.isArrayLiteral || !isArrayLiteralHasTrailingComma(lastArgument.node, sourceCode)) | ||
arrayHasTrailingComma | ||
&& (!lastArgument.isArrayLiteral || !isArrayLiteralHasTrailingComma(lastArgument.node, sourceCode)) | ||
) { | ||
@@ -193,4 +158,4 @@ text = `${text},`; | ||
if ( | ||
!arrayIsArrayLiteral && | ||
needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[') | ||
!arrayIsArrayLiteral | ||
&& needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[') | ||
) { | ||
@@ -200,7 +165,7 @@ yield fixer.insertTextBefore(node, ';'); | ||
yield ( | ||
concatCallArguments.length - fixableArguments.length === 0 ? | ||
fixer.replaceTextRange(getRangeAfterCalleeObject(node, sourceCode), '') : | ||
removeArguments(fixer) | ||
); | ||
if (concatCallArguments.length - fixableArguments.length === 0) { | ||
yield * removeMethodCall(fixer, node, sourceCode); | ||
} else { | ||
yield removeArguments(fixer); | ||
} | ||
@@ -269,4 +234,4 @@ const text = getFixedText(); | ||
if ( | ||
!isParenthesized(object, sourceCode) && | ||
shouldAddParenthesesToSpreadElementArgument(object) | ||
!isParenthesized(object, sourceCode) | ||
&& shouldAddParenthesesToSpreadElementArgument(object) | ||
) { | ||
@@ -279,9 +244,2 @@ text = `(${text})`; | ||
function * removeObject(fixer) { | ||
yield * replaceNodeOrTokenAndSpacesBefore(object, '', fixer, sourceCode); | ||
const commaToken = sourceCode.getTokenAfter(object, isCommaToken); | ||
yield * replaceNodeOrTokenAndSpacesBefore(commaToken, '', fixer, sourceCode); | ||
yield removeSpacesAfter(commaToken, sourceCode, fixer); | ||
} | ||
return function * (fixer) { | ||
@@ -295,15 +253,7 @@ // Fixed code always starts with `[` | ||
if (node.arguments.length === 1) { | ||
yield fixer.replaceText(node, objectText); | ||
return; | ||
} | ||
// `Array.from(object, mapFunction, thisArgument)` -> `[...object].map(mapFunction, thisArgument)` | ||
yield fixer.replaceText(node.callee.object, objectText); | ||
yield fixer.replaceText(node.callee.property, 'map'); | ||
yield * removeObject(fixer); | ||
yield fixer.replaceText(node, objectText); | ||
}; | ||
} | ||
function fixSlice(node, sourceCode) { | ||
function methodCallToSpread(node, sourceCode) { | ||
return function * (fixer) { | ||
@@ -318,115 +268,266 @@ // Fixed code always starts with `[` | ||
// The array is already accessing `.slice`, there should not any case need add extra `()` | ||
// The array is already accessing `.slice` or `.split`, there should not any case need add extra `()` | ||
yield fixer.replaceTextRange(getRangeAfterCalleeObject(node, sourceCode), ''); | ||
yield * removeMethodCall(fixer, node, sourceCode); | ||
}; | ||
} | ||
function isClassName(node) { | ||
if (node.type === 'MemberExpression') { | ||
node = node.property; | ||
} | ||
if (node.type !== 'Identifier') { | ||
return false; | ||
} | ||
const {name} = node; | ||
return /^[A-Z]./.test(name) && name.toUpperCase() !== name; | ||
} | ||
function isNotArray(node, scope) { | ||
if ( | ||
node.type === 'TemplateLiteral' | ||
|| node.type === 'Literal' | ||
|| node.type === 'BinaryExpression' | ||
|| isClassName(node) | ||
// `foo.join()` | ||
|| (isMethodNamed(node, 'join') && node.arguments.length <= 1) | ||
) { | ||
return true; | ||
} | ||
const staticValue = getStaticValue(node, scope); | ||
if (staticValue && !Array.isArray(staticValue.value)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[arrayFromCallSelector](node) { | ||
// `Array.from()` | ||
context.on('CallExpression', node => { | ||
if ( | ||
isMethodCall(node, { | ||
object: 'Array', | ||
method: 'from', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// Allow `Array.from({length})` | ||
&& node.arguments[0].type !== 'ObjectExpression' | ||
) { | ||
return { | ||
node, | ||
messageId: ERROR_ARRAY_FROM, | ||
fix: fixArrayFrom(node, sourceCode) | ||
fix: fixArrayFrom(node, sourceCode), | ||
}; | ||
}, | ||
[arrayConcatCallSelector](node) { | ||
const scope = context.getScope(); | ||
const staticResult = getStaticValue(node.callee.object, scope); | ||
} | ||
}); | ||
if (staticResult && !Array.isArray(staticResult.value)) { | ||
return; | ||
} | ||
// `array.concat()` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'concat', | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_CONCAT | ||
}; | ||
const {object} = node.callee; | ||
const scope = sourceCode.getScope(object); | ||
const fixableArguments = getConcatFixableArguments(node.arguments, scope); | ||
if (isNotArray(object, scope)) { | ||
return; | ||
} | ||
if (fixableArguments.length > 0 || node.arguments.length === 0) { | ||
problem.fix = fixConcat(node, sourceCode, fixableArguments); | ||
return problem; | ||
} | ||
const staticResult = getStaticValue(object, scope); | ||
if (staticResult && !Array.isArray(staticResult.value)) { | ||
return; | ||
} | ||
const [firstArgument, ...restArguments] = node.arguments; | ||
if (firstArgument.type === 'SpreadElement') { | ||
return problem; | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_CONCAT, | ||
}; | ||
const fixableArgumentsAfterFirstArgument = getConcatFixableArguments(restArguments, scope); | ||
const suggestions = [ | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE, | ||
isSpreadable: true | ||
}, | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE, | ||
isSpreadable: false | ||
} | ||
]; | ||
const fixableArguments = getConcatFixableArguments(node.arguments, scope); | ||
if (!hasSideEffect(firstArgument, sourceCode)) { | ||
suggestions.push({ | ||
messageId: SUGGESTION_CONCAT_TEST_ARGUMENT, | ||
testArgument: true | ||
}); | ||
} | ||
if (fixableArguments.length > 0 || node.arguments.length === 0) { | ||
problem.fix = fixConcat(node, sourceCode, fixableArguments); | ||
return problem; | ||
} | ||
problem.suggest = suggestions.map(({messageId, isSpreadable, testArgument}) => ({ | ||
messageId, | ||
const [firstArgument, ...restArguments] = node.arguments; | ||
if (firstArgument.type === 'SpreadElement') { | ||
return problem; | ||
} | ||
const fixableArgumentsAfterFirstArgument = getConcatFixableArguments(restArguments, scope); | ||
const suggestions = [ | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE, | ||
isSpreadable: true, | ||
}, | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE, | ||
isSpreadable: false, | ||
}, | ||
]; | ||
if (!hasSideEffect(firstArgument, sourceCode)) { | ||
suggestions.push({ | ||
messageId: SUGGESTION_CONCAT_TEST_ARGUMENT, | ||
testArgument: true, | ||
}); | ||
} | ||
problem.suggest = suggestions.map(({messageId, isSpreadable, testArgument}) => ({ | ||
messageId, | ||
fix: fixConcat( | ||
node, | ||
sourceCode, | ||
// When apply suggestion, we also merge fixable arguments after the first one | ||
[ | ||
{ | ||
node: firstArgument, | ||
isSpreadable, | ||
testArgument, | ||
}, | ||
...fixableArgumentsAfterFirstArgument, | ||
], | ||
), | ||
})); | ||
if ( | ||
fixableArgumentsAfterFirstArgument.length < restArguments.length | ||
&& restArguments.every(({type}) => type !== 'SpreadElement') | ||
) { | ||
problem.suggest.push({ | ||
messageId: SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS, | ||
fix: fixConcat( | ||
node, | ||
sourceCode, | ||
// When apply suggestion, we also merge fixable arguments after the first one | ||
[ | ||
{ | ||
node: firstArgument, | ||
isSpreadable, | ||
testArgument | ||
}, | ||
...fixableArgumentsAfterFirstArgument | ||
] | ||
) | ||
})); | ||
node.arguments.map(node => getConcatArgumentSpreadable(node, scope) || {node, isSpreadable: true}), | ||
), | ||
}); | ||
} | ||
if ( | ||
fixableArgumentsAfterFirstArgument.length < restArguments.length && | ||
restArguments.every(({type}) => type !== 'SpreadElement') | ||
) { | ||
problem.suggest.push({ | ||
messageId: SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS, | ||
fix: fixConcat( | ||
node, | ||
sourceCode, | ||
node.arguments.map(node => getConcatArgumentSpreadable(node, scope) || {node, isSpreadable: true}) | ||
) | ||
}); | ||
} | ||
return problem; | ||
}); | ||
return problem; | ||
}, | ||
[arraySliceCallSelector](node) { | ||
if (isNodeMatches(node.callee.object, ignoredSliceCallee)) { | ||
return; | ||
} | ||
// `array.slice()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'slice', | ||
minimumArguments: 0, | ||
maximumArguments: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.callee.object.type !== 'ArrayExpression' | ||
)) { | ||
return; | ||
} | ||
const [firstArgument] = node.arguments; | ||
if (firstArgument && !isLiteralValue(firstArgument, 0)) { | ||
if (isNodeMatches(node.callee.object, ignoredSliceCallee)) { | ||
return; | ||
} | ||
const [firstArgument] = node.arguments; | ||
if (firstArgument && !isLiteral(firstArgument, 0)) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_SLICE, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}; | ||
}); | ||
// `array.toSpliced()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'toSpliced', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.callee.object.type !== 'ArrayExpression' | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_TO_SPLICED, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}; | ||
}); | ||
// `string.split()` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'split', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const [separator] = node.arguments; | ||
if (!isLiteral(separator, '')) { | ||
return; | ||
} | ||
const string = node.callee.object; | ||
const staticValue = getStaticValue(string, sourceCode.getScope(string)); | ||
let hasSameResult = false; | ||
if (staticValue) { | ||
const {value} = staticValue; | ||
if (typeof value !== 'string') { | ||
return; | ||
} | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_SLICE, | ||
fix: fixSlice(node, sourceCode) | ||
}; | ||
// eslint-disable-next-line unicorn/prefer-spread | ||
const resultBySplit = value.split(''); | ||
const resultBySpread = [...value]; | ||
hasSameResult = resultBySplit.length === resultBySpread.length | ||
&& resultBySplit.every((character, index) => character === resultBySpread[index]); | ||
} | ||
}; | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_STRING_SPLIT, | ||
}; | ||
if (hasSameResult) { | ||
problem.fix = methodCallToSpread(node, sourceCode); | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_USE_SPREAD, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}, | ||
]; | ||
} | ||
return problem; | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -437,8 +538,8 @@ create, | ||
docs: { | ||
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`.' | ||
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split(\'\')`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const quoteString = require('./utils/quote-string.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {parse: parseRegExp} = require('regjsparser'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const {isRegexLiteral, isNewExpression, isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-string-replace-all'; | ||
const MESSAGE_ID_USE_REPLACE_ALL = 'method'; | ||
const MESSAGE_ID_USE_STRING = 'pattern'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `String#replaceAll()` over `String#replace()`.' | ||
[MESSAGE_ID_USE_REPLACE_ALL]: 'Prefer `String#replaceAll()` over `String#replace()`.', | ||
[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.', | ||
}; | ||
const selector = methodCallSelector({ | ||
name: 'replace', | ||
length: 2 | ||
}); | ||
function getPatternReplacement(node) { | ||
if (!isRegexLiteral(node)) { | ||
return; | ||
} | ||
function isRegexWithGlobalFlag(node) { | ||
const {type, regex} = node; | ||
if (type !== 'Literal' || !regex) { | ||
return false; | ||
const {pattern, flags} = node.regex; | ||
if (flags.replace('u', '').replace('v', '') !== 'g') { | ||
return; | ||
} | ||
const {flags} = regex; | ||
return flags.replace('u', '') === 'g'; | ||
} | ||
let tree; | ||
function isLiteralCharactersOnly(node) { | ||
const searchPattern = node.regex.pattern; | ||
return !/[$()*+.?[\\\]^{}]/.test(searchPattern.replace(/\\[$()*+.?[\\\]^{}]/g, '')); | ||
try { | ||
tree = parseRegExp(pattern, flags, { | ||
unicodePropertyEscape: flags.includes('u'), | ||
unicodeSet: flags.includes('v'), | ||
namedGroups: true, | ||
lookbehind: true, | ||
}); | ||
} catch { | ||
return; | ||
} | ||
const parts = tree.type === 'alternative' ? tree.body : [tree]; | ||
if (parts.some(part => part.type !== 'value')) { | ||
return; | ||
} | ||
// TODO: Preserve escape | ||
const string = String.fromCodePoint(...parts.map(part => part.codePoint)); | ||
return escapeString(string); | ||
} | ||
function removeEscapeCharacters(regexString) { | ||
let fixedString = regexString; | ||
let index = 0; | ||
do { | ||
index = fixedString.indexOf('\\', index); | ||
const isRegExpWithGlobalFlag = (node, scope) => { | ||
if (isRegexLiteral(node)) { | ||
return node.regex.flags.includes('g'); | ||
} | ||
if (index >= 0) { | ||
fixedString = fixedString.slice(0, index) + fixedString.slice(index + 1); | ||
index++; | ||
if ( | ||
isNewExpression(node, {name: 'RegExp'}) | ||
&& node.arguments[0]?.type !== 'SpreadElement' | ||
&& node.arguments[1]?.type === 'Literal' | ||
&& typeof node.arguments[1].value === 'string' | ||
) { | ||
return node.arguments[1].value.includes('g'); | ||
} | ||
const staticResult = getStaticValue(node, scope); | ||
// Don't know if there is `g` flag | ||
if (!staticResult) { | ||
return false; | ||
} | ||
const {value} = staticResult; | ||
return ( | ||
Object.prototype.toString.call(value) === '[object RegExp]' | ||
&& value.global | ||
); | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if (!isMethodCall(node, { | ||
methods: ['replace', 'replaceAll'], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
} while (index >= 0); | ||
return fixedString; | ||
} | ||
const { | ||
arguments: [pattern], | ||
callee: {property}, | ||
} = node; | ||
const create = () => { | ||
return { | ||
[selector]: node => { | ||
const {arguments: arguments_, callee} = node; | ||
const [search] = arguments_; | ||
if (!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))) { | ||
return; | ||
} | ||
if (!isRegexWithGlobalFlag(search) || !isLiteralCharactersOnly(search)) { | ||
const methodName = property.name; | ||
const patternReplacement = getPatternReplacement(pattern); | ||
if (methodName === 'replaceAll') { | ||
if (!patternReplacement) { | ||
return; | ||
@@ -56,13 +106,31 @@ } | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => [ | ||
fixer.insertTextAfter(callee, 'All'), | ||
fixer.replaceText(search, quoteString(removeEscapeCharacters(search.regex.pattern))) | ||
] | ||
node: pattern, | ||
messageId: MESSAGE_ID_USE_STRING, | ||
data: { | ||
// Show `This pattern can be replaced with a string literal.` for long strings | ||
replacement: patternReplacement.length < 20 ? patternReplacement : 'a string literal', | ||
}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => fixer.replaceText(pattern, patternReplacement), | ||
}; | ||
} | ||
}; | ||
}; | ||
return { | ||
node: property, | ||
messageId: MESSAGE_ID_USE_REPLACE_ALL, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
yield fixer.insertTextAfter(property, 'All'); | ||
if (!patternReplacement) { | ||
return; | ||
} | ||
yield fixer.replaceText(pattern, patternReplacement); | ||
}, | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -73,7 +141,7 @@ create, | ||
docs: { | ||
description: 'Prefer `String#replaceAll()` over regex searches with the global flag.' | ||
description: 'Prefer `String#replaceAll()` over regex searches with the global flag.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const eslintTemplateVisitor = require('eslint-template-visitor'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const isNumber = require('./utils/is-number.js'); | ||
const {replaceArgument} = require('./fix/index.js'); | ||
const {isNumberLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -8,17 +12,7 @@ const MESSAGE_ID_SUBSTR = 'substr'; | ||
[MESSAGE_ID_SUBSTR]: 'Prefer `String#slice()` over `String#substr()`.', | ||
[MESSAGE_ID_SUBSTRING]: 'Prefer `String#slice()` over `String#substring()`.' | ||
[MESSAGE_ID_SUBSTRING]: 'Prefer `String#slice()` over `String#substring()`.', | ||
}; | ||
const templates = eslintTemplateVisitor(); | ||
const objectVariable = templates.variable(); | ||
const argumentsVariable = templates.spreadVariable(); | ||
const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`; | ||
const substringCallTemplate = templates.template`${objectVariable}.substring(${argumentsVariable})`; | ||
const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number'; | ||
const getNumericValue = node => { | ||
if (isLiteralNumber(node)) { | ||
if (isNumberLiteral(node)) { | ||
return node.value; | ||
@@ -34,165 +28,147 @@ } | ||
const isLengthProperty = node => ( | ||
node && | ||
node.type === 'MemberExpression' && | ||
node.computed === false && | ||
node.property.type === 'Identifier' && | ||
node.property.name === 'length' | ||
node?.type === 'MemberExpression' | ||
&& node.computed === false | ||
&& node.property.type === 'Identifier' | ||
&& node.property.name === 'length' | ||
); | ||
const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node); | ||
function * fixSubstrArguments({node, fixer, context, abort}) { | ||
const argumentNodes = node.arguments; | ||
const [firstArgument, secondArgument] = argumentNodes; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
if (!secondArgument) { | ||
return; | ||
} | ||
const getNodeText = node => { | ||
const text = sourceCode.getText(node); | ||
const before = sourceCode.getTokenBefore(node); | ||
const after = sourceCode.getTokenAfter(node); | ||
if ( | ||
(before && before.type === 'Punctuator' && before.value === '(') && | ||
(after && after.type === 'Punctuator' && after.value === ')') | ||
) { | ||
return `(${text})`; | ||
const {sourceCode} = context; | ||
const scope = sourceCode.getScope(node); | ||
const firstArgumentStaticResult = getStaticValue(firstArgument, scope); | ||
const secondArgumentRange = getParenthesizedRange(secondArgument, sourceCode); | ||
const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode); | ||
if (firstArgumentStaticResult?.value === 0) { | ||
if (isNumberLiteral(secondArgument) || isLengthProperty(secondArgument)) { | ||
return; | ||
} | ||
return text; | ||
}; | ||
if (typeof getNumericValue(secondArgument) === 'number') { | ||
yield replaceSecondArgument(Math.max(0, getNumericValue(secondArgument))); | ||
return; | ||
} | ||
return templates.visitor({ | ||
[substrCallTemplate](node) { | ||
const objectNode = substrCallTemplate.context.getMatch(objectVariable); | ||
const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable); | ||
yield fixer.insertTextBeforeRange(secondArgumentRange, 'Math.max(0, '); | ||
yield fixer.insertTextAfterRange(secondArgumentRange, ')'); | ||
return; | ||
} | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_SUBSTR | ||
}; | ||
if (argumentNodes.every(node => isNumberLiteral(node))) { | ||
yield replaceSecondArgument(firstArgument.value + secondArgument.value); | ||
return; | ||
} | ||
const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined; | ||
const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined; | ||
if (argumentNodes.every(node => isNumber(node, scope))) { | ||
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); | ||
let sliceArguments; | ||
yield fixer.insertTextBeforeRange(secondArgumentRange, `${firstArgumentText} + `); | ||
return; | ||
} | ||
switch (argumentNodes.length) { | ||
case 0: { | ||
sliceArguments = []; | ||
break; | ||
} | ||
return abort(); | ||
} | ||
case 1: { | ||
sliceArguments = [firstArgument]; | ||
break; | ||
} | ||
function * fixSubstringArguments({node, fixer, context, abort}) { | ||
const {sourceCode} = context; | ||
const [firstArgument, secondArgument] = node.arguments; | ||
case 2: { | ||
if (firstArgument === '0') { | ||
sliceArguments = [firstArgument]; | ||
if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) { | ||
sliceArguments.push(secondArgument); | ||
} else if (typeof getNumericValue(argumentNodes[1]) === 'number') { | ||
sliceArguments.push(Math.max(0, getNumericValue(argumentNodes[1]))); | ||
} else { | ||
sliceArguments.push(`Math.max(0, ${secondArgument})`); | ||
} | ||
} else if ( | ||
isLiteralNumber(argumentNodes[0]) && | ||
isLiteralNumber(argumentNodes[1]) | ||
) { | ||
sliceArguments = [ | ||
firstArgument, | ||
argumentNodes[0].value + argumentNodes[1].value | ||
]; | ||
} else if ( | ||
isLikelyNumeric(argumentNodes[0]) && | ||
isLikelyNumeric(argumentNodes[1]) | ||
) { | ||
sliceArguments = [firstArgument, firstArgument + ' + ' + secondArgument]; | ||
} | ||
const firstNumber = firstArgument ? getNumericValue(firstArgument) : undefined; | ||
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); | ||
const replaceFirstArgument = text => replaceArgument(fixer, firstArgument, text, sourceCode); | ||
break; | ||
} | ||
// No default | ||
} | ||
if (!secondArgument) { | ||
if (isLengthProperty(firstArgument)) { | ||
return; | ||
} | ||
if (sliceArguments) { | ||
const objectText = getNodeText(objectNode); | ||
const optionalMemberSuffix = node.callee.optional ? '?' : ''; | ||
const optionalCallSuffix = node.optional ? '?.' : ''; | ||
if (firstNumber !== undefined) { | ||
yield replaceFirstArgument(Math.max(0, firstNumber)); | ||
return; | ||
} | ||
problem.fix = fixer => fixer.replaceText(node, `${objectText}${optionalMemberSuffix}.slice${optionalCallSuffix}(${sliceArguments.join(', ')})`); | ||
} | ||
const firstArgumentRange = getParenthesizedRange(firstArgument, sourceCode); | ||
yield fixer.insertTextBeforeRange(firstArgumentRange, 'Math.max(0, '); | ||
yield fixer.insertTextAfterRange(firstArgumentRange, ')'); | ||
return; | ||
} | ||
context.report(problem); | ||
}, | ||
const secondNumber = getNumericValue(secondArgument); | ||
const secondArgumentText = getParenthesizedText(secondArgument, sourceCode); | ||
const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode); | ||
[substringCallTemplate](node) { | ||
const objectNode = substringCallTemplate.context.getMatch(objectVariable); | ||
const argumentNodes = substringCallTemplate.context.getMatch(argumentsVariable); | ||
if (firstNumber !== undefined && secondNumber !== undefined) { | ||
const argumentsValue = [Math.max(0, firstNumber), Math.max(0, secondNumber)]; | ||
if (firstNumber > secondNumber) { | ||
argumentsValue.reverse(); | ||
} | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_SUBSTRING | ||
}; | ||
if (argumentsValue[0] !== firstNumber) { | ||
yield replaceFirstArgument(argumentsValue[0]); | ||
} | ||
const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined; | ||
const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined; | ||
if (argumentsValue[1] !== secondNumber) { | ||
yield replaceSecondArgument(argumentsValue[1]); | ||
} | ||
const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined; | ||
return; | ||
} | ||
let sliceArguments; | ||
if (firstNumber === 0 || secondNumber === 0) { | ||
yield replaceFirstArgument(0); | ||
yield replaceSecondArgument(`Math.max(0, ${firstNumber === 0 ? secondArgumentText : firstArgumentText})`); | ||
return; | ||
} | ||
switch (argumentNodes.length) { | ||
case 0: { | ||
sliceArguments = []; | ||
break; | ||
} | ||
// As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue: | ||
// .substring(0, 2) and .substring(2, 0) returns the same result | ||
// .slice(0, 2) and .slice(2, 0) doesn't return the same result | ||
// There's also an issue with us now knowing whether the value will be negative or not, due to: | ||
// .substring() treats a negative number the same as it treats a zero. | ||
// The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice | ||
case 1: { | ||
if (firstNumber !== undefined) { | ||
sliceArguments = [Math.max(0, firstNumber)]; | ||
} else if (isLengthProperty(argumentNodes[0])) { | ||
sliceArguments = [firstArgument]; | ||
} else { | ||
sliceArguments = [`Math.max(0, ${firstArgument})`]; | ||
} | ||
return abort(); | ||
} | ||
break; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if (!isMethodCall(node, {methods: ['substr', 'substring']})) { | ||
return; | ||
} | ||
case 2: { | ||
const secondNumber = getNumericValue(argumentNodes[1]); | ||
const method = node.callee.property.name; | ||
if (firstNumber !== undefined && secondNumber !== undefined) { | ||
sliceArguments = firstNumber > secondNumber ? | ||
[Math.max(0, secondNumber), Math.max(0, firstNumber)] : | ||
[Math.max(0, firstNumber), Math.max(0, secondNumber)]; | ||
} else if (firstNumber === 0 || secondNumber === 0) { | ||
sliceArguments = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`]; | ||
} else { | ||
// As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue: | ||
// .substring(0, 2) and .substring(2, 0) returns the same result | ||
// .slice(0, 2) and .slice(2, 0) doesn't return the same result | ||
// There's also an issue with us now knowing whether the value will be negative or not, due to: | ||
// .substring() treats a negative number the same as it treats a zero. | ||
// The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice | ||
} | ||
return { | ||
node, | ||
messageId: method, | ||
* fix(fixer, {abort}) { | ||
yield fixer.replaceText(node.callee.property, 'slice'); | ||
break; | ||
if (node.arguments.length === 0) { | ||
return; | ||
} | ||
// No default | ||
} | ||
if (sliceArguments) { | ||
const objectText = getNodeText(objectNode); | ||
const optionalMemberSuffix = node.callee.optional ? '?' : ''; | ||
const optionalCallSuffix = node.optional ? '?.' : ''; | ||
if ( | ||
node.arguments.length > 2 | ||
|| node.arguments.some(node => node.type === 'SpreadElement') | ||
) { | ||
return abort(); | ||
} | ||
problem.fix = fixer => fixer.replaceText(node, `${objectText}${optionalMemberSuffix}.slice${optionalCallSuffix}(${sliceArguments.join(', ')})`); | ||
} | ||
const fixArguments = method === 'substr' ? fixSubstrArguments : fixSubstringArguments; | ||
yield * fixArguments({node, fixer, context, abort}); | ||
}, | ||
}; | ||
}, | ||
}); | ||
context.report(problem); | ||
} | ||
}); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -203,7 +179,7 @@ create, | ||
docs: { | ||
description: 'Prefer `String#slice()` over `String#substr()` and `String#substring()`.' | ||
description: 'Prefer `String#slice()` over `String#substr()` and `String#substring()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const quoteString = require('./utils/quote-string.js'); | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const shouldAddParenthesesToLogicalExpressionChild = require('./utils/should-add-parentheses-to-logical-expression-child.js'); | ||
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {isMethodCall, isRegexLiteral} = require('./ast/index.js'); | ||
@@ -19,3 +19,3 @@ const MESSAGE_STARTS_WITH = 'prefer-starts-with'; | ||
[FIX_TYPE_OPTIONAL_CHAINING]: 'Use optional chaining `…?.{{method}}()`.', | ||
[FIX_TYPE_NULLISH_COALESCING]: 'Use nullish coalescing `(… ?? \'\').{{method}}()`.' | ||
[FIX_TYPE_NULLISH_COALESCING]: 'Use nullish coalescing `(… ?? \'\').{{method}}()`.', | ||
}; | ||
@@ -26,11 +26,6 @@ | ||
string, | ||
['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*', '|'] | ||
['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*', '|'], | ||
); | ||
const addParentheses = text => `(${text})`; | ||
const regexTestSelector = [ | ||
methodCallSelector({name: 'test', length: 1}), | ||
'[callee.object.regex]' | ||
].join(''); | ||
const checkRegex = ({pattern, flags}) => { | ||
@@ -47,3 +42,3 @@ if (flags.includes('i') || flags.includes('m')) { | ||
messageId: MESSAGE_STARTS_WITH, | ||
string | ||
string, | ||
}; | ||
@@ -59,3 +54,3 @@ } | ||
messageId: MESSAGE_ENDS_WITH, | ||
string | ||
string, | ||
}; | ||
@@ -66,7 +61,20 @@ } | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[regexTestSelector](node) { | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'test', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| !isRegexLiteral(node.callee.object) | ||
) { | ||
return; | ||
} | ||
const regexNode = node.callee.object; | ||
@@ -82,11 +90,11 @@ const {regex} = regexNode; | ||
let isString = target.type === 'TemplateLiteral' || | ||
( | ||
target.type === 'CallExpression' && | ||
target.callee.type === 'Identifier' && | ||
target.callee.name === 'String' | ||
let isString = target.type === 'TemplateLiteral' | ||
|| ( | ||
target.type === 'CallExpression' | ||
&& target.callee.type === 'Identifier' | ||
&& target.callee.name === 'String' | ||
); | ||
let isNonString = false; | ||
if (!isString) { | ||
const staticValue = getStaticValue(target, context.getScope()); | ||
const staticValue = getStaticValue(target, sourceCode.getScope(target)); | ||
@@ -101,3 +109,3 @@ if (staticValue) { | ||
node, | ||
messageId: result.messageId | ||
messageId: result.messageId, | ||
}; | ||
@@ -112,6 +120,6 @@ | ||
// Goal: `(target ?? '').startsWith(pattern)` | ||
case FIX_TYPE_NULLISH_COALESCING: | ||
case FIX_TYPE_NULLISH_COALESCING: { | ||
if ( | ||
!isTargetParenthesized && | ||
shouldAddParenthesesToLogicalExpressionChild(target, {operator: '??', property: 'left'}) | ||
!isTargetParenthesized | ||
&& shouldAddParenthesesToLogicalExpressionChild(target, {operator: '??', property: 'left'}) | ||
) { | ||
@@ -130,5 +138,6 @@ targetText = addParentheses(targetText); | ||
break; | ||
} | ||
// Goal: `String(target).startsWith(pattern)` | ||
case FIX_TYPE_STRING_CASTING: | ||
case FIX_TYPE_STRING_CASTING: { | ||
// `target` was a call argument, don't need check parentheses | ||
@@ -138,16 +147,20 @@ targetText = `String(${targetText})`; | ||
break; | ||
} | ||
// Goal: `target.startsWith(pattern)` or `target?.startsWith(pattern)` | ||
case FIX_TYPE_OPTIONAL_CHAINING: | ||
case FIX_TYPE_OPTIONAL_CHAINING: { | ||
// Optional chaining: `target.startsWith` => `target?.startsWith` | ||
yield fixer.replaceText(sourceCode.getTokenBefore(node.callee.property), '?.'); | ||
// Fallthrough | ||
default: | ||
} | ||
// Fallthrough | ||
default: { | ||
if ( | ||
!isRegexParenthesized && | ||
!isTargetParenthesized && | ||
shouldAddParenthesesToMemberExpressionObject(target, sourceCode) | ||
!isRegexParenthesized | ||
&& !isTargetParenthesized | ||
&& shouldAddParenthesesToMemberExpressionObject(target, sourceCode) | ||
) { | ||
targetText = addParentheses(targetText); | ||
} | ||
} | ||
} | ||
@@ -164,3 +177,3 @@ | ||
// Replace argument with result.string | ||
yield fixer.replaceTextRange(getParenthesizedRange(target, sourceCode), quoteString(result.string)); | ||
yield fixer.replaceTextRange(getParenthesizedRange(target, sourceCode), escapeString(result.string)); | ||
} | ||
@@ -176,3 +189,3 @@ | ||
FIX_TYPE_OPTIONAL_CHAINING, | ||
FIX_TYPE_NULLISH_COALESCING | ||
FIX_TYPE_NULLISH_COALESCING, | ||
].map(type => ({messageId: type, data: {method}, fix: fixer => fix(fixer, type)})); | ||
@@ -182,6 +195,7 @@ } | ||
return problem; | ||
} | ||
}, | ||
}; | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -192,8 +206,8 @@ create, | ||
docs: { | ||
description: 'Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`.' | ||
description: 'Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-string-trim-start-end'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer `String#{{replacement}}()` over `String#{{method}}()`.' | ||
[MESSAGE_ID]: 'Prefer `String#{{replacement}}()` over `String#{{method}}()`.', | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
names: ['trimLeft', 'trimRight'], | ||
length: 0 | ||
}), | ||
' > .callee', | ||
' > .property' | ||
].join(' '); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
methods: ['trimLeft', 'trimRight'], | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
})) { | ||
return; | ||
} | ||
const create = () => { | ||
return { | ||
[selector](node) { | ||
const method = node.name; | ||
const replacement = method === 'trimLeft' ? 'trimStart' : 'trimEnd'; | ||
const node = callExpression.callee.property; | ||
const method = node.name; | ||
const replacement = method === 'trimLeft' ? 'trimStart' : 'trimEnd'; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {method, replacement}, | ||
fix: fixer => fixer.replaceText(node, replacement) | ||
}; | ||
} | ||
}; | ||
}; | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {method, replacement}, | ||
fix: fixer => fixer.replaceText(node, replacement), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -39,7 +39,7 @@ create, | ||
docs: { | ||
description: 'Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`.' | ||
description: 'Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {hasSideEffect} = require('eslint-utils'); | ||
const {hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const isSameReference = require('./utils/is-same-reference.js'); | ||
@@ -8,3 +8,3 @@ const getIndentString = require('./utils/get-indent-string.js'); | ||
const messages = { | ||
[MESSAGE_ID]: 'Use `switch` instead of multiple `else-if`.' | ||
[MESSAGE_ID]: 'Use `switch` instead of multiple `else-if`.', | ||
}; | ||
@@ -65,3 +65,3 @@ | ||
compareExpressions, | ||
discriminantCandidates | ||
discriminantCandidates, | ||
); | ||
@@ -77,3 +77,3 @@ | ||
statement, | ||
compareExpressions | ||
compareExpressions, | ||
}); | ||
@@ -84,3 +84,3 @@ } | ||
ifStatements, | ||
discriminant: discriminantCandidates && discriminantCandidates[0] | ||
discriminant: discriminantCandidates && discriminantCandidates[0], | ||
}; | ||
@@ -95,3 +95,3 @@ } | ||
'ForInStatement', | ||
'SwitchStatement' | ||
'SwitchStatement', | ||
]); | ||
@@ -170,9 +170,11 @@ const getBreakTarget = node => { | ||
case 'ReturnStatement': | ||
case 'ThrowStatement': | ||
case 'ThrowStatement': { | ||
return false; | ||
} | ||
case 'IfStatement': | ||
return !node.alternate || | ||
shouldInsertBreakStatement(node.consequent) || | ||
shouldInsertBreakStatement(node.alternate); | ||
case 'IfStatement': { | ||
return !node.alternate | ||
|| shouldInsertBreakStatement(node.consequent) | ||
|| shouldInsertBreakStatement(node.alternate); | ||
} | ||
@@ -184,4 +186,5 @@ case 'BlockStatement': { | ||
default: | ||
default: { | ||
return true; | ||
} | ||
} | ||
@@ -198,3 +201,3 @@ } | ||
const lastStatement = ifStatements[ifStatements.length - 1].statement; | ||
const lastStatement = ifStatements.at(-1).statement; | ||
if (lastStatement.alternate) { | ||
@@ -216,5 +219,7 @@ const {alternate} = lastStatement; | ||
switch (options.emptyDefaultCase) { | ||
case 'no-default-comment': | ||
case 'no-default-comment': { | ||
yield fixer.insertTextAfter(firstStatement, `\n${indent}// No default`); | ||
break; | ||
} | ||
case 'do-nothing-comment': { | ||
@@ -255,2 +260,3 @@ yield fixer.insertTextAfter(firstStatement, `\n${indent}default:\n${indent}// Do nothing`); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
@@ -261,5 +267,5 @@ const options = { | ||
insertBreakInDefaultCase: false, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const ifStatements = new Set(); | ||
@@ -270,7 +276,9 @@ const breakStatements = []; | ||
return { | ||
'IfStatement'(node) { | ||
IfStatement(node) { | ||
ifStatements.add(node); | ||
}, | ||
'BreakStatement:not([label])'(node) { | ||
breakStatements.push(node); | ||
BreakStatement(node) { | ||
if (!node.label) { | ||
breakStatements.push(node); | ||
} | ||
}, | ||
@@ -296,10 +304,10 @@ * 'Program:exit'() { | ||
start: node.loc.start, | ||
end: node.consequent.loc.start | ||
end: node.consequent.loc.start, | ||
}, | ||
messageId: MESSAGE_ID | ||
messageId: MESSAGE_ID, | ||
}; | ||
if ( | ||
!hasSideEffect(discriminant, sourceCode) && | ||
!ifStatements.some(({statement}) => hasBreakInside(breakStatements, statement)) | ||
!hasSideEffect(discriminant, sourceCode) | ||
&& !ifStatements.some(({statement}) => hasBreakInside(breakStatements, statement)) | ||
) { | ||
@@ -311,3 +319,3 @@ problem.fix = fix({discriminant, ifStatements}, sourceCode, options); | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -319,2 +327,3 @@ }; | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
@@ -324,3 +333,3 @@ minimumCases: { | ||
minimum: 2, | ||
default: 3 | ||
default: 3, | ||
}, | ||
@@ -331,11 +340,11 @@ emptyDefaultCase: { | ||
'do-nothing-comment', | ||
'no-default-case' | ||
'no-default-case', | ||
], | ||
default: 'no-default-comment' | ||
} | ||
default: 'no-default-comment', | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -346,8 +355,8 @@ create, | ||
docs: { | ||
description: 'Prefer `switch` over multiple `else-if`.' | ||
description: 'Prefer `switch` over multiple `else-if`.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const extendFixRange = require('./utils/extend-fix-range.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
@@ -10,17 +9,11 @@ const isSameReference = require('./utils/is-same-reference.js'); | ||
const shouldAddParenthesesToConditionalExpressionChild = require('./utils/should-add-parentheses-to-conditional-expression-child.js'); | ||
const {extendFixRange} = require('./fix/index.js'); | ||
const getScopes = require('./utils/get-scopes.js'); | ||
const messageId = 'prefer-ternary'; | ||
const selector = [ | ||
'IfStatement', | ||
':not(IfStatement > .alternate)', | ||
'[test.type!="ConditionalExpression"]', | ||
'[consequent]', | ||
'[alternate]' | ||
].join(''); | ||
const isTernary = node => node?.type === 'ConditionalExpression'; | ||
const isTernary = node => node && node.type === 'ConditionalExpression'; | ||
function getNodeBody(node) { | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (!node) { | ||
@@ -44,12 +37,8 @@ return; | ||
const getScopes = scope => [ | ||
scope, | ||
...scope.childScopes.flatMap(scope => getScopes(scope)) | ||
]; | ||
const isSingleLineNode = node => node.loc.start.line === node.loc.end.line; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const onlySingleLine = context.options[0] === 'only-single-line'; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const scopeToNamesGeneratedByFixer = new WeakMap(); | ||
@@ -64,4 +53,4 @@ const isSafeName = (name, scopes) => scopes.every(scope => { | ||
if ( | ||
!isParenthesized(node, sourceCode) && | ||
shouldAddParenthesesToConditionalExpressionChild(node) | ||
!isParenthesized(node, sourceCode) | ||
&& shouldAddParenthesesToConditionalExpressionChild(node) | ||
) { | ||
@@ -80,3 +69,3 @@ text = `(${text})`; | ||
alternate, | ||
node | ||
node, | ||
} = options; | ||
@@ -86,7 +75,7 @@ | ||
checkThrowStatement, | ||
returnFalseIfNotMergeable | ||
returnFalseIfNotMergeable, | ||
} = { | ||
checkThrowStatement: false, | ||
returnFalseIfNotMergeable: false, | ||
...mergeOptions | ||
...mergeOptions, | ||
}; | ||
@@ -101,5 +90,5 @@ | ||
if ( | ||
type === 'ReturnStatement' && | ||
!isTernary(argument) && | ||
!isTernary(alternate.argument) | ||
type === 'ReturnStatement' | ||
&& !isTernary(argument) | ||
&& !isTernary(alternate.argument) | ||
) { | ||
@@ -111,3 +100,3 @@ return merge({ | ||
alternate: alternate.argument === null ? 'undefined' : alternate.argument, | ||
node | ||
node, | ||
}); | ||
@@ -117,6 +106,6 @@ } | ||
if ( | ||
type === 'YieldExpression' && | ||
delegate === alternate.delegate && | ||
!isTernary(argument) && | ||
!isTernary(alternate.argument) | ||
type === 'YieldExpression' | ||
&& delegate === alternate.delegate | ||
&& !isTernary(argument) | ||
&& !isTernary(alternate.argument) | ||
) { | ||
@@ -128,3 +117,3 @@ return merge({ | ||
alternate: alternate.argument === null ? 'undefined' : alternate.argument, | ||
node | ||
node, | ||
}); | ||
@@ -134,5 +123,5 @@ } | ||
if ( | ||
type === 'AwaitExpression' && | ||
!isTernary(argument) && | ||
!isTernary(alternate.argument) | ||
type === 'AwaitExpression' | ||
&& !isTernary(argument) | ||
&& !isTernary(alternate.argument) | ||
) { | ||
@@ -144,3 +133,3 @@ return merge({ | ||
alternate: alternate.argument, | ||
node | ||
node, | ||
}); | ||
@@ -150,6 +139,6 @@ } | ||
if ( | ||
checkThrowStatement && | ||
type === 'ThrowStatement' && | ||
!isTernary(argument) && | ||
!isTernary(alternate.argument) | ||
checkThrowStatement | ||
&& type === 'ThrowStatement' | ||
&& !isTernary(argument) | ||
&& !isTernary(alternate.argument) | ||
) { | ||
@@ -166,3 +155,3 @@ // `ThrowStatement` don't check nested | ||
consequent: argument, | ||
alternate: alternate.argument | ||
alternate: alternate.argument, | ||
}; | ||
@@ -172,9 +161,9 @@ } | ||
if ( | ||
type === 'AssignmentExpression' && | ||
operator === alternate.operator && | ||
!isTernary(left) && | ||
!isTernary(alternate.left) && | ||
!isTernary(right) && | ||
!isTernary(alternate.right) && | ||
isSameReference(left, alternate.left) | ||
type === 'AssignmentExpression' | ||
&& operator === alternate.operator | ||
&& !isTernary(left) | ||
&& !isTernary(alternate.left) | ||
&& !isTernary(right) | ||
&& !isTernary(alternate.right) | ||
&& isSameReference(left, alternate.left) | ||
) { | ||
@@ -186,3 +175,3 @@ return merge({ | ||
alternate: alternate.right, | ||
node | ||
node, | ||
}); | ||
@@ -195,3 +184,12 @@ } | ||
return { | ||
[selector](node) { | ||
IfStatement(node) { | ||
if ( | ||
(node.parent.type === 'IfStatement' && node.parent.alternate === node) | ||
|| node.test.type === 'ConditionalExpression' | ||
|| !node.consequent | ||
|| !node.alternate | ||
) { | ||
return; | ||
} | ||
const consequent = getNodeBody(node.consequent); | ||
@@ -201,4 +199,4 @@ const alternate = getNodeBody(node.alternate); | ||
if ( | ||
onlySingleLine && | ||
[consequent, alternate, node.test].some(node => !isSingleLineNode(node)) | ||
onlySingleLine | ||
&& [consequent, alternate, node.test].some(node => !isSingleLineNode(node)) | ||
) { | ||
@@ -210,3 +208,3 @@ return; | ||
checkThrowStatement: true, | ||
returnFalseIfNotMergeable: true | ||
returnFalseIfNotMergeable: true, | ||
}); | ||
@@ -218,58 +216,62 @@ | ||
const scope = context.getScope(); | ||
const problem = {node, messageId}; | ||
return { | ||
node, | ||
messageId, | ||
* fix(fixer) { | ||
const testText = getText(node.test); | ||
const consequentText = typeof result.consequent === 'string' ? | ||
result.consequent : | ||
getText(result.consequent); | ||
const alternateText = typeof result.alternate === 'string' ? | ||
result.alternate : | ||
getText(result.alternate); | ||
// Don't fix if there are comments | ||
if (sourceCode.getCommentsInside(node).length > 0) { | ||
return problem; | ||
} | ||
let {type, before, after} = result; | ||
const scope = sourceCode.getScope(node); | ||
problem.fix = function * (fixer) { | ||
const testText = getText(node.test); | ||
const consequentText = typeof result.consequent === 'string' | ||
? result.consequent | ||
: getText(result.consequent); | ||
const alternateText = typeof result.alternate === 'string' | ||
? result.alternate | ||
: getText(result.alternate); | ||
let generateNewVariables = false; | ||
if (type === 'ThrowStatement') { | ||
const scopes = getScopes(scope); | ||
const errorName = avoidCapture('error', scopes, context.parserOptions.ecmaVersion, isSafeName); | ||
let {type, before, after} = result; | ||
for (const scope of scopes) { | ||
if (!scopeToNamesGeneratedByFixer.has(scope)) { | ||
scopeToNamesGeneratedByFixer.set(scope, new Set()); | ||
} | ||
let generateNewVariables = false; | ||
if (type === 'ThrowStatement') { | ||
const scopes = getScopes(scope); | ||
const errorName = avoidCapture('error', scopes, isSafeName); | ||
const generatedNames = scopeToNamesGeneratedByFixer.get(scope); | ||
generatedNames.add(errorName); | ||
for (const scope of scopes) { | ||
if (!scopeToNamesGeneratedByFixer.has(scope)) { | ||
scopeToNamesGeneratedByFixer.set(scope, new Set()); | ||
} | ||
const indentString = getIndentString(node, sourceCode); | ||
after = after | ||
.replace('{{INDENT_STRING}}', indentString) | ||
.replace('{{ERROR_NAME}}', errorName); | ||
before = before | ||
.replace('{{INDENT_STRING}}', indentString) | ||
.replace('{{ERROR_NAME}}', errorName); | ||
generateNewVariables = true; | ||
const generatedNames = scopeToNamesGeneratedByFixer.get(scope); | ||
generatedNames.add(errorName); | ||
} | ||
let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`; | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed); | ||
if (shouldAddSemicolonBefore) { | ||
fixed = `;${fixed}`; | ||
} | ||
const indentString = getIndentString(node, sourceCode); | ||
yield fixer.replaceText(node, fixed); | ||
after = after | ||
.replace('{{INDENT_STRING}}', indentString) | ||
.replace('{{ERROR_NAME}}', errorName); | ||
before = before | ||
.replace('{{INDENT_STRING}}', indentString) | ||
.replace('{{ERROR_NAME}}', errorName); | ||
generateNewVariables = true; | ||
} | ||
if (generateNewVariables) { | ||
yield * extendFixRange(fixer, sourceCode.ast.range); | ||
} | ||
let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`; | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed); | ||
if (shouldAddSemicolonBefore) { | ||
fixed = `;${fixed}`; | ||
} | ||
yield fixer.replaceText(node, fixed); | ||
if (generateNewVariables) { | ||
yield * extendFixRange(fixer, sourceCode.ast.range); | ||
} | ||
}; | ||
} | ||
return problem; | ||
}, | ||
}; | ||
@@ -281,6 +283,7 @@ }; | ||
enum: ['always', 'only-single-line'], | ||
default: 'always' | ||
} | ||
default: 'always', | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -291,3 +294,3 @@ create, | ||
docs: { | ||
description: 'Prefer ternary expressions over simple `if-else` statements.' | ||
description: 'Prefer ternary expressions over simple `if-else` statements.', | ||
}, | ||
@@ -297,5 +300,5 @@ fixable: 'code', | ||
messages: { | ||
[messageId]: 'This `if` statement can be replaced by a ternary expression.' | ||
} | ||
} | ||
[messageId]: 'This `if` statement can be replaced by a ternary expression.', | ||
}, | ||
}, | ||
}; |
'use strict'; | ||
const {findVariable, getFunctionHeadLocation} = require('eslint-utils'); | ||
const {matches, memberExpressionSelector} = require('./selectors/index.js'); | ||
const {findVariable, getFunctionHeadLocation} = require('@eslint-community/eslint-utils'); | ||
const {isFunction, isMemberExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -13,45 +13,100 @@ const ERROR_PROMISE = 'promise'; | ||
[ERROR_IDENTIFIER]: 'Prefer top-level await over an async function `{{name}}` call.', | ||
[SUGGESTION_ADD_AWAIT]: 'Insert `await`.' | ||
[SUGGESTION_ADD_AWAIT]: 'Insert `await`.', | ||
}; | ||
const topLevelCallExpression = 'Program > ExpressionStatement > CallExpression[optional!=true].expression'; | ||
const iife = [ | ||
topLevelCallExpression, | ||
matches([ | ||
'[callee.type="FunctionExpression"]', | ||
'[callee.type="ArrowFunctionExpression"]' | ||
]), | ||
'[callee.async!=false]', | ||
'[callee.generator!=true]' | ||
].join(''); | ||
const promise = [ | ||
topLevelCallExpression, | ||
memberExpressionSelector({ | ||
path: 'callee', | ||
names: ['then', 'catch', 'finally'] | ||
const promisePrototypeMethods = ['then', 'catch', 'finally']; | ||
const isTopLevelCallExpression = node => { | ||
if (node.type !== 'CallExpression') { | ||
return false; | ||
} | ||
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) { | ||
if ( | ||
isFunction(ancestor) | ||
|| ancestor.type === 'ClassDeclaration' | ||
|| ancestor.type === 'ClassExpression' | ||
) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
const isPromiseMethodCalleeObject = node => | ||
node.parent.type === 'MemberExpression' | ||
&& node.parent.object === node | ||
&& !node.parent.computed | ||
&& node.parent.property.type === 'Identifier' | ||
&& promisePrototypeMethods.includes(node.parent.property.name) | ||
&& node.parent.parent.type === 'CallExpression' | ||
&& node.parent.parent.callee === node.parent; | ||
const isAwaitExpressionArgument = node => { | ||
if (node.parent.type === 'ChainExpression') { | ||
node = node.parent; | ||
} | ||
return node.parent.type === 'AwaitExpression' && node.parent.argument === node; | ||
}; | ||
// `Promise.{all,allSettled,any,race}([foo()])` | ||
const isInPromiseMethods = node => | ||
node.parent.type === 'ArrayExpression' | ||
&& node.parent.elements.includes(node) | ||
&& isMethodCall(node.parent.parent, { | ||
object: 'Promise', | ||
methods: ['all', 'allSettled', 'any', 'race'], | ||
argumentsLength: 1, | ||
}) | ||
].join(''); | ||
const identifier = [ | ||
topLevelCallExpression, | ||
'[callee.type="Identifier"]' | ||
].join(''); | ||
&& node.parent.parent.arguments[0] === node.parent; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
function create(context) { | ||
if (context.filename.toLowerCase().endsWith('.cjs')) { | ||
return; | ||
} | ||
return { | ||
[promise](node) { | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_PROMISE | ||
}; | ||
}, | ||
[iife](node) { | ||
return { | ||
node, | ||
loc: getFunctionHeadLocation(node.callee, context.getSourceCode()), | ||
messageId: ERROR_IIFE | ||
}; | ||
}, | ||
[identifier](node) { | ||
const variable = findVariable(context.getScope(), node.callee); | ||
CallExpression(node) { | ||
if ( | ||
!isTopLevelCallExpression(node) | ||
|| isPromiseMethodCalleeObject(node) | ||
|| isAwaitExpressionArgument(node) | ||
|| isInPromiseMethods(node) | ||
) { | ||
return; | ||
} | ||
// Promises | ||
if (isMemberExpression(node.callee, { | ||
properties: promisePrototypeMethods, | ||
computed: false, | ||
})) { | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_PROMISE, | ||
}; | ||
} | ||
const {sourceCode} = context; | ||
// IIFE | ||
if ( | ||
(node.callee.type === 'FunctionExpression' || node.callee.type === 'ArrowFunctionExpression') | ||
&& node.callee.async | ||
&& !node.callee.generator | ||
) { | ||
return { | ||
node, | ||
loc: getFunctionHeadLocation(node.callee, sourceCode), | ||
messageId: ERROR_IIFE, | ||
}; | ||
} | ||
// Identifier | ||
if (node.callee.type !== 'Identifier') { | ||
return; | ||
} | ||
const variable = findVariable(sourceCode.getScope(node), node.callee); | ||
if (!variable || variable.defs.length !== 1) { | ||
@@ -62,13 +117,8 @@ return; | ||
const [definition] = variable.defs; | ||
const value = definition.type === 'Variable' && definition.kind === 'const' ? | ||
definition.node.init : | ||
definition.node; | ||
const value = definition.type === 'Variable' && definition.kind === 'const' | ||
? definition.node.init | ||
: definition.node; | ||
if ( | ||
!( | ||
( | ||
value.type === 'ArrowFunctionExpression' || | ||
value.type === 'FunctionExpression' || | ||
value.type === 'FunctionDeclaration' | ||
) && !value.generator && value.async | ||
) | ||
!value | ||
|| !(isFunction(value) && !value.generator && value.async) | ||
) { | ||
@@ -85,10 +135,11 @@ return; | ||
messageId: SUGGESTION_ADD_AWAIT, | ||
fix: fixer => fixer.insertTextBefore(node, 'await ') | ||
} | ||
] | ||
fix: fixer => fixer.insertTextBefore(node, 'await '), | ||
}, | ||
], | ||
}; | ||
} | ||
}, | ||
}; | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -99,7 +150,7 @@ create, | ||
docs: { | ||
description: 'Prefer top-level await over top-level promises and async function calls.' | ||
description: 'Prefer top-level await over top-level promises and async function calls.', | ||
}, | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'prefer-type-error'; | ||
const messages = { | ||
[MESSAGE_ID]: '`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.' | ||
[MESSAGE_ID]: '`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.', | ||
}; | ||
const tcIdentifiers = new Set([ | ||
const typeCheckIdentifiers = new Set([ | ||
'isArguments', | ||
@@ -46,23 +46,18 @@ 'isArray', | ||
'isWindow', | ||
'isXMLDoc' | ||
'isXMLDoc', | ||
]); | ||
const tcGlobalIdentifiers = new Set([ | ||
const typeCheckGlobalIdentifiers = new Set([ | ||
'isNaN', | ||
'isFinite' | ||
'isFinite', | ||
]); | ||
const selector = [ | ||
'ThrowStatement', | ||
newExpressionSelector({name: 'Error', path: 'argument'}) | ||
].join(''); | ||
const isTypecheckingIdentifier = (node, callExpression, isMemberExpression) => | ||
callExpression !== undefined && | ||
callExpression.arguments.length > 0 && | ||
node.type === 'Identifier' && | ||
((isMemberExpression === true && | ||
tcIdentifiers.has(node.name)) || | ||
(isMemberExpression === false && | ||
tcGlobalIdentifiers.has(node.name))); | ||
callExpression !== undefined | ||
&& callExpression.arguments.length > 0 | ||
&& node.type === 'Identifier' | ||
&& ( | ||
(isMemberExpression === true && typeCheckIdentifiers.has(node.name)) | ||
|| (isMemberExpression === false && typeCheckGlobalIdentifiers.has(node.name)) | ||
); | ||
@@ -85,26 +80,39 @@ const isLone = node => node.parent && node.parent.body && node.parent.body.length === 1; | ||
switch (node.type) { | ||
case 'Identifier': | ||
case 'Identifier': { | ||
return isTypecheckingIdentifier(node, callExpression, false); | ||
case 'MemberExpression': | ||
} | ||
case 'MemberExpression': { | ||
return isTypecheckingMemberExpression(node, callExpression); | ||
case 'CallExpression': | ||
} | ||
case 'CallExpression': { | ||
return isTypecheckingExpression(node.callee, node); | ||
case 'UnaryExpression': | ||
} | ||
case 'UnaryExpression': { | ||
return ( | ||
node.operator === 'typeof' || | ||
(node.operator === '!' && isTypecheckingExpression(node.argument)) | ||
node.operator === 'typeof' | ||
|| (node.operator === '!' && isTypecheckingExpression(node.argument)) | ||
); | ||
case 'BinaryExpression': | ||
} | ||
case 'BinaryExpression': { | ||
return ( | ||
node.operator === 'instanceof' || | ||
isTypecheckingExpression(node.left, callExpression) || | ||
isTypecheckingExpression(node.right, callExpression) | ||
node.operator === 'instanceof' | ||
|| isTypecheckingExpression(node.left, callExpression) | ||
|| isTypecheckingExpression(node.right, callExpression) | ||
); | ||
case 'LogicalExpression': | ||
} | ||
case 'LogicalExpression': { | ||
return ( | ||
isTypecheckingExpression(node.left, callExpression) && | ||
isTypecheckingExpression(node.right, callExpression) | ||
isTypecheckingExpression(node.left, callExpression) | ||
&& isTypecheckingExpression(node.right, callExpression) | ||
); | ||
default: | ||
} | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -115,21 +123,22 @@ }; | ||
const create = () => { | ||
return { | ||
[selector]: node => { | ||
if ( | ||
isLone(node) && | ||
node.parent.parent && | ||
isTypechecking(node.parent.parent) | ||
) { | ||
const errorConstructor = node.argument.callee; | ||
return { | ||
node: errorConstructor, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.insertTextBefore(errorConstructor, 'Type') | ||
}; | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
ThrowStatement(node) { | ||
if ( | ||
isNewExpression(node.argument, {name: 'Error'}) | ||
&& isLone(node) | ||
&& node.parent.parent | ||
&& isTypechecking(node.parent.parent) | ||
) { | ||
const errorConstructor = node.argument.callee; | ||
return { | ||
node: errorConstructor, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.insertTextBefore(errorConstructor, 'Type'), | ||
}; | ||
} | ||
}; | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -140,7 +149,7 @@ create, | ||
docs: { | ||
description: 'Enforce throwing `TypeError` in type checking conditions.' | ||
description: 'Enforce throwing `TypeError` in type checking conditions.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const path = require('path'); | ||
const {defaultsDeep, upperFirst, lowerFirst} = require('lodash'); | ||
const path = require('node:path'); | ||
const {defaultsDeep, upperFirst, lowerFirst} = require('./utils/lodash.js'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
@@ -10,6 +9,15 @@ const cartesianProductSamples = require('./utils/cartesian-product-samples.js'); | ||
const getVariableIdentifiers = require('./utils/get-variable-identifiers.js'); | ||
const renameVariable = require('./utils/rename-variable.js'); | ||
const isStaticRequire = require('./utils/is-static-require.js'); | ||
const {defaultReplacements, defaultAllowList} = require('./shared/abbreviations.js'); | ||
const {defaultReplacements, defaultAllowList, defaultIgnore} = require('./shared/abbreviations.js'); | ||
const {renameVariable} = require('./fix/index.js'); | ||
const getScopes = require('./utils/get-scopes.js'); | ||
const {isStaticRequire} = require('./ast/index.js'); | ||
const MESSAGE_ID_REPLACE = 'replace'; | ||
const MESSAGE_ID_SUGGESTION = 'suggestion'; | ||
const anotherNameMessage = 'A more descriptive name will do too.'; | ||
const messages = { | ||
[MESSAGE_ID_REPLACE]: `The {{nameTypeText}} \`{{discouragedName}}\` should be named \`{{replacement}}\`. ${anotherNameMessage}`, | ||
[MESSAGE_ID_SUGGESTION]: `Please rename the {{nameTypeText}} \`{{discouragedName}}\`. Suggested names are: {{replacementsText}}. ${anotherNameMessage}`, | ||
}; | ||
const isUpperCase = string => string === string.toUpperCase(); | ||
@@ -34,14 +42,16 @@ const isUpperFirst = string => isUpperCase(string[0]); | ||
ignore = [] | ||
ignore = [], | ||
} = {}) => { | ||
const mergedReplacements = extendDefaultReplacements ? | ||
defaultsDeep({}, replacements, defaultReplacements) : | ||
replacements; | ||
const mergedReplacements = extendDefaultReplacements | ||
? defaultsDeep({}, replacements, defaultReplacements) | ||
: replacements; | ||
const mergedAllowList = extendDefaultAllowList ? | ||
defaultsDeep({}, allowList, defaultAllowList) : | ||
allowList; | ||
const mergedAllowList = extendDefaultAllowList | ||
? defaultsDeep({}, allowList, defaultAllowList) | ||
: allowList; | ||
ignore = [...defaultIgnore, ...ignore]; | ||
ignore = ignore.map( | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u') | ||
pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'), | ||
); | ||
@@ -62,8 +72,8 @@ | ||
([discouragedName, replacements]) => | ||
[discouragedName, new Map(Object.entries(replacements))] | ||
) | ||
[discouragedName, new Map(Object.entries(replacements))], | ||
), | ||
), | ||
allowList: new Map(Object.entries(mergedAllowList)), | ||
ignore | ||
ignore, | ||
}; | ||
@@ -78,5 +88,5 @@ }; | ||
const replacement = replacements.get(lowerFirst(word)) || | ||
replacements.get(word) || | ||
replacements.get(upperFirst(word)); | ||
const replacement = replacements.get(lowerFirst(word)) | ||
|| replacements.get(word) | ||
|| replacements.get(upperFirst(word)); | ||
@@ -108,3 +118,3 @@ let wordReplacement = []; | ||
total: exactReplacements.length, | ||
samples: exactReplacements.slice(0, limit) | ||
samples: exactReplacements.slice(0, limit), | ||
}; | ||
@@ -135,38 +145,52 @@ } | ||
total, | ||
samples | ||
samples, | ||
} = cartesianProductSamples(combinations, limit); | ||
// `retVal` -> `['returnValue', 'Value']` -> `['returnValue']` | ||
for (const parts of samples) { | ||
for (let index = parts.length - 1; index > 0; index--) { | ||
const word = parts[index]; | ||
if (/^[A-Za-z]+$/.test(word) && parts[index - 1].endsWith(parts[index])) { | ||
parts.splice(index, 1); | ||
} | ||
} | ||
} | ||
return { | ||
total, | ||
samples: samples.map(words => words.join('')) | ||
samples: samples.map(words => words.join('')), | ||
}; | ||
}; | ||
const anotherNameMessage = 'A more descriptive name will do too.'; | ||
const formatMessage = (discouragedName, replacements, nameTypeText) => { | ||
const message = []; | ||
const getMessage = (discouragedName, replacements, nameTypeText) => { | ||
const {total, samples = []} = replacements; | ||
if (total === 1) { | ||
message.push(`The ${nameTypeText} \`${discouragedName}\` should be named \`${samples[0]}\`.`); | ||
} else { | ||
let replacementsText = samples | ||
.map(replacement => `\`${replacement}\``) | ||
.join(', '); | ||
return { | ||
messageId: MESSAGE_ID_REPLACE, | ||
data: { | ||
nameTypeText, | ||
discouragedName, | ||
replacement: samples[0], | ||
}, | ||
}; | ||
} | ||
const omittedReplacementsCount = total - samples.length; | ||
if (omittedReplacementsCount > 0) { | ||
replacementsText += `, ... (${omittedReplacementsCount > 99 ? '99+' : omittedReplacementsCount} more omitted)`; | ||
} | ||
let replacementsText = samples | ||
.map(replacement => `\`${replacement}\``) | ||
.join(', '); | ||
message.push( | ||
`Please rename the ${nameTypeText} \`${discouragedName}\`.`, | ||
`Suggested names are: ${replacementsText}.` | ||
); | ||
const omittedReplacementsCount = total - samples.length; | ||
if (omittedReplacementsCount > 0) { | ||
replacementsText += `, ... (${omittedReplacementsCount > 99 ? '99+' : omittedReplacementsCount} more omitted)`; | ||
} | ||
message.push(anotherNameMessage); | ||
return message.join(' '); | ||
return { | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: { | ||
nameTypeText, | ||
discouragedName, | ||
replacementsText, | ||
}, | ||
}; | ||
}; | ||
@@ -176,8 +200,8 @@ | ||
if ( | ||
identifier.parent.type === 'VariableDeclarator' && | ||
identifier.parent.id === identifier | ||
identifier.parent.type === 'VariableDeclarator' | ||
&& identifier.parent.id === identifier | ||
) { | ||
return ( | ||
identifier.parent.parent.type === 'VariableDeclaration' && | ||
identifier.parent.parent.parent.type === 'ExportNamedDeclaration' | ||
identifier.parent.parent.type === 'VariableDeclaration' | ||
&& identifier.parent.parent.parent.type === 'ExportNamedDeclaration' | ||
); | ||
@@ -187,4 +211,4 @@ } | ||
if ( | ||
identifier.parent.type === 'FunctionDeclaration' && | ||
identifier.parent.id === identifier | ||
identifier.parent.type === 'FunctionDeclaration' | ||
&& identifier.parent.id === identifier | ||
) { | ||
@@ -195,4 +219,4 @@ return identifier.parent.parent.type === 'ExportNamedDeclaration'; | ||
if ( | ||
identifier.parent.type === 'ClassDeclaration' && | ||
identifier.parent.id === identifier | ||
identifier.parent.type === 'ClassDeclaration' | ||
&& identifier.parent.id === identifier | ||
) { | ||
@@ -203,4 +227,4 @@ return identifier.parent.parent.type === 'ExportNamedDeclaration'; | ||
if ( | ||
identifier.parent.type === 'TSTypeAliasDeclaration' && | ||
identifier.parent.id === identifier | ||
identifier.parent.type === 'TSTypeAliasDeclaration' | ||
&& identifier.parent.id === identifier | ||
) { | ||
@@ -213,10 +237,14 @@ return identifier.parent.parent.type === 'ExportNamedDeclaration'; | ||
const shouldFix = variable => { | ||
return !getVariableIdentifiers(variable).some(identifier => isExportedIdentifier(identifier)); | ||
}; | ||
const shouldFix = variable => getVariableIdentifiers(variable) | ||
.every(identifier => | ||
!isExportedIdentifier(identifier) | ||
// In typescript parser, only `JSXOpeningElement` is added to variable | ||
// `<foo></foo>` -> `<bar></foo>` will cause parse error | ||
&& identifier.type !== 'JSXIdentifier', | ||
); | ||
const isDefaultOrNamespaceImportName = identifier => { | ||
if ( | ||
identifier.parent.type === 'ImportDefaultSpecifier' && | ||
identifier.parent.local === identifier | ||
identifier.parent.type === 'ImportDefaultSpecifier' | ||
&& identifier.parent.local === identifier | ||
) { | ||
@@ -227,4 +255,4 @@ return true; | ||
if ( | ||
identifier.parent.type === 'ImportNamespaceSpecifier' && | ||
identifier.parent.local === identifier | ||
identifier.parent.type === 'ImportNamespaceSpecifier' | ||
&& identifier.parent.local === identifier | ||
) { | ||
@@ -235,6 +263,6 @@ return true; | ||
if ( | ||
identifier.parent.type === 'ImportSpecifier' && | ||
identifier.parent.local === identifier && | ||
identifier.parent.imported.type === 'Identifier' && | ||
identifier.parent.imported.name === 'default' | ||
identifier.parent.type === 'ImportSpecifier' | ||
&& identifier.parent.local === identifier | ||
&& identifier.parent.imported.type === 'Identifier' | ||
&& identifier.parent.imported.name === 'default' | ||
) { | ||
@@ -245,5 +273,5 @@ return true; | ||
if ( | ||
identifier.parent.type === 'VariableDeclarator' && | ||
identifier.parent.id === identifier && | ||
isStaticRequire(identifier.parent.init) | ||
identifier.parent.type === 'VariableDeclarator' | ||
&& identifier.parent.id === identifier | ||
&& isStaticRequire(identifier.parent.init) | ||
) { | ||
@@ -268,7 +296,7 @@ return true; | ||
if ( | ||
identifier.parent.type === 'MemberExpression' && | ||
identifier.parent.property === identifier && | ||
!identifier.parent.computed && | ||
identifier.parent.parent.type === 'AssignmentExpression' && | ||
identifier.parent.parent.left === identifier.parent | ||
identifier.parent.type === 'MemberExpression' | ||
&& identifier.parent.property === identifier | ||
&& !identifier.parent.computed | ||
&& identifier.parent.parent.type === 'AssignmentExpression' | ||
&& identifier.parent.parent.left === identifier.parent | ||
) { | ||
@@ -279,7 +307,7 @@ return true; | ||
if ( | ||
identifier.parent.type === 'Property' && | ||
identifier.parent.key === identifier && | ||
!identifier.parent.computed && | ||
!identifier.parent.shorthand && // Shorthand properties are reported and fixed as variables | ||
identifier.parent.parent.type === 'ObjectExpression' | ||
identifier.parent.type === 'Property' | ||
&& identifier.parent.key === identifier | ||
&& !identifier.parent.computed | ||
&& !identifier.parent.shorthand // Shorthand properties are reported and fixed as variables | ||
&& identifier.parent.parent.type === 'ObjectExpression' | ||
) { | ||
@@ -290,5 +318,5 @@ return true; | ||
if ( | ||
identifier.parent.type === 'ExportSpecifier' && | ||
identifier.parent.exported === identifier && | ||
identifier.parent.local !== identifier // Same as shorthand properties above | ||
identifier.parent.type === 'ExportSpecifier' | ||
&& identifier.parent.exported === identifier | ||
&& identifier.parent.local !== identifier // Same as shorthand properties above | ||
) { | ||
@@ -299,5 +327,8 @@ return true; | ||
if ( | ||
identifier.parent.type === 'MethodDefinition' && | ||
identifier.parent.key === identifier && | ||
!identifier.parent.computed | ||
( | ||
identifier.parent.type === 'MethodDefinition' | ||
|| identifier.parent.type === 'PropertyDefinition' | ||
) | ||
&& identifier.parent.key === identifier | ||
&& !identifier.parent.computed | ||
) { | ||
@@ -307,10 +338,2 @@ return true; | ||
if ( | ||
(identifier.parent.type === 'ClassProperty' || identifier.parent.type === 'PropertyDefinition') && | ||
identifier.parent.key === identifier && | ||
!identifier.parent.computed | ||
) { | ||
return true; | ||
} | ||
return false; | ||
@@ -329,14 +352,14 @@ }; | ||
return ( | ||
!source.includes('node_modules') && | ||
(source.startsWith('.') || source.startsWith('/')) | ||
!source.includes('node_modules') | ||
&& (source.startsWith('.') || source.startsWith('/')) | ||
); | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {ecmaVersion} = context.parserOptions; | ||
const options = prepareOptions(context.options[0]); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
// A `class` declaration produces two variables in two scopes: | ||
// the inner class scope, and the outer one (whereever the class is declared). | ||
// the inner class scope, and the outer one (wherever the class is declared). | ||
// This map holds the outer ones to be later processed when the inner one is encountered. | ||
@@ -363,3 +386,3 @@ // For why this is not a eslint issue see https://github.com/eslint/eslint-scope/issues/48#issuecomment-464358754 | ||
identifiers: variable.identifiers, | ||
references: [...variable.references, ...outerClassVariable.references] | ||
references: [...variable.references, ...outerClassVariable.references], | ||
}; | ||
@@ -402,4 +425,4 @@ | ||
if ( | ||
options.checkDefaultAndNamespaceImports === 'internal' && | ||
!isInternalImport(definition) | ||
options.checkDefaultAndNamespaceImports === 'internal' | ||
&& !isInternalImport(definition) | ||
) { | ||
@@ -416,4 +439,4 @@ return; | ||
if ( | ||
options.checkShorthandImports === 'internal' && | ||
!isInternalImport(definition) | ||
options.checkShorthandImports === 'internal' | ||
&& !isInternalImport(definition) | ||
) { | ||
@@ -425,4 +448,4 @@ return; | ||
if ( | ||
!options.checkShorthandProperties && | ||
isShorthandPropertyValue(definition.name) | ||
!options.checkShorthandProperties | ||
&& isShorthandPropertyValue(definition.name) | ||
) { | ||
@@ -440,14 +463,19 @@ return; | ||
...variable.references.map(reference => reference.from), | ||
variable.scope | ||
variable.scope, | ||
]; | ||
variableReplacements.samples = variableReplacements.samples.map( | ||
name => avoidCapture(name, scopes, ecmaVersion, isSafeName) | ||
name => avoidCapture(name, scopes, isSafeName), | ||
); | ||
const problem = { | ||
...getMessage(definition.name.name, variableReplacements, 'variable'), | ||
node: definition.name, | ||
message: formatMessage(definition.name.name, variableReplacements, 'variable') | ||
}; | ||
if (variableReplacements.total === 1 && shouldFix(variable)) { | ||
if ( | ||
variableReplacements.total === 1 | ||
&& shouldFix(variable) | ||
&& variableReplacements.samples[0] | ||
&& !variable.references.some(reference => reference.vueUsedInTemplate) | ||
) { | ||
const [replacement] = variableReplacements.samples; | ||
@@ -476,14 +504,9 @@ | ||
const checkChildScopes = scope => { | ||
for (const childScope of scope.childScopes) { | ||
checkScope(childScope); | ||
const checkScope = scope => { | ||
const scopes = getScopes(scope); | ||
for (const scope of scopes) { | ||
checkVariables(scope); | ||
} | ||
}; | ||
const checkScope = scope => { | ||
checkVariables(scope); | ||
return checkChildScopes(scope); | ||
}; | ||
return { | ||
@@ -510,4 +533,4 @@ Identifier(node) { | ||
const problem = { | ||
...getMessage(node.name, identifierReplacements, 'property'), | ||
node, | ||
message: formatMessage(node.name, identifierReplacements, 'property') | ||
}; | ||
@@ -524,4 +547,4 @@ | ||
if ( | ||
filenameWithExtension === '<input>' || | ||
filenameWithExtension === '<text>' | ||
filenameWithExtension === '<input>' | ||
|| filenameWithExtension === '<text>' | ||
) { | ||
@@ -531,5 +554,5 @@ return; | ||
const extension = path.extname(filenameWithExtension); | ||
const filename = path.basename(filenameWithExtension, extension); | ||
const filenameReplacements = getNameReplacements(filename, options); | ||
const filename = path.basename(filenameWithExtension); | ||
const extension = path.extname(filename); | ||
const filenameReplacements = getNameReplacements(path.basename(filename, extension), options); | ||
@@ -543,8 +566,8 @@ if (filenameReplacements.total === 0) { | ||
context.report({ | ||
...getMessage(filename, filenameReplacements, 'filename'), | ||
node, | ||
message: formatMessage(filenameWithExtension, filenameReplacements, 'filename') | ||
}); | ||
}, | ||
'Program:exit'() { | ||
'Program:exit'(program) { | ||
if (!options.checkVariables) { | ||
@@ -554,84 +577,89 @@ return; | ||
checkScope(context.getScope()); | ||
} | ||
checkScope(context.sourceCode.getScope(program)); | ||
}, | ||
}; | ||
}; | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
checkProperties: { | ||
type: 'boolean' | ||
const schema = { | ||
type: 'array', | ||
additionalItems: false, | ||
items: [ | ||
{ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkProperties: { | ||
type: 'boolean', | ||
}, | ||
checkVariables: { | ||
type: 'boolean', | ||
}, | ||
checkDefaultAndNamespaceImports: { | ||
type: [ | ||
'boolean', | ||
'string', | ||
], | ||
pattern: 'internal', | ||
}, | ||
checkShorthandImports: { | ||
type: [ | ||
'boolean', | ||
'string', | ||
], | ||
pattern: 'internal', | ||
}, | ||
checkShorthandProperties: { | ||
type: 'boolean', | ||
}, | ||
checkFilenames: { | ||
type: 'boolean', | ||
}, | ||
extendDefaultReplacements: { | ||
type: 'boolean', | ||
}, | ||
replacements: { | ||
$ref: '#/definitions/abbreviations', | ||
}, | ||
extendDefaultAllowList: { | ||
type: 'boolean', | ||
}, | ||
allowList: { | ||
$ref: '#/definitions/booleanObject', | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true, | ||
}, | ||
}, | ||
checkVariables: { | ||
type: 'boolean' | ||
}, | ||
], | ||
definitions: { | ||
abbreviations: { | ||
type: 'object', | ||
additionalProperties: { | ||
$ref: '#/definitions/replacements', | ||
}, | ||
checkDefaultAndNamespaceImports: { | ||
type: [ | ||
'boolean', | ||
'string' | ||
], | ||
pattern: 'internal' | ||
}, | ||
replacements: { | ||
anyOf: [ | ||
{ | ||
enum: [ | ||
false, | ||
], | ||
}, | ||
{ | ||
$ref: '#/definitions/booleanObject', | ||
}, | ||
], | ||
}, | ||
booleanObject: { | ||
type: 'object', | ||
additionalProperties: { | ||
type: 'boolean', | ||
}, | ||
checkShorthandImports: { | ||
type: [ | ||
'boolean', | ||
'string' | ||
], | ||
pattern: 'internal' | ||
}, | ||
checkShorthandProperties: { | ||
type: 'boolean' | ||
}, | ||
checkFilenames: { | ||
type: 'boolean' | ||
}, | ||
extendDefaultReplacements: { | ||
type: 'boolean' | ||
}, | ||
replacements: { | ||
$ref: '#/items/0/definitions/abbreviations' | ||
}, | ||
extendDefaultAllowList: { | ||
type: 'boolean' | ||
}, | ||
allowList: { | ||
$ref: '#/items/0/definitions/booleanObject' | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
}, | ||
additionalProperties: false, | ||
definitions: { | ||
abbreviations: { | ||
type: 'object', | ||
additionalProperties: { | ||
$ref: '#/items/0/definitions/replacements' | ||
} | ||
}, | ||
replacements: { | ||
anyOf: [ | ||
{ | ||
enum: [ | ||
false | ||
] | ||
}, | ||
{ | ||
$ref: '#/items/0/definitions/booleanObject' | ||
} | ||
] | ||
}, | ||
booleanObject: { | ||
type: 'object', | ||
additionalProperties: { | ||
type: 'boolean' | ||
} | ||
} | ||
} | ||
} | ||
]; | ||
}, | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -642,7 +670,8 @@ create, | ||
docs: { | ||
description: 'Prevent abbreviations.' | ||
description: 'Prevent abbreviations.', | ||
}, | ||
fixable: 'code', | ||
schema | ||
} | ||
schema, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {matches, methodCallSelector, arrayPrototypeMethodSelector} = require('./selectors/index.js'); | ||
const {appendArgument} = require('./fix/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isArrayPrototypeProperty} = require('./utils/index.js'); | ||
const MESSAGE_ID = 'require-array-join-separator'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Missing the separator argument.' | ||
[MESSAGE_ID]: 'Missing the separator argument.', | ||
}; | ||
const selector = matches([ | ||
// `foo.join()` | ||
methodCallSelector({name: 'join', length: 0}), | ||
// `[].join.call(foo)` and `Array.prototype.join.call(foo)` | ||
[ | ||
methodCallSelector({name: 'call', length: 1}), | ||
arrayPrototypeMethodSelector({path: 'callee.object', name: 'join'}) | ||
].join('') | ||
]); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
return { | ||
[selector](node) { | ||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2); | ||
const isPrototypeMethod = node.arguments.length === 1; | ||
return { | ||
loc: { | ||
start: penultimateToken.loc[isPrototypeMethod ? 'end' : 'start'], | ||
end: lastToken.loc.end | ||
}, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => appendArgument(fixer, node, '\',\'', sourceCode) | ||
}; | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if (!( | ||
// `foo.join()` | ||
isMethodCall(node, { | ||
method: 'join', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
}) | ||
// `[].join.call(foo)` and `Array.prototype.join.call(foo)` | ||
|| ( | ||
isMethodCall(node, { | ||
method: 'call', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayPrototypeProperty(node.callee.object, { | ||
property: 'join', | ||
}) | ||
) | ||
)) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const {sourceCode} = context; | ||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2); | ||
const isPrototypeMethod = node.arguments.length === 1; | ||
return { | ||
loc: { | ||
start: penultimateToken.loc[isPrototypeMethod ? 'end' : 'start'], | ||
end: lastToken.loc.end, | ||
}, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => appendArgument(fixer, node, '\',\'', sourceCode), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -45,7 +58,7 @@ create, | ||
docs: { | ||
description: 'Enforce using the separator argument with `Array#join()`.' | ||
description: 'Enforce using the separator argument with `Array#join()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {appendArgument} = require('./fix/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const MESSAGE_ID = 'require-number-to-fixed-digits-argument'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Missing the digits argument.' | ||
[MESSAGE_ID]: 'Missing the digits argument.', | ||
}; | ||
const mathToFixed = methodCallSelector({ | ||
name: 'toFixed', | ||
length: 0 | ||
}); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
return { | ||
[mathToFixed](node) { | ||
const [ | ||
openingParenthesis, | ||
closingParenthesis | ||
] = sourceCode.getLastTokens(node, 2); | ||
return { | ||
loc: { | ||
start: openingParenthesis.loc.start, | ||
end: closingParenthesis.loc.end | ||
}, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => appendArgument(fixer, node, '0', sourceCode) | ||
}; | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'toFixed', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| node.callee.object.type === 'NewExpression' | ||
) { | ||
return; | ||
} | ||
}; | ||
}; | ||
const {sourceCode} = context; | ||
const [ | ||
openingParenthesis, | ||
closingParenthesis, | ||
] = sourceCode.getLastTokens(node, 2); | ||
return { | ||
loc: { | ||
start: openingParenthesis.loc.start, | ||
end: closingParenthesis.loc.end, | ||
}, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => appendArgument(fixer, node, '0', sourceCode), | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -43,7 +49,7 @@ create, | ||
docs: { | ||
description: 'Enforce using the digits argument with `Number#toFixed()`.' | ||
description: 'Enforce using the digits argument with `Number#toFixed()`.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {appendArgument} = require('./fix/index.js'); | ||
const ERROR = 'error'; | ||
const SUGGESTION_TARGET_LOCATION_ORIGIN = 'target-location-origin'; | ||
const SUGGESTION_SELF_LOCATION_ORIGIN = 'self-location-origin'; | ||
const SUGGESTION_STAR = 'star'; | ||
const SUGGESTION = 'suggestion'; | ||
const messages = { | ||
[ERROR]: 'Missing the `targetOrigin` argument.', | ||
[SUGGESTION_TARGET_LOCATION_ORIGIN]: 'Use `{{target}}.location.origin`.', | ||
[SUGGESTION_SELF_LOCATION_ORIGIN]: 'Use `self.location.origin`.', | ||
[SUGGESTION_STAR]: 'Use `"*"`.' | ||
[SUGGESTION]: 'Use `{{code}}`.', | ||
}; | ||
@@ -18,7 +14,16 @@ | ||
function create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[methodCallSelector({name: 'postMessage', length: 1})](node) { | ||
CallExpression(node) { | ||
if (!isMethodCall(node, { | ||
method: 'postMessage', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2); | ||
const suggestions = []; | ||
const replacements = []; | ||
const target = node.callee.object; | ||
@@ -28,16 +33,12 @@ if (target.type === 'Identifier') { | ||
suggestions.push({ | ||
messageId: SUGGESTION_TARGET_LOCATION_ORIGIN, | ||
data: {target: name}, | ||
code: `${target.name}.location.origin` | ||
}); | ||
replacements.push(`${name}.location.origin`); | ||
if (name !== 'self' && name !== 'window' && name !== 'globalThis') { | ||
suggestions.push({messageId: SUGGESTION_SELF_LOCATION_ORIGIN, code: 'self.location.origin'}); | ||
replacements.push('self.location.origin'); | ||
} | ||
} else { | ||
suggestions.push({messageId: SUGGESTION_SELF_LOCATION_ORIGIN, code: 'self.location.origin'}); | ||
replacements.push('self.location.origin'); | ||
} | ||
suggestions.push({messageId: SUGGESTION_STAR, code: '\'*\''}); | ||
replacements.push('\'*\''); | ||
@@ -47,16 +48,17 @@ return { | ||
start: penultimateToken.loc.end, | ||
end: lastToken.loc.end | ||
end: lastToken.loc.end, | ||
}, | ||
messageId: ERROR, | ||
suggest: suggestions.map(({messageId, data, code}) => ({ | ||
messageId, | ||
data, | ||
suggest: replacements.map(code => ({ | ||
messageId: SUGGESTION, | ||
data: {code}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => appendArgument(fixer, node, code, sourceCode) | ||
})) | ||
fix: fixer => appendArgument(fixer, node, code, sourceCode), | ||
})), | ||
}; | ||
} | ||
}, | ||
}; | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -67,7 +69,7 @@ create, | ||
docs: { | ||
description: 'Enforce using the `targetOrigin` argument with `window.postMessage()`.' | ||
description: 'Enforce using the `targetOrigin` argument with `window.postMessage()`.', | ||
}, | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
@@ -5,153 +5,170 @@ /* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */ | ||
acc: { | ||
accumulator: true | ||
accumulator: true, | ||
}, | ||
arg: { | ||
argument: true | ||
argument: true, | ||
}, | ||
args: { | ||
arguments: true | ||
arguments: true, | ||
}, | ||
arr: { | ||
array: true | ||
array: true, | ||
}, | ||
attr: { | ||
attribute: true | ||
attribute: true, | ||
}, | ||
attrs: { | ||
attributes: true | ||
attributes: true, | ||
}, | ||
btn: { | ||
button: true | ||
button: true, | ||
}, | ||
cb: { | ||
callback: true | ||
callback: true, | ||
}, | ||
conf: { | ||
config: true | ||
config: true, | ||
}, | ||
ctx: { | ||
context: true | ||
context: true, | ||
}, | ||
cur: { | ||
current: true | ||
current: true, | ||
}, | ||
curr: { | ||
current: true | ||
current: true, | ||
}, | ||
db: { | ||
database: true | ||
database: true, | ||
}, | ||
def: { | ||
defer: true, | ||
deferred: true, | ||
define: true, | ||
definition: true, | ||
}, | ||
dest: { | ||
destination: true | ||
destination: true, | ||
}, | ||
dev: { | ||
development: true | ||
development: true, | ||
}, | ||
dir: { | ||
direction: true, | ||
directory: true | ||
directory: true, | ||
}, | ||
dirs: { | ||
directories: true | ||
directories: true, | ||
}, | ||
dist: { | ||
distribution: true, | ||
}, | ||
doc: { | ||
document: true | ||
document: true, | ||
}, | ||
docs: { | ||
documentation: true, | ||
documents: true | ||
documents: true, | ||
}, | ||
dst: { | ||
daylightSavingTime: true, | ||
destination: true, | ||
distribution: true, | ||
}, | ||
e: { | ||
error: true, | ||
event: true | ||
event: true, | ||
}, | ||
el: { | ||
element: true | ||
element: true, | ||
}, | ||
elem: { | ||
element: true | ||
element: true, | ||
}, | ||
elems: { | ||
elements: true, | ||
}, | ||
env: { | ||
environment: true | ||
environment: true, | ||
}, | ||
envs: { | ||
environments: true | ||
environments: true, | ||
}, | ||
err: { | ||
error: true | ||
error: true, | ||
}, | ||
ev: { | ||
event: true | ||
event: true, | ||
}, | ||
evt: { | ||
event: true | ||
event: true, | ||
}, | ||
ext: { | ||
extension: true | ||
extension: true, | ||
}, | ||
exts: { | ||
extensions: true | ||
extensions: true, | ||
}, | ||
fn: { | ||
function: true | ||
function: true, | ||
}, | ||
func: { | ||
function: true | ||
function: true, | ||
}, | ||
i: { | ||
index: true | ||
index: true, | ||
}, | ||
idx: { | ||
index: true | ||
index: true, | ||
}, | ||
j: { | ||
index: true | ||
index: true, | ||
}, | ||
len: { | ||
length: true | ||
length: true, | ||
}, | ||
lib: { | ||
library: true | ||
library: true, | ||
}, | ||
mod: { | ||
module: true | ||
module: true, | ||
}, | ||
msg: { | ||
message: true | ||
message: true, | ||
}, | ||
num: { | ||
number: true | ||
number: true, | ||
}, | ||
obj: { | ||
object: true | ||
object: true, | ||
}, | ||
opts: { | ||
options: true | ||
options: true, | ||
}, | ||
param: { | ||
parameter: true | ||
parameter: true, | ||
}, | ||
params: { | ||
parameters: true | ||
parameters: true, | ||
}, | ||
pkg: { | ||
package: true | ||
package: true, | ||
}, | ||
prev: { | ||
previous: true | ||
previous: true, | ||
}, | ||
prod: { | ||
production: true | ||
production: true, | ||
}, | ||
prop: { | ||
property: true | ||
property: true, | ||
}, | ||
props: { | ||
properties: true | ||
properties: true, | ||
}, | ||
ref: { | ||
reference: true | ||
reference: true, | ||
}, | ||
refs: { | ||
references: true | ||
references: true, | ||
}, | ||
@@ -161,53 +178,53 @@ rel: { | ||
relationship: true, | ||
relative: true | ||
relative: true, | ||
}, | ||
req: { | ||
request: true | ||
request: true, | ||
}, | ||
res: { | ||
response: true, | ||
result: true | ||
result: true, | ||
}, | ||
ret: { | ||
returnValue: true | ||
returnValue: true, | ||
}, | ||
retval: { | ||
returnValue: true | ||
returnValue: true, | ||
}, | ||
sep: { | ||
separator: true | ||
separator: true, | ||
}, | ||
src: { | ||
source: true | ||
source: true, | ||
}, | ||
stdDev: { | ||
standardDeviation: true | ||
standardDeviation: true, | ||
}, | ||
str: { | ||
string: true | ||
string: true, | ||
}, | ||
tbl: { | ||
table: true | ||
table: true, | ||
}, | ||
temp: { | ||
temporary: true | ||
temporary: true, | ||
}, | ||
tit: { | ||
title: true | ||
title: true, | ||
}, | ||
tmp: { | ||
temporary: true | ||
temporary: true, | ||
}, | ||
val: { | ||
value: true | ||
value: true, | ||
}, | ||
var: { | ||
variable: true | ||
variable: true, | ||
}, | ||
vars: { | ||
variables: true | ||
variables: true, | ||
}, | ||
ver: { | ||
version: true | ||
} | ||
version: true, | ||
}, | ||
}; | ||
@@ -231,2 +248,4 @@ | ||
getInitialProps: true, | ||
getServerSideProps: true, | ||
getStaticProps: true, | ||
// React PropTypes | ||
@@ -237,3 +256,10 @@ // https://reactjs.org/docs/typechecking-with-proptypes.html | ||
// https://jestjs.io/docs/en/configuration#setupfilesafterenv-array | ||
setupFilesAfterEnv: true | ||
setupFilesAfterEnv: true, | ||
}; | ||
module.exports.defaultIgnore = [ | ||
// Internationalization and localization | ||
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1188 | ||
'i18n', | ||
'l10n', | ||
]; |
@@ -51,3 +51,3 @@ /* eslint sort-keys: ["error", "asc", {natural: true}] */ | ||
222: '\'', | ||
224: 'Meta' | ||
224: 'Meta', | ||
}; |
'use strict'; | ||
const isSameReference = require('../utils/is-same-reference.js'); | ||
const {getParenthesizedRange} = require('../utils/parentheses.js'); | ||
const {isNumberLiteral} = require('../ast/index.js'); | ||
const isLengthMemberExpression = node => | ||
node.type === 'MemberExpression' && | ||
!node.computed && | ||
!node.optional && | ||
node.property.type === 'Identifier' && | ||
node.property.name === 'length'; | ||
node.type === 'MemberExpression' | ||
&& !node.computed | ||
&& !node.optional | ||
&& node.property.type === 'Identifier' | ||
&& node.property.name === 'length'; | ||
const isLiteralPositiveNumber = node => | ||
node.type === 'Literal' && | ||
typeof node.value === 'number' && | ||
node.value > 0; | ||
isNumberLiteral(node) | ||
&& node.value > 0; | ||
@@ -39,3 +39,3 @@ function getNegativeIndexLengthNode(node, objectNode) { | ||
start, | ||
end + sourceCode.text.slice(end).match(/\S|$/).index | ||
end + sourceCode.text.slice(end).match(/\S|$/).index, | ||
]); | ||
@@ -46,3 +46,3 @@ } | ||
getNegativeIndexLengthNode, | ||
removeLengthNode | ||
removeLengthNode, | ||
}; |
'use strict'; | ||
const {hasSideEffect, isParenthesized, findVariable} = require('eslint-utils'); | ||
const {matches, methodCallSelector} = require('../selectors/index.js'); | ||
const isFunctionSelfUsedInside = require('../utils/is-function-self-used-inside.js'); | ||
const {hasSideEffect, isParenthesized, findVariable} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('../ast/index.js'); | ||
const {isSameIdentifier, isFunctionSelfUsedInside} = require('../utils/index.js'); | ||
const getBinaryExpressionSelector = path => [ | ||
`[${path}.type="BinaryExpression"]`, | ||
`[${path}.operator="==="]`, | ||
`:matches([${path}.left.type="Identifier"], [${path}.right.type="Identifier"])` | ||
].join(''); | ||
const getFunctionSelector = path => [ | ||
`[${path}.generator!=true]`, | ||
`[${path}.async!=true]`, | ||
`[${path}.params.length=1]`, | ||
`[${path}.params.0.type="Identifier"]` | ||
].join(''); | ||
const callbackFunctionSelector = path => matches([ | ||
const isSimpleCompare = (node, compareNode) => | ||
node.type === 'BinaryExpression' | ||
&& node.operator === '===' | ||
&& ( | ||
isSameIdentifier(node.left, compareNode) | ||
|| isSameIdentifier(node.right, compareNode) | ||
); | ||
const isSimpleCompareCallbackFunction = node => | ||
// Matches `foo.findIndex(bar => bar === baz)` | ||
[ | ||
`[${path}.type="ArrowFunctionExpression"]`, | ||
getFunctionSelector(path), | ||
getBinaryExpressionSelector(`${path}.body`) | ||
].join(''), | ||
( | ||
node.type === 'ArrowFunctionExpression' | ||
&& !node.async | ||
&& node.params.length === 1 | ||
&& isSimpleCompare(node.body, node.params[0]) | ||
) | ||
// Matches `foo.findIndex(bar => {return bar === baz})` | ||
// Matches `foo.findIndex(function (bar) {return bar === baz})` | ||
[ | ||
`:matches([${path}.type="ArrowFunctionExpression"], [${path}.type="FunctionExpression"])`, | ||
getFunctionSelector(path), | ||
`[${path}.body.type="BlockStatement"]`, | ||
`[${path}.body.body.length=1]`, | ||
`[${path}.body.body.0.type="ReturnStatement"]`, | ||
getBinaryExpressionSelector(`${path}.body.body.0.argument`) | ||
].join('') | ||
]); | ||
|| ( | ||
(node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') | ||
&& !node.async | ||
&& !node.generator | ||
&& node.params.length === 1 | ||
&& node.body.type === 'BlockStatement' | ||
&& node.body.body.length === 1 | ||
&& node.body.body[0].type === 'ReturnStatement' | ||
&& isSimpleCompare(node.body.body[0].argument, node.params[0]) | ||
); | ||
const isIdentifierNamed = ({type, name}, expectName) => type === 'Identifier' && name === expectName; | ||
@@ -41,7 +39,8 @@ | ||
const MESSAGE_ID_PREFIX = `prefer-${replacement}-over-${method}/`; | ||
const ERROR = `${MESSAGE_ID_PREFIX}/error`; | ||
const SUGGESTION = `${MESSAGE_ID_PREFIX}/suggestion`; | ||
const ERROR = `${MESSAGE_ID_PREFIX}error`; | ||
const SUGGESTION = `${MESSAGE_ID_PREFIX}suggestion`; | ||
const ERROR_MESSAGES = { | ||
findIndex: 'Use `.indexOf()` instead of `.findIndex()` when looking for the index of an item.', | ||
some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.` | ||
findLastIndex: 'Use `.lastIndexOf()` instead of `findLastIndex() when looking for the index of an item.`', | ||
some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.`, | ||
}; | ||
@@ -51,79 +50,81 @@ | ||
[ERROR]: ERROR_MESSAGES[method], | ||
[SUGGESTION]: `Replace \`.${method}()\` with \`.${replacement}()\`.` | ||
[SUGGESTION]: `Replace \`.${method}()\` with \`.${replacement}()\`.`, | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
name: method, | ||
length: 1 | ||
}), | ||
callbackFunctionSelector('arguments.0') | ||
].join(''); | ||
function createListeners(context) { | ||
const sourceCode = context.getSourceCode(); | ||
function listen(context) { | ||
const {sourceCode} = context; | ||
const {scopeManager} = sourceCode; | ||
return { | ||
[selector](node) { | ||
const [callback] = node.arguments; | ||
const binaryExpression = callback.body.type === 'BinaryExpression' ? | ||
callback.body : | ||
callback.body.body[0].argument; | ||
const [parameter] = callback.params; | ||
const {left, right} = binaryExpression; | ||
const {name} = parameter; | ||
context.on('CallExpression', callExpression => { | ||
if ( | ||
!isMethodCall(callExpression, { | ||
method, | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| !isSimpleCompareCallbackFunction(callExpression.arguments[0]) | ||
) { | ||
return; | ||
} | ||
let searchValueNode; | ||
let parameterInBinaryExpression; | ||
if (isIdentifierNamed(left, name)) { | ||
searchValueNode = right; | ||
parameterInBinaryExpression = left; | ||
} else if (isIdentifierNamed(right, name)) { | ||
searchValueNode = left; | ||
parameterInBinaryExpression = right; | ||
} else { | ||
return; | ||
} | ||
const [callback] = callExpression.arguments; | ||
const binaryExpression = callback.body.type === 'BinaryExpression' | ||
? callback.body | ||
: callback.body.body[0].argument; | ||
const [parameter] = callback.params; | ||
const {left, right} = binaryExpression; | ||
const {name} = parameter; | ||
const callbackScope = scopeManager.acquire(callback); | ||
if ( | ||
// `parameter` is used somewhere else | ||
findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression) || | ||
isFunctionSelfUsedInside(callback, callbackScope) | ||
) { | ||
return; | ||
} | ||
let searchValueNode; | ||
let parameterInBinaryExpression; | ||
if (isIdentifierNamed(left, name)) { | ||
searchValueNode = right; | ||
parameterInBinaryExpression = left; | ||
} else if (isIdentifierNamed(right, name)) { | ||
searchValueNode = left; | ||
parameterInBinaryExpression = right; | ||
} else { | ||
return; | ||
} | ||
const method = node.callee.property; | ||
const problem = { | ||
node: method, | ||
messageId: ERROR, | ||
suggest: [] | ||
}; | ||
const callbackScope = scopeManager.acquire(callback); | ||
if ( | ||
// `parameter` is used somewhere else | ||
findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression) | ||
|| isFunctionSelfUsedInside(callback, callbackScope) | ||
) { | ||
return; | ||
} | ||
const fix = function * (fixer) { | ||
let text = sourceCode.getText(searchValueNode); | ||
if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) { | ||
text = `(${text})`; | ||
} | ||
const methodNode = callExpression.callee.property; | ||
const problem = { | ||
node: methodNode, | ||
messageId: ERROR, | ||
suggest: [], | ||
}; | ||
yield fixer.replaceText(method, replacement); | ||
yield fixer.replaceText(callback, text); | ||
}; | ||
if (hasSideEffect(searchValueNode, sourceCode)) { | ||
problem.suggest.push({messageId: SUGGESTION, fix}); | ||
} else { | ||
problem.fix = fix; | ||
const fix = function * (fixer) { | ||
let text = sourceCode.getText(searchValueNode); | ||
if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) { | ||
text = `(${text})`; | ||
} | ||
return problem; | ||
yield fixer.replaceText(methodNode, replacement); | ||
yield fixer.replaceText(callback, text); | ||
}; | ||
if (hasSideEffect(searchValueNode, sourceCode)) { | ||
problem.suggest.push({messageId: SUGGESTION, fix}); | ||
} else { | ||
problem.fix = fix; | ||
} | ||
}; | ||
return problem; | ||
}); | ||
} | ||
return {messages, createListeners}; | ||
return {messages, listen}; | ||
} | ||
module.exports = simpleArraySearchRule; |
'use strict'; | ||
const quoteString = require('./utils/quote-string.js'); | ||
const replaceTemplateElement = require('./utils/replace-template-element.js'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const escapeTemplateElementRaw = require('./utils/escape-template-element-raw.js'); | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
@@ -9,3 +9,3 @@ const defaultMessage = 'Prefer `{{suggest}}` over `{{match}}`.'; | ||
const messages = { | ||
[SUGGESTION_MESSAGE_ID]: 'Replace `{{match}}` with `{{suggest}}`.' | ||
[SUGGESTION_MESSAGE_ID]: 'Replace `{{match}}` with `{{suggest}}`.', | ||
}; | ||
@@ -16,7 +16,7 @@ | ||
'html', | ||
'svg' | ||
'svg', | ||
]); | ||
const ignoredMemberExpressionObject = new Set([ | ||
'styled' | ||
'styled', | ||
]); | ||
@@ -38,4 +38,4 @@ | ||
if ( | ||
object.type === 'Identifier' && | ||
ignoredMemberExpressionObject.has(object.name) | ||
object.type === 'Identifier' | ||
&& ignoredMemberExpressionObject.has(object.name) | ||
) { | ||
@@ -54,3 +54,3 @@ return true; | ||
options = { | ||
suggest: options | ||
suggest: options, | ||
}; | ||
@@ -63,3 +63,3 @@ } | ||
fix: true, | ||
...options | ||
...options, | ||
}; | ||
@@ -69,6 +69,7 @@ }); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const {patterns} = { | ||
patterns: {}, | ||
...context.options[0] | ||
...context.options[0], | ||
}; | ||
@@ -78,64 +79,60 @@ const replacements = getReplacements(patterns); | ||
if (replacements.length === 0) { | ||
return {}; | ||
return; | ||
} | ||
return { | ||
'Literal, TemplateElement': node => { | ||
const {type, value, raw} = node; | ||
context.on(['Literal', 'TemplateElement'], node => { | ||
const {type, value, raw} = node; | ||
let string; | ||
if (type === 'Literal') { | ||
string = value; | ||
} else if (!isIgnoredTag(node)) { | ||
string = value.raw; | ||
} | ||
let string; | ||
if (type === 'Literal') { | ||
string = value; | ||
} else if (!isIgnoredTag(node)) { | ||
string = value.raw; | ||
} | ||
if (!string || typeof string !== 'string') { | ||
return; | ||
} | ||
if (!string || typeof string !== 'string') { | ||
return; | ||
} | ||
const replacement = replacements.find(({regex}) => regex.test(string)); | ||
const replacement = replacements.find(({regex}) => regex.test(string)); | ||
if (!replacement) { | ||
return; | ||
} | ||
if (!replacement) { | ||
return; | ||
} | ||
const {fix: autoFix, message = defaultMessage, match, suggest, regex} = replacement; | ||
const messageData = { | ||
const {fix: autoFix, message = defaultMessage, match, suggest, regex} = replacement; | ||
const problem = { | ||
node, | ||
message, | ||
data: { | ||
match, | ||
suggest | ||
}; | ||
const problem = { | ||
suggest, | ||
}, | ||
}; | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' | ||
? fixer => fixer.replaceText( | ||
node, | ||
message, | ||
data: messageData | ||
}; | ||
escapeString(fixed, raw[0]), | ||
) | ||
: fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed), | ||
); | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' ? | ||
fixer => fixer.replaceText( | ||
node, | ||
quoteString(fixed, raw[0]) | ||
) : | ||
fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed) | ||
); | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
fix, | ||
}, | ||
]; | ||
} | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
data: messageData, | ||
fix | ||
} | ||
]; | ||
} | ||
return problem; | ||
} | ||
}; | ||
return problem; | ||
}); | ||
}; | ||
@@ -146,2 +143,3 @@ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
@@ -153,3 +151,3 @@ patterns: { | ||
{ | ||
type: 'string' | ||
type: 'string', | ||
}, | ||
@@ -159,26 +157,26 @@ { | ||
required: [ | ||
'suggest' | ||
'suggest', | ||
], | ||
properties: { | ||
suggest: { | ||
type: 'string' | ||
type: 'string', | ||
}, | ||
fix: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
// Default: true | ||
}, | ||
message: { | ||
type: 'string' | ||
type: 'string', | ||
// Default: '' | ||
} | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
] | ||
}} | ||
additionalProperties: false, | ||
}, | ||
], | ||
}}, | ||
}, | ||
additionalProperties: false | ||
} | ||
}, | ||
]; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -189,9 +187,9 @@ create, | ||
docs: { | ||
description: 'Enforce better string content.' | ||
description: 'Enforce better string content.', | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
schema, | ||
messages, | ||
hasSuggestions: true | ||
} | ||
}, | ||
}; |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const {switchCallExpressionToNewExpression} = require('./fix/index.js'); | ||
const messageId = 'throw-new-error'; | ||
const messages = { | ||
[messageId]: 'Use `new` when throwing an error.' | ||
[messageId]: 'Use `new` when throwing an error.', | ||
}; | ||
@@ -11,32 +11,34 @@ | ||
const selector = [ | ||
'ThrowStatement', | ||
' > ', | ||
'CallExpression.argument', | ||
matches([ | ||
// `throw FooError()` | ||
[ | ||
'[callee.type="Identifier"]', | ||
`[callee.name=/${customError.source}/]` | ||
].join(''), | ||
// `throw lib.FooError()` | ||
[ | ||
'[callee.type="MemberExpression"]', | ||
'[callee.computed=false]', | ||
'[callee.property.type="Identifier"]', | ||
`[callee.property.name=/${customError.source}/]` | ||
].join('') | ||
]) | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if (!( | ||
node.parent.type === 'ThrowStatement' | ||
&& node.parent.argument === node | ||
)) { | ||
return; | ||
} | ||
const create = () => ({ | ||
[selector]: node => { | ||
const {callee} = node; | ||
if (!( | ||
(callee.type === 'Identifier' && customError.test(callee.name)) | ||
|| ( | ||
callee.type === 'MemberExpression' | ||
&& !callee.computed | ||
&& callee.property.type === 'Identifier' | ||
&& customError.test(callee.property.name) | ||
) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node, | ||
messageId, | ||
fix: fixer => fixer.insertTextBefore(node, 'new ') | ||
fix: fixer => switchCallExpressionToNewExpression(node, context.sourceCode, fixer), | ||
}; | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
@@ -47,7 +49,7 @@ create, | ||
docs: { | ||
description: 'Require `new` when throwing an error.' | ||
description: 'Require `new` when throwing an error.', | ||
}, | ||
fixable: 'code', | ||
messages | ||
} | ||
messages, | ||
}, | ||
}; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
function assertToken(token, {test, expected, ruleId}) { | ||
if (test && test(token)) { | ||
if (test?.(token)) { | ||
return; | ||
@@ -14,7 +14,7 @@ } | ||
if ( | ||
!test && | ||
expected.some( | ||
!test | ||
&& expected.some( | ||
expectedToken => | ||
Object.entries(expectedToken) | ||
.every(([key, value]) => token[key] === value) | ||
.every(([key, value]) => token[key] === value), | ||
) | ||
@@ -21,0 +21,0 @@ ) { |
'use strict'; | ||
const reservedWords = require('reserved-words'); | ||
const { | ||
isIdentifierName, | ||
isStrictReservedWord, | ||
isKeyword, | ||
} = require('@babel/helper-validator-identifier'); | ||
const resolveVariableName = require('./resolve-variable-name.js'); | ||
const getReferences = require('./get-references.js'); | ||
const indexifyName = (name, index) => name + '_'.repeat(index); | ||
// https://github.com/microsoft/TypeScript/issues/2536#issuecomment-87194347 | ||
const typescriptReservedWords = new Set([ | ||
'break', | ||
'case', | ||
'catch', | ||
'class', | ||
'const', | ||
'continue', | ||
'debugger', | ||
'default', | ||
'delete', | ||
'do', | ||
'else', | ||
'enum', | ||
'export', | ||
'extends', | ||
'false', | ||
'finally', | ||
'for', | ||
'function', | ||
'if', | ||
'import', | ||
'in', | ||
'instanceof', | ||
'new', | ||
'null', | ||
'return', | ||
'super', | ||
'switch', | ||
'this', | ||
'throw', | ||
'true', | ||
'try', | ||
'typeof', | ||
'var', | ||
'void', | ||
'while', | ||
'with', | ||
'as', | ||
'implements', | ||
'interface', | ||
'let', | ||
'package', | ||
'private', | ||
'protected', | ||
'public', | ||
'static', | ||
'yield', | ||
'any', | ||
'boolean', | ||
'constructor', | ||
'declare', | ||
'get', | ||
'module', | ||
'require', | ||
'number', | ||
'set', | ||
'string', | ||
'symbol', | ||
'type', | ||
'from', | ||
'of', | ||
]); | ||
const scopeHasArgumentsSpecial = scope => { | ||
while (scope) { | ||
/* istanbul ignore next: `someScopeHasVariableName` seems already handle this */ | ||
if (scope.taints.get('arguments')) { | ||
return true; | ||
} | ||
// Copied from https://github.com/babel/babel/blob/fce35af69101c6b316557e28abf60bdbf77d6a36/packages/babel-types/src/validators/isValidIdentifier.ts#L7 | ||
// Use this function instead of `require('@babel/types').isIdentifier`, since `@babel/helper-validator-identifier` package is much smaller | ||
const isValidIdentifier = name => | ||
typeof name === 'string' | ||
&& !isKeyword(name) | ||
&& !isStrictReservedWord(name, true) | ||
&& isIdentifierName(name) | ||
&& name !== 'arguments' | ||
&& !typescriptReservedWords.has(name); | ||
scope = scope.upper; | ||
} | ||
return false; | ||
}; | ||
const someScopeHasVariableName = (name, scopes) => scopes.some(scope => resolveVariableName(name, scope)); | ||
const someScopeIsStrict = scopes => scopes.some(scope => scope.isStrict); | ||
const nameCollidesWithArgumentsSpecial = (name, scopes, isStrict) => { | ||
if (name !== 'arguments') { | ||
return false; | ||
} | ||
return isStrict || scopes.some(scope => scopeHasArgumentsSpecial(scope)); | ||
}; | ||
/* | ||
@@ -49,18 +101,8 @@ Unresolved reference is probably from the global scope. We should avoid using that name. | ||
*/ | ||
const isUnresolvedName = (name, scopes) => scopes.some(scope => | ||
scope.references.some(reference => reference.identifier && reference.identifier.name === name && !reference.resolved) || | ||
isUnresolvedName(name, scope.childScopes) | ||
); | ||
const isUnresolvedName = (name, scope) => | ||
getReferences(scope).some(({identifier, resolved}) => identifier?.name === name && !resolved); | ||
const isSafeName = (name, scopes, ecmaVersion, isStrict) => { | ||
ecmaVersion = Math.min(6, ecmaVersion); // 6 is the latest version understood by `reservedWords` | ||
const isSafeName = (name, scopes) => | ||
!scopes.some(scope => resolveVariableName(name, scope) || isUnresolvedName(name, scope)); | ||
return ( | ||
!someScopeHasVariableName(name, scopes) && | ||
!reservedWords.check(name, ecmaVersion, isStrict) && | ||
!nameCollidesWithArgumentsSpecial(name, scopes, isStrict) && | ||
!isUnresolvedName(name, scopes) | ||
); | ||
}; | ||
const alwaysTrue = () => true; | ||
@@ -72,5 +114,5 @@ | ||
@callback isSafe | ||
@param {string} indexifiedName - The generated candidate name. | ||
@param {string} name - The generated candidate name. | ||
@param {Scope[]} scopes - The same list of scopes you pass to `avoidCapture`. | ||
@returns {boolean} - `true` if the `indexifiedName` is ok. | ||
@returns {boolean} - `true` if the `name` is ok. | ||
*/ | ||
@@ -89,20 +131,19 @@ | ||
@param {Scope[]} scopes - The list of scopes the new variable will be referenced in. | ||
@param {number} ecmaVersion - The language version, get it from `context.parserOptions.ecmaVersion`. | ||
@param {isSafe} [isSafe] - Rule-specific name check function. | ||
@returns {string} - Either `name` as is, or a string like `${name}_` suffixed with underscores to make the name unique. | ||
*/ | ||
module.exports = (name, scopes, ecmaVersion, isSafe = alwaysTrue) => { | ||
const isStrict = someScopeIsStrict(scopes); | ||
module.exports = (name, scopes, isSafe = alwaysTrue) => { | ||
if (!isValidIdentifier(name)) { | ||
name += '_'; | ||
let index = 0; | ||
let indexifiedName = indexifyName(name, index); | ||
while ( | ||
!isSafeName(indexifiedName, scopes, ecmaVersion, isStrict) || | ||
!isSafe(indexifiedName, scopes) | ||
) { | ||
index++; | ||
indexifiedName = indexifyName(name, index); | ||
if (!isValidIdentifier(name)) { | ||
return; | ||
} | ||
} | ||
return indexifiedName; | ||
while (!isSafeName(name, scopes) || !isSafe(name, scopes)) { | ||
name += '_'; | ||
} | ||
return name; | ||
}; |
@@ -5,31 +5,21 @@ 'use strict'; | ||
const isLogicNot = node => | ||
node && | ||
node.type === 'UnaryExpression' && | ||
node.operator === '!'; | ||
const isLogicNotArgument = node => | ||
isLogicNot(node.parent) && | ||
node.parent.argument === node; | ||
const isBooleanCallArgument = node => | ||
isBooleanCall(node.parent) && | ||
node.parent.arguments[0] === node; | ||
const isLogicNot = node => node?.type === 'UnaryExpression' && node.operator === '!'; | ||
const isLogicNotArgument = node => isLogicNot(node.parent) && node.parent.argument === node; | ||
const isBooleanCallArgument = node => isBooleanCall(node.parent) && node.parent.arguments[0] === node; | ||
const isBooleanCall = node => | ||
node && | ||
node.type === 'CallExpression' && | ||
node.callee && | ||
node.callee.type === 'Identifier' && | ||
node.callee.name === 'Boolean' && | ||
node.arguments.length === 1; | ||
node?.type === 'CallExpression' | ||
&& node.callee.type === 'Identifier' | ||
&& node.callee.name === 'Boolean' | ||
&& node.arguments.length === 1; | ||
const isVueBooleanAttributeValue = node => | ||
node && | ||
node.type === 'VExpressionContainer' && | ||
node.parent.type === 'VAttribute' && | ||
node.parent.directive && | ||
node.parent.value === node && | ||
node.parent.key.type === 'VDirectiveKey' && | ||
node.parent.key.name.type === 'VIdentifier' && | ||
( | ||
node.parent.key.name.rawName === 'if' || | ||
node.parent.key.name.rawName === 'else-if' || | ||
node.parent.key.name.rawName === 'show' | ||
node?.type === 'VExpressionContainer' | ||
&& node.parent.type === 'VAttribute' | ||
&& node.parent.directive | ||
&& node.parent.value === node | ||
&& node.parent.key.type === 'VDirectiveKey' | ||
&& node.parent.key.name.type === 'VIdentifier' | ||
&& ( | ||
node.parent.key.name.rawName === 'if' | ||
|| node.parent.key.name.rawName === 'else-if' | ||
|| node.parent.key.name.rawName === 'show' | ||
); | ||
@@ -45,6 +35,6 @@ | ||
if ( | ||
isLogicNot(node) || | ||
isLogicNotArgument(node) || | ||
isBooleanCall(node) || | ||
isBooleanCallArgument(node) | ||
isLogicNot(node) | ||
|| isLogicNotArgument(node) | ||
|| isBooleanCall(node) | ||
|| isBooleanCallArgument(node) | ||
) { | ||
@@ -61,9 +51,9 @@ return true; | ||
( | ||
parent.type === 'IfStatement' || | ||
parent.type === 'ConditionalExpression' || | ||
parent.type === 'WhileStatement' || | ||
parent.type === 'DoWhileStatement' || | ||
parent.type === 'ForStatement' | ||
) && | ||
parent.test === node | ||
parent.type === 'IfStatement' | ||
|| parent.type === 'ConditionalExpression' | ||
|| parent.type === 'WhileStatement' | ||
|| parent.type === 'DoWhileStatement' | ||
|| parent.type === 'ForStatement' | ||
) | ||
&& parent.test === node | ||
) { | ||
@@ -70,0 +60,0 @@ return true; |
'use strict'; | ||
const typedArray = require('../shared/typed-array.js'); | ||
@@ -7,13 +8,6 @@ const enforceNew = [ | ||
'ArrayBuffer', | ||
'BigInt64Array', | ||
'BigUint64Array', | ||
'DataView', | ||
'Date', | ||
'Error', | ||
'Float32Array', | ||
'Float64Array', | ||
'Function', | ||
'Int8Array', | ||
'Int16Array', | ||
'Int32Array', | ||
'Map', | ||
@@ -25,6 +19,7 @@ 'WeakMap', | ||
'RegExp', | ||
'Uint8Array', | ||
'Uint16Array', | ||
'Uint32Array', | ||
'Uint8ClampedArray' | ||
'SharedArrayBuffer', | ||
'Proxy', | ||
'WeakRef', | ||
'FinalizationRegistry', | ||
...typedArray, | ||
]; | ||
@@ -37,3 +32,3 @@ | ||
'String', | ||
'Symbol' | ||
'Symbol', | ||
]; | ||
@@ -43,3 +38,3 @@ | ||
enforceNew, | ||
disallowNew | ||
disallowNew, | ||
}; |
'use strict'; | ||
const getTotal = combinations => { | ||
let total = 1; | ||
for (const {length} of combinations) { | ||
total *= length; | ||
} | ||
return total; | ||
}; | ||
module.exports = (combinations, length = Number.POSITIVE_INFINITY) => { | ||
const total = getTotal(combinations); | ||
const total = combinations.reduce((total, {length}) => total * length, 1); | ||
@@ -31,4 +22,4 @@ const samples = Array.from({length: Math.min(total, length)}, (_, sampleIndex) => { | ||
total, | ||
samples | ||
samples, | ||
}; | ||
}; |
'use strict'; | ||
const packageJson = require('../../package.json'); | ||
const repoUrl = 'https://github.com/sindresorhus/eslint-plugin-unicorn'; | ||
/** @returns {{ [ruleName: string]: import('eslint').Rule.RuleModule }} */ | ||
function createDeprecatedRules(data) { | ||
@@ -13,10 +15,9 @@ return Object.fromEntries( | ||
docs: { | ||
url: `${repoUrl}/blob/v${packageJson.version}/docs/deprecated-rules.md#${ruleId}` | ||
url: `${repoUrl}/blob/v${packageJson.version}/docs/deprecated-rules.md#${ruleId}`, | ||
}, | ||
deprecated: true, | ||
replacedBy: Array.isArray(replacedBy) ? replacedBy : [replacedBy], | ||
schema: [] | ||
} | ||
} | ||
]) | ||
}, | ||
}, | ||
]), | ||
); | ||
@@ -23,0 +24,0 @@ } |
'use strict'; | ||
module.exports = string => string.replace( | ||
module.exports = string => string.replaceAll( | ||
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g, | ||
'\\$<symbol>' | ||
'\\$<symbol>', | ||
); |
'use strict'; | ||
const {isOpeningParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken} = require('@eslint-community/eslint-utils'); | ||
@@ -17,3 +17,3 @@ /** | ||
openingParenthesisToken.range[1], | ||
closingParenthesisToken.range[0] | ||
closingParenthesisToken.range[0], | ||
); | ||
@@ -20,0 +20,0 @@ }; |
'use strict'; | ||
const path = require('path'); | ||
const path = require('node:path'); | ||
const packageJson = require('../../package.json'); | ||
@@ -4,0 +4,0 @@ |
'use strict'; | ||
const {uniq} = require('lodash'); | ||
const getReferences = scope => uniq([ | ||
...scope.references, | ||
...scope.childScopes.flatMap(scope => getReferences(scope)) | ||
]); | ||
const getScopes = require('./get-scopes.js'); | ||
const getReferences = scope => [...new Set( | ||
getScopes(scope).flatMap(({references}) => references), | ||
)]; | ||
module.exports = getReferences; |
'use strict'; | ||
const {uniq} = require('lodash'); | ||
// Get identifiers of given variable | ||
module.exports = ({identifiers, references}) => uniq([ | ||
module.exports = ({identifiers, references}) => [...new Set([ | ||
...identifiers, | ||
...references.map(({identifier}) => identifier) | ||
]); | ||
...references.map(({identifier}) => identifier), | ||
])]; |
'use strict'; | ||
module.exports = (node1, node2) => | ||
node1 && | ||
node2 && | ||
node1.range[0] === node2.range[0] && | ||
node1.range[1] === node2.range[1]; | ||
node1 | ||
&& node2 | ||
&& node1.range[0] === node2.range[0] | ||
&& node1.range[1] === node2.range[1]; |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
@@ -17,3 +17,3 @@ const getReferences = (scope, nodeOrName) => { | ||
function isFunctionSelfUsedInside(functionNode, functionScope) { | ||
/* istanbul ignore next */ | ||
/* c8 ignore next 3 */ | ||
if (functionScope.block !== functionNode) { | ||
@@ -20,0 +20,0 @@ throw new Error('"functionScope" should be the scope of "functionNode".'); |
'use strict'; | ||
// Keep logic sync with `../selector/not-left-hand-side.js` | ||
const isLeftHandSide = node => | ||
(node.parent.type === 'AssignmentExpression' && node.parent.left === node) || | ||
(node.parent.type === 'UpdateExpression' && node.parent.argument === node) || | ||
( | ||
node.parent.type === 'UnaryExpression' && | ||
node.parent.operator === 'delete' && | ||
node.parent.argument === node | ||
(node.parent.type === 'AssignmentExpression' || node.parent.type === 'AssignmentPattern') | ||
&& node.parent.left === node | ||
) | ||
|| (node.parent.type === 'UpdateExpression' && node.parent.argument === node) | ||
|| (node.parent.type === 'ArrayPattern' && node.parent.elements.includes(node)) | ||
|| ( | ||
node.parent.type === 'Property' | ||
&& node.parent.value === node | ||
&& node.parent.parent.type === 'ObjectPattern' | ||
&& node.parent.parent.properties.includes(node.parent) | ||
) | ||
|| ( | ||
node.parent.type === 'UnaryExpression' | ||
&& node.parent.operator === 'delete' | ||
&& node.parent.argument === node | ||
); | ||
module.exports = isLeftHandSide; |
@@ -13,6 +13,5 @@ 'use strict'; | ||
const isLogicalExpression = node => | ||
node && | ||
node.type === 'LogicalExpression' && | ||
(node.operator === '&&' || node.operator === '||'); | ||
node?.type === 'LogicalExpression' | ||
&& (node.operator === '&&' || node.operator === '||'); | ||
module.exports = isLogicalExpression; |
'use strict'; | ||
const isMethodNamed = (node, name) => | ||
node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.property.type === 'Identifier' && | ||
node.callee.property.name === name; | ||
node.type === 'CallExpression' | ||
&& node.callee.type === 'MemberExpression' | ||
&& node.callee.property.type === 'Identifier' | ||
&& node.callee.property.name === name; | ||
module.exports = isMethodNamed; |
'use strict'; | ||
const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
@@ -21,7 +21,7 @@ /** | ||
// The expression should end with its own parens, for example, `new new Foo()` is not a new expression with parens. | ||
return isOpeningParenToken(penultimateToken) && | ||
isClosingParenToken(lastToken) && | ||
node.callee.range[1] < node.range[1]; | ||
return isOpeningParenToken(penultimateToken) | ||
&& isClosingParenToken(lastToken) | ||
&& node.callee.range[1] < node.range[1]; | ||
} | ||
module.exports = isNewExpressionWithParentheses; |
@@ -20,4 +20,4 @@ 'use strict'; | ||
return ( | ||
(node.type === 'Identifier' && node.name === name) || | ||
(name === 'this' && node.type === 'ThisExpression') | ||
(node.type === 'Identifier' && node.name === name) | ||
|| (name === 'this' && node.type === 'ThisExpression') | ||
); | ||
@@ -27,7 +27,7 @@ } | ||
if ( | ||
node.type !== 'MemberExpression' || | ||
node.optional || | ||
node.computed || | ||
node.property.type !== 'Identifier' || | ||
node.property.name !== name | ||
node.type !== 'MemberExpression' | ||
|| node.optional | ||
|| node.computed | ||
|| node.property.type !== 'Identifier' | ||
|| node.property.name !== name | ||
) { | ||
@@ -54,3 +54,3 @@ return false; | ||
isNodeMatchesNameOrPath, | ||
isNodeMatches | ||
isNodeMatches, | ||
}; |
@@ -5,8 +5,8 @@ 'use strict'; | ||
return ( | ||
callee.type === 'MemberExpression' && | ||
callee.object.type === 'Identifier' && | ||
callee.object.name === object && | ||
callee.property.type === 'Identifier' && | ||
callee.property.name === method | ||
callee.type === 'MemberExpression' | ||
&& callee.object.type === 'Identifier' | ||
&& callee.object.name === object | ||
&& callee.property.type === 'Identifier' | ||
&& callee.property.name === method | ||
); | ||
}; |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
// Copied from https://github.com/eslint/eslint/blob/c3e9accce2f61b04ab699fd37c90703305281aa3/lib/rules/utils/ast-utils.js#L379 | ||
// Copied from https://github.com/eslint/eslint/blob/94ba68d76a6940f68ff82eea7332c6505f93df76/lib/rules/utils/ast-utils.js#L392 | ||
@@ -39,18 +39,21 @@ /** | ||
switch (node && node.type) { | ||
case 'MemberExpression': | ||
switch (node?.type) { | ||
case 'MemberExpression': { | ||
property = node.property; | ||
break; | ||
} | ||
/* istanbul ignore next: Hard to test */ | ||
case 'ChainExpression': | ||
/* c8 ignore next 2 */ | ||
case 'ChainExpression': { | ||
return getStaticPropertyName(node.expression); | ||
} | ||
/* istanbul ignore next: Only reachable when use this to get class/object member key */ | ||
// Only reachable when use this to get class/object member key | ||
/* c8 ignore next */ | ||
case 'Property': | ||
case 'MethodDefinition': | ||
/* istanbul ignore next */ | ||
case 'MethodDefinition': { | ||
/* c8 ignore next 2 */ | ||
property = node.key; | ||
/* istanbul ignore next */ | ||
break; | ||
} | ||
@@ -84,6 +87,6 @@ // No default | ||
return Boolean( | ||
left.regex && | ||
right.regex && | ||
left.regex.pattern === right.regex.pattern && | ||
left.regex.flags === right.regex.flags | ||
left.regex | ||
&& right.regex | ||
&& left.regex.pattern === right.regex.pattern | ||
&& left.regex.flags === right.regex.flags, | ||
); | ||
@@ -126,13 +129,18 @@ } | ||
case 'Super': | ||
case 'ThisExpression': | ||
case 'ThisExpression': { | ||
return true; | ||
} | ||
case 'Identifier': | ||
case 'PrivateIdentifier': { | ||
return left.name === right.name; | ||
} | ||
case 'Literal': | ||
case 'Literal': { | ||
return equalLiteralValue(left, right); | ||
} | ||
case 'ChainExpression': | ||
case 'ChainExpression': { | ||
return isSameReference(left.expression, right.expression); | ||
} | ||
@@ -142,12 +150,25 @@ case 'MemberExpression': { | ||
// X.y = x["y"] | ||
// `x.y = x["y"]` | ||
if (nameA !== undefined) { | ||
return ( | ||
isSameReference(left.object, right.object) | ||
&& nameA === getStaticPropertyName(right) | ||
); | ||
} | ||
/* | ||
`x[0] = x[0]` | ||
`x[y] = x[y]` | ||
`x.y = x.y` | ||
*/ | ||
return ( | ||
typeof nameA !== 'undefined' && | ||
isSameReference(left.object, right.object) && | ||
nameA === getStaticPropertyName(right) | ||
left.computed === right.computed | ||
&& isSameReference(left.object, right.object) | ||
&& isSameReference(left.property, right.property) | ||
); | ||
} | ||
default: | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -154,0 +175,0 @@ } |
@@ -28,5 +28,4 @@ 'use strict'; | ||
return ( | ||
reference && | ||
reference.resolved && | ||
reference.resolved.defs.length > 0 | ||
Boolean(reference?.resolved) | ||
&& reference.resolved.defs.length > 0 | ||
); | ||
@@ -33,0 +32,0 @@ } |
'use strict'; | ||
const isShorthandPropertyValue = require('./is-shorthand-property-value.js'); | ||
const isShorthandPropertyAssignmentPatternLeft = identifier => | ||
identifier.parent.type === 'AssignmentPattern' && | ||
identifier.parent.left === identifier && | ||
isShorthandPropertyValue(identifier.parent); | ||
identifier.parent.type === 'AssignmentPattern' | ||
&& identifier.parent.left === identifier | ||
&& isShorthandPropertyValue(identifier.parent); | ||
module.exports = isShorthandPropertyAssignmentPatternLeft; |
'use strict'; | ||
const isShorthandPropertyValue = identifier => | ||
identifier.parent.type === 'Property' && | ||
identifier.parent.shorthand && | ||
identifier === identifier.parent.value; | ||
identifier.parent.type === 'Property' | ||
&& identifier.parent.shorthand | ||
&& identifier === identifier.parent.value; | ||
module.exports = isShorthandPropertyValue; |
'use strict'; | ||
module.exports = ({parent}) => !parent || parent.type === 'ExpressionStatement'; | ||
const {isExpressionStatement} = require('../ast/index.js'); | ||
module.exports = node => isExpressionStatement(node.parent); |
@@ -9,3 +9,3 @@ 'use strict'; | ||
'Numeric', | ||
'RegularExpression' | ||
'RegularExpression', | ||
]); | ||
@@ -22,3 +22,3 @@ | ||
',', | ||
'.' | ||
'.', | ||
]); | ||
@@ -37,4 +37,4 @@ | ||
if ( | ||
code === '' || | ||
(code && !charactersMightNeedsSemicolon.has(code.charAt(0))) | ||
code === '' | ||
|| (code && !charactersMightNeedsSemicolon.has(code.charAt(0))) | ||
) { | ||
@@ -41,0 +41,0 @@ return false; |
'use strict'; | ||
const {isNumberLiteral, isBigIntLiteral} = require('../ast/index.js'); | ||
// Determine whether this node is a decimal integer literal. | ||
@@ -7,8 +9,6 @@ // Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/utils/ast-utils.js#L1237 | ||
const isDecimalInteger = text => DECIMAL_INTEGER_PATTERN.test(text); | ||
const isDecimalIntegerNode = node => isNumber(node) && isDecimalInteger(node.raw); | ||
const isDecimalIntegerNode = node => isNumberLiteral(node) && isDecimalInteger(node.raw); | ||
const isNumber = node => typeof node.value === 'number'; | ||
const isBigInt = node => Boolean(node.bigint); | ||
const isNumeric = node => isNumber(node) || isBigInt(node); | ||
const isLegacyOctal = node => isNumber(node) && /^0\d+$/.test(node.raw); | ||
const isNumeric = node => isNumberLiteral(node) || isBigIntLiteral(node); | ||
const isLegacyOctal = node => isNumberLiteral(node) && /^0\d+$/.test(node.raw); | ||
@@ -32,3 +32,3 @@ function getPrefix(text) { | ||
sign = '', | ||
power = '' | ||
power = '', | ||
} = text.match(/^(?<number>[\d._]*?)(?:(?<mark>[Ee])(?<sign>[+-])?(?<power>[\d_]+))?$/).groups; | ||
@@ -50,4 +50,2 @@ | ||
isDecimalInteger, | ||
isNumber, | ||
isBigInt, | ||
isNumeric, | ||
@@ -57,3 +55,3 @@ isLegacyOctal, | ||
parseNumber, | ||
parseFloatNumber | ||
parseFloatNumber, | ||
}; |
'use strict'; | ||
const {isParenthesized, isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const {isParenthesized, isOpeningParenToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
@@ -12,7 +12,2 @@ /* | ||
function getParenthesizedTimes(node, sourceCode) { | ||
// Workaround for https://github.com/mysticatea/eslint-utils/pull/25 | ||
if (!node.parent) { | ||
return 0; | ||
} | ||
let times = 0; | ||
@@ -43,3 +38,3 @@ | ||
...sourceCode.getTokensBefore(node, {count, filter: isOpeningParenToken}), | ||
...sourceCode.getTokensAfter(node, {count, filter: isClosingParenToken}) | ||
...sourceCode.getTokensAfter(node, {count, filter: isClosingParenToken}), | ||
]; | ||
@@ -58,3 +53,3 @@ } | ||
const [start] = (parentheses[0] || node).range; | ||
const [, end] = (parentheses[parentheses.length - 1] || node).range; | ||
const [, end] = (parentheses.at(-1) || node).range; | ||
return [start, end]; | ||
@@ -80,3 +75,3 @@ } | ||
getParenthesizedRange, | ||
getParenthesizedText | ||
getParenthesizedText, | ||
}; |
'use strict'; | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
const path = require('node:path'); | ||
const fs = require('node:fs'); | ||
const getDocumentationUrl = require('./get-documentation-url.js'); | ||
const isIterable = object => typeof object[Symbol.iterator] === 'function'; | ||
const isIterable = object => typeof object?.[Symbol.iterator] === 'function'; | ||
function reportListenerProblems(listener, context) { | ||
// Listener arguments can be `codePath, node` or `node` | ||
return function (...listenerArguments) { | ||
let problems = listener(...listenerArguments); | ||
class FixAbortError extends Error {} | ||
const fixOptions = { | ||
abort() { | ||
throw new FixAbortError('Fix aborted.'); | ||
}, | ||
}; | ||
if (!problems) { | ||
return; | ||
function wrapFixFunction(fix) { | ||
return fixer => { | ||
const result = fix(fixer, fixOptions); | ||
if (isIterable(result)) { | ||
try { | ||
return [...result]; | ||
} catch (error) { | ||
if (error instanceof FixAbortError) { | ||
return; | ||
} | ||
/* c8 ignore next */ | ||
throw error; | ||
} | ||
} | ||
if (!isIterable(problems)) { | ||
problems = [problems]; | ||
return result; | ||
}; | ||
} | ||
function reportListenerProblems(problems, context) { | ||
if (!problems) { | ||
return; | ||
} | ||
if (!isIterable(problems)) { | ||
problems = [problems]; | ||
} | ||
for (const problem of problems) { | ||
if (problem.fix) { | ||
problem.fix = wrapFixFunction(problem.fix); | ||
} | ||
// TODO: Allow `fix` function to abort | ||
for (const problem of problems) { | ||
if (problem) { | ||
context.report(problem); | ||
if (Array.isArray(problem.suggest)) { | ||
for (const suggest of problem.suggest) { | ||
if (suggest.fix) { | ||
suggest.fix = wrapFixFunction(suggest.fix); | ||
} | ||
suggest.data = { | ||
...problem.data, | ||
...suggest.data, | ||
}; | ||
} | ||
} | ||
}; | ||
context.report(problem); | ||
} | ||
} | ||
@@ -37,7 +74,51 @@ | ||
const wrapped = context => Object.fromEntries( | ||
Object.entries(create(context)) | ||
.map(([selector, listener]) => [selector, reportListenerProblems(listener, context)]) | ||
); | ||
const wrapped = context => { | ||
const listeners = {}; | ||
const addListener = (selector, listener) => { | ||
listeners[selector] ??= []; | ||
listeners[selector].push(listener); | ||
}; | ||
const contextProxy = new Proxy(context, { | ||
get(target, property, receiver) { | ||
if (property === 'on') { | ||
return (selectorOrSelectors, listener) => { | ||
const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors]; | ||
for (const selector of selectors) { | ||
addListener(selector, listener); | ||
} | ||
}; | ||
} | ||
if (property === 'onExit') { | ||
return (selectorOrSelectors, listener) => { | ||
const selectors = Array.isArray(selectorOrSelectors) ? selectorOrSelectors : [selectorOrSelectors]; | ||
for (const selector of selectors) { | ||
addListener(`${selector}:exit`, listener); | ||
} | ||
}; | ||
} | ||
return Reflect.get(target, property, receiver); | ||
}, | ||
}); | ||
for (const [selector, listener] of Object.entries(create(contextProxy) ?? {})) { | ||
addListener(selector, listener); | ||
} | ||
return Object.fromEntries( | ||
Object.entries(listeners) | ||
.map(([selector, listeners]) => [ | ||
selector, | ||
// Listener arguments can be `codePath, node` or `node` | ||
(...listenerArguments) => { | ||
for (const listener of listeners) { | ||
reportListenerProblems(listener(...listenerArguments), context); | ||
} | ||
}, | ||
]), | ||
); | ||
}; | ||
wrappedFunctions.add(wrapped); | ||
@@ -50,6 +131,6 @@ | ||
const { | ||
visitScriptBlock | ||
visitScriptBlock, | ||
} = { | ||
visitScriptBlock: true, | ||
...options | ||
...options, | ||
}; | ||
@@ -61,11 +142,9 @@ | ||
const listeners = create(context); | ||
const {parserServices} = context.sourceCode; | ||
// `vue-eslint-parser` | ||
if ( | ||
context.parserServices && | ||
context.parserServices.defineTemplateBodyVisitor | ||
) { | ||
return visitScriptBlock ? | ||
context.parserServices.defineTemplateBodyVisitor(listeners, listeners) : | ||
context.parserServices.defineTemplateBodyVisitor(listeners); | ||
if (parserServices?.defineTemplateBodyVisitor) { | ||
return visitScriptBlock | ||
? parserServices.defineTemplateBodyVisitor(listeners, listeners) | ||
: parserServices.defineTemplateBodyVisitor(listeners); | ||
} | ||
@@ -80,2 +159,3 @@ | ||
/** @returns {import('eslint').Rule.RuleModule} */ | ||
function loadRule(ruleId) { | ||
@@ -92,6 +172,6 @@ const rule = require(`../${ruleId}`); | ||
...rule.meta.docs, | ||
url: getDocumentationUrl(ruleId) | ||
} | ||
url: getDocumentationUrl(ruleId), | ||
}, | ||
}, | ||
create: reportProblems(rule.create) | ||
create: reportProblems(rule.create), | ||
}; | ||
@@ -107,3 +187,3 @@ } | ||
return [ruleId, loadRule(ruleId)]; | ||
}) | ||
}), | ||
); | ||
@@ -115,3 +195,3 @@ } | ||
loadRules, | ||
checkVueTemplate | ||
checkVueTemplate, | ||
}; |
@@ -10,9 +10,9 @@ 'use strict'; | ||
function shouldAddParenthesesToConditionalExpressionChild(node) { | ||
return node.type === 'AwaitExpression' || | ||
return node.type === 'AwaitExpression' | ||
// Lower precedence, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table | ||
node.type === 'AssignmentExpression' || | ||
node.type === 'YieldExpression' || | ||
node.type === 'SequenceExpression'; | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'YieldExpression' | ||
|| node.type === 'SequenceExpression'; | ||
} | ||
module.exports = shouldAddParenthesesToConditionalExpressionChild; |
@@ -12,8 +12,13 @@ 'use strict'; | ||
switch (node.type) { | ||
case 'ObjectExpression': | ||
case 'ObjectExpression': { | ||
return true; | ||
case 'AssignmentExpression': | ||
} | ||
case 'AssignmentExpression': { | ||
return node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern'; | ||
default: | ||
} | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -20,0 +25,0 @@ } |
@@ -10,11 +10,19 @@ 'use strict'; | ||
function shouldAddParenthesesToLogicalExpressionChild(node, {operator, property}) { | ||
/* istanbul ignore next: When operator or property is different, need check `LogicalExpression` operator precedence, not implemented */ | ||
if (operator !== '??' || property !== 'left') { | ||
throw new Error('Not supported.'); | ||
// We are not using this, but we can improve this function with it | ||
/* c8 ignore next 3 */ | ||
if (!property) { | ||
throw new Error('`property` is required.'); | ||
} | ||
if ( | ||
node.type === 'LogicalExpression' | ||
&& node.operator === operator | ||
) { | ||
return false; | ||
} | ||
// Not really needed, but more readable | ||
if ( | ||
node.type === 'AwaitExpression' || | ||
node.type === 'BinaryExpression' | ||
node.type === 'AwaitExpression' | ||
|| node.type === 'BinaryExpression' | ||
) { | ||
@@ -27,7 +35,8 @@ return true; | ||
if ( | ||
node.type === 'ConditionalExpression' || | ||
node.type === 'AssignmentExpression' || | ||
node.type === 'AssignmentExpression' || | ||
node.type === 'YieldExpression' || | ||
node.type === 'SequenceExpression' | ||
node.type === 'LogicalExpression' | ||
|| node.type === 'ConditionalExpression' | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'ArrowFunctionExpression' | ||
|| node.type === 'YieldExpression' | ||
|| node.type === 'SequenceExpression' | ||
) { | ||
@@ -34,0 +43,0 @@ return true; |
@@ -24,8 +24,12 @@ 'use strict'; | ||
case 'ArrayExpression': | ||
case 'FunctionExpression': | ||
case 'FunctionExpression': { | ||
return false; | ||
case 'NewExpression': | ||
} | ||
case 'NewExpression': { | ||
return !isNewExpressionWithParentheses(node, sourceCode); | ||
} | ||
case 'Literal': { | ||
/* istanbul ignore next */ | ||
/* c8 ignore next */ | ||
if (isDecimalIntegerNode(node)) { | ||
@@ -38,4 +42,5 @@ return true; | ||
default: | ||
default: { | ||
return true; | ||
} | ||
} | ||
@@ -42,0 +47,0 @@ } |
@@ -10,3 +10,3 @@ 'use strict'; | ||
'TemplateLiteral', | ||
'ThisExpression' | ||
'ThisExpression', | ||
]); | ||
@@ -13,0 +13,0 @@ |
@@ -17,3 +17,3 @@ 'use strict'; | ||
start: sourceCode.getLocFromIndex(start + startOffset), | ||
end: sourceCode.getLocFromIndex(end + endOffset) | ||
end: sourceCode.getLocFromIndex(end + endOffset), | ||
}; | ||
@@ -20,0 +20,0 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
647989
210
20575
2
15
28
218
+ Addedesquery@^1.5.0
+ Addedindent-string@^4.0.0
+ Addedjsesc@^3.0.2
+ Addedregjsparser@^0.10.0
+ Addedstrip-indent@^3.0.0
+ Addedindent-string@4.0.0(transitive)
+ Addedjsesc@0.5.0(transitive)
+ Addedmin-indent@1.0.1(transitive)
+ Addedregjsparser@0.10.0(transitive)
+ Addedstrip-indent@3.0.0(transitive)
- Removedeslint-template-visitor@^2.3.2
- Removedeslint-utils@^3.0.0
- Removedlodash@^4.17.21
- Removedreserved-words@^0.1.2
- Removedsafe-regex@^2.1.1
- Removed@ampproject/remapping@2.3.0(transitive)
- Removed@babel/compat-data@7.25.9(transitive)
- Removed@babel/core@7.25.9(transitive)
- Removed@babel/eslint-parser@7.25.9(transitive)
- Removed@babel/generator@7.25.9(transitive)
- Removed@babel/helper-compilation-targets@7.25.9(transitive)
- Removed@babel/helper-module-imports@7.25.9(transitive)
- Removed@babel/helper-module-transforms@7.25.9(transitive)
- Removed@babel/helper-simple-access@7.25.9(transitive)
- Removed@babel/helper-string-parser@7.25.9(transitive)
- Removed@babel/helper-validator-option@7.25.9(transitive)
- Removed@babel/helpers@7.25.9(transitive)
- Removed@babel/parser@7.25.9(transitive)
- Removed@babel/template@7.25.9(transitive)
- Removed@babel/traverse@7.25.9(transitive)
- Removed@babel/types@7.25.9(transitive)
- Removed@jridgewell/gen-mapping@0.3.5(transitive)
- Removed@jridgewell/resolve-uri@3.1.2(transitive)
- Removed@jridgewell/set-array@1.2.1(transitive)
- Removed@jridgewell/sourcemap-codec@1.5.0(transitive)
- Removed@jridgewell/trace-mapping@0.3.25(transitive)
- Removed@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1(transitive)
- Removedbrowserslist@4.24.2(transitive)
- Removedcaniuse-lite@1.0.30001669(transitive)
- Removedconvert-source-map@2.0.0(transitive)
- Removedelectron-to-chromium@1.5.43(transitive)
- Removedescalade@3.2.0(transitive)
- Removedeslint-scope@5.1.1(transitive)
- Removedeslint-template-visitor@2.3.2(transitive)
- Removedeslint-utils@3.0.0(transitive)
- Removedeslint-visitor-keys@2.1.0(transitive)
- Removedestraverse@4.3.0(transitive)
- Removedgensync@1.0.0-beta.2(transitive)
- Removedglobals@11.12.0(transitive)
- Removedjson5@2.2.3(transitive)
- Removedlodash@4.17.21(transitive)
- Removedlru-cache@5.1.1(transitive)
- Removedmultimap@1.1.0(transitive)
- Removednode-releases@2.0.18(transitive)
- Removedreserved-words@0.1.2(transitive)
- Removedsafe-regex@2.1.1(transitive)
- Removedsemver@6.3.1(transitive)
- Removedupdate-browserslist-db@1.1.1(transitive)
- Removedyallist@3.1.1(transitive)
Updatedci-info@^3.8.0
Updatedis-builtin-module@^3.2.1
Updatedregexp-tree@^0.1.27
Updatedsemver@^7.5.4