eslint-plugin-unicorn
Advanced tools
Comparing version 42.0.0 to 50.0.1
'use strict'; | ||
const {rules, ...baseConfigs} = require('./recommended.js'); | ||
const recommended = require('./recommended.js'); | ||
module.exports = { | ||
...baseConfigs, | ||
rules: Object.fromEntries(Object.entries(rules).map( | ||
([ruleId, severity]) => [ruleId, ruleId.startsWith('unicorn/') ? 'error' : severity], | ||
)), | ||
}; | ||
module.exports = Object.fromEntries(Object.entries(recommended).map( | ||
([ruleId, severity]) => [ruleId, ruleId.startsWith('unicorn/') ? 'error' : severity], | ||
)); |
'use strict'; | ||
module.exports = { | ||
env: { | ||
es2022: true, | ||
}, | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
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-await-expression-member': 'error', | ||
'unicorn/no-console-spaces': 'error', | ||
'unicorn/no-document-cookie': 'error', | ||
'unicorn/no-empty-file': 'error', | ||
'unicorn/no-for-loop': 'error', | ||
'unicorn/no-hex-escape': 'error', | ||
'unicorn/no-instanceof-array': 'error', | ||
'unicorn/no-invalid-remove-event-listener': '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-thenable': 'error', | ||
'unicorn/no-this-assignment': 'error', | ||
'unicorn/no-unreadable-array-destructuring': 'error', | ||
'unicorn/no-unreadable-iife': 'error', | ||
'unicorn/no-unsafe-regex': 'off', | ||
'unicorn/no-unused-properties': 'off', | ||
'unicorn/no-useless-fallback-in-spread': 'error', | ||
'unicorn/no-useless-length-check': 'error', | ||
'unicorn/no-useless-promise-resolve-reject': 'error', | ||
'unicorn/no-useless-spread': 'error', | ||
'unicorn/no-useless-switch-case': 'error', | ||
'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-code-point': 'error', | ||
'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-export-from': 'error', | ||
'unicorn/prefer-includes': 'error', | ||
'unicorn/prefer-json-parse-buffer': 'off', | ||
'unicorn/prefer-keyboard-event-key': 'error', | ||
'unicorn/prefer-math-trunc': 'error', | ||
'unicorn/prefer-modern-dom-apis': 'error', | ||
'unicorn/prefer-modern-math-apis': 'error', | ||
'unicorn/prefer-module': 'error', | ||
'unicorn/prefer-native-coercion-functions': 'error', | ||
'unicorn/prefer-negative-index': 'error', | ||
'unicorn/prefer-node-protocol': 'error', | ||
'unicorn/prefer-number-properties': 'error', | ||
'unicorn/prefer-object-from-entries': 'error', | ||
'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/relative-url-style': 'error', | ||
'unicorn/require-array-join-separator': 'error', | ||
'unicorn/require-number-to-fixed-digits-argument': 'error', | ||
// Turned off because we can't distinguish `widow.postMessage` and `{Worker,MessagePort,Client,BroadcastChannel}#postMessage()` | ||
// See #1396 | ||
'unicorn/require-post-message-target-origin': 'off', | ||
'unicorn/string-content': 'off', | ||
'unicorn/template-indent': 'error', | ||
'unicorn/text-encoding-identifier-case': 'error', | ||
'unicorn/throw-new-error': 'error', | ||
}, | ||
'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-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-await-expression-member': 'error', | ||
'unicorn/no-console-spaces': 'error', | ||
'unicorn/no-document-cookie': 'error', | ||
'unicorn/no-empty-file': 'error', | ||
'unicorn/no-for-loop': 'error', | ||
'unicorn/no-hex-escape': 'error', | ||
'unicorn/no-instanceof-array': 'error', | ||
'unicorn/no-invalid-remove-event-listener': 'error', | ||
'unicorn/no-keyword-prefix': 'off', | ||
'unicorn/no-lonely-if': 'error', | ||
'no-negated-condition': 'off', | ||
'unicorn/no-negated-condition': '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-thenable': 'error', | ||
'unicorn/no-this-assignment': 'error', | ||
'unicorn/no-typeof-undefined': 'error', | ||
'unicorn/no-unnecessary-await': 'error', | ||
'unicorn/no-unnecessary-polyfills': 'error', | ||
'unicorn/no-unreadable-array-destructuring': 'error', | ||
'unicorn/no-unreadable-iife': 'error', | ||
'unicorn/no-unused-properties': 'off', | ||
'unicorn/no-useless-fallback-in-spread': 'error', | ||
'unicorn/no-useless-length-check': 'error', | ||
'unicorn/no-useless-promise-resolve-reject': 'error', | ||
'unicorn/no-useless-spread': 'error', | ||
'unicorn/no-useless-switch-case': 'error', | ||
'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': 'error', | ||
'unicorn/prefer-blob-reading-methods': 'error', | ||
'unicorn/prefer-code-point': 'error', | ||
'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-event-target': 'error', | ||
'unicorn/prefer-export-from': 'error', | ||
'unicorn/prefer-includes': 'error', | ||
'unicorn/prefer-json-parse-buffer': 'off', | ||
'unicorn/prefer-keyboard-event-key': 'error', | ||
'unicorn/prefer-logical-operator-over-ternary': 'error', | ||
'unicorn/prefer-math-trunc': 'error', | ||
'unicorn/prefer-modern-dom-apis': 'error', | ||
'unicorn/prefer-modern-math-apis': 'error', | ||
'unicorn/prefer-module': 'error', | ||
'unicorn/prefer-native-coercion-functions': 'error', | ||
'unicorn/prefer-negative-index': 'error', | ||
'unicorn/prefer-node-protocol': 'error', | ||
'unicorn/prefer-number-properties': 'error', | ||
'unicorn/prefer-object-from-entries': 'error', | ||
'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-set-size': 'error', | ||
'unicorn/prefer-spread': 'error', | ||
'unicorn/prefer-string-replace-all': 'error', | ||
'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': 'error', | ||
'unicorn/prefer-type-error': 'error', | ||
'unicorn/prevent-abbreviations': 'error', | ||
'unicorn/relative-url-style': 'error', | ||
'unicorn/require-array-join-separator': 'error', | ||
'unicorn/require-number-to-fixed-digits-argument': 'error', | ||
// Turned off because we can't distinguish `widow.postMessage` and `{Worker,MessagePort,Client,BroadcastChannel}#postMessage()` | ||
// See #1396 | ||
'unicorn/require-post-message-target-origin': 'off', | ||
'unicorn/string-content': 'off', | ||
'unicorn/switch-case-braces': 'error', | ||
'unicorn/template-indent': 'error', | ||
'unicorn/text-encoding-identifier-case': 'error', | ||
'unicorn/throw-new-error': 'error', | ||
}; |
34
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 legacyConfigBase = require('./configs/legacy-config-base.js'); | ||
const flatConfigBase = require('./configs/flat-config-base.js'); | ||
const recommendedRules = require('./configs/recommended.js'); | ||
const allRules = 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', | ||
@@ -26,3 +31,13 @@ 'prefer-event-key': 'unicorn/prefer-keyboard-event-key', | ||
module.exports = { | ||
const createConfig = (rules, isLegacyConfig = false) => ({ | ||
...(isLegacyConfig ? legacyConfigBase : flatConfigBase), | ||
plugins: isLegacyConfig ? ['unicorn'] : {unicorn}, | ||
rules, | ||
}); | ||
const unicorn = { | ||
meta: { | ||
name, | ||
version, | ||
}, | ||
rules: { | ||
@@ -32,6 +47,11 @@ ...loadRules(), | ||
}, | ||
configs: { | ||
recommended: recommendedConfig, | ||
all: allRulesEnabledConfig, | ||
}, | ||
}; | ||
const configs = { | ||
recommended: createConfig(recommendedRules, /* isLegacyConfig */ true), | ||
all: createConfig(allRules, /* isLegacyConfig */ true), | ||
'flat/recommended': createConfig(recommendedRules), | ||
'flat/all': createConfig(allRules), | ||
}; | ||
module.exports = {...unicorn, configs}; |
101
package.json
{ | ||
"name": "eslint-plugin-unicorn", | ||
"version": "42.0.0", | ||
"description": "Various awesome ESLint rules", | ||
"version": "50.0.1", | ||
"description": "More than 100 powerful ESLint rules", | ||
"license": "MIT", | ||
@@ -13,18 +13,21 @@ "repository": "sindresorhus/eslint-plugin-unicorn", | ||
}, | ||
"main": "index.js", | ||
"sideEffects": false, | ||
"engines": { | ||
"node": ">=12" | ||
"node": ">=16" | ||
}, | ||
"scripts": { | ||
"create-rule": "node ./scripts/create-rule.mjs && npm run generate-rule-notices && npm run generate-rules-table", | ||
"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", | ||
"generate-rule-notices": "node ./scripts/generate-rule-notices.mjs", | ||
"generate-rules-table": "node ./scripts/generate-rules-table.mjs", | ||
"fix:markdown": "npm run lint:markdown -- --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:markdown": "markdownlint \"**/*.md\"", | ||
"lint:package-json": "npmPkgJsonLint .", | ||
"run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.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", | ||
@@ -50,47 +53,51 @@ "test": "npm-run-all --continue-on-error lint test:*", | ||
"dependencies": { | ||
"@babel/helper-validator-identifier": "^7.15.7", | ||
"ci-info": "^3.3.0", | ||
"@babel/helper-validator-identifier": "^7.22.20", | ||
"@eslint-community/eslint-utils": "^4.4.0", | ||
"@eslint/eslintrc": "^2.1.4", | ||
"ci-info": "^4.0.0", | ||
"clean-regexp": "^1.0.0", | ||
"eslint-utils": "^3.0.0", | ||
"esquery": "^1.4.0", | ||
"core-js-compat": "^3.34.0", | ||
"esquery": "^1.5.0", | ||
"indent-string": "^4.0.0", | ||
"is-builtin-module": "^3.1.0", | ||
"lodash": "^4.17.21", | ||
"is-builtin-module": "^3.2.1", | ||
"jsesc": "^3.0.2", | ||
"pluralize": "^8.0.0", | ||
"read-pkg-up": "^7.0.1", | ||
"regexp-tree": "^0.1.24", | ||
"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.16.0", | ||
"@babel/core": "^7.16.5", | ||
"@babel/eslint-parser": "^7.16.5", | ||
"@babel/code-frame": "^7.23.5", | ||
"@babel/core": "^7.23.6", | ||
"@babel/eslint-parser": "^7.23.3", | ||
"@lubien/fixture-beta-package": "^1.0.0-beta.1", | ||
"@typescript-eslint/parser": "^5.7.0", | ||
"ava": "^3.15.0", | ||
"c8": "^7.11.0", | ||
"chalk": "^5.0.0", | ||
"enquirer": "^2.3.6", | ||
"eslint": "^8.8.0", | ||
"eslint-ava-rule-tester": "^4.0.0", | ||
"eslint-plugin-eslint-plugin": "^4.1.0", | ||
"@typescript-eslint/parser": "^6.15.0", | ||
"ava": "^6.0.1", | ||
"c8": "^8.0.1", | ||
"chalk": "^5.3.0", | ||
"enquirer": "^2.4.1", | ||
"eslint": "^8.56.0", | ||
"eslint-ava-rule-tester": "^4.2.0", | ||
"eslint-doc-generator": "^1.6.1", | ||
"eslint-plugin-eslint-plugin": "^5.2.1", | ||
"eslint-plugin-internal-rules": "file:./scripts/internal-rules/", | ||
"eslint-remote-tester": "^2.0.1", | ||
"eslint-remote-tester-repositories": "^0.0.3", | ||
"execa": "^6.0.0", | ||
"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", | ||
"markdownlint-cli": "^0.30.0", | ||
"mem": "^9.0.1", | ||
"npm-package-json-lint": "^5.4.2", | ||
"npm-run-all": "^4.1.5", | ||
"markdownlint-cli": "^0.38.0", | ||
"memoize": "^10.0.0", | ||
"npm-package-json-lint": "^7.1.0", | ||
"npm-run-all2": "^6.1.1", | ||
"outdent": "^0.8.0", | ||
"typescript": "^4.5.4", | ||
"vue-eslint-parser": "^8.0.1", | ||
"xo": "^0.48.0" | ||
"typescript": "^5.3.3", | ||
"vue-eslint-parser": "^9.3.2", | ||
"xo": "^0.56.0", | ||
"yaml": "^2.3.4" | ||
}, | ||
"peerDependencies": { | ||
"eslint": ">=8.8.0" | ||
"eslint": ">=8.56.0" | ||
}, | ||
@@ -110,8 +117,13 @@ "ava": { | ||
"xo": { | ||
"extends": [ | ||
"plugin:internal-rules/all" | ||
], | ||
"ignores": [ | ||
".cache-eslint-remote-tester", | ||
"eslint-remote-tester-results", | ||
"rules/utils/lodash.js", | ||
"test/integration/{fixtures,fixtures-local}/**" | ||
], | ||
"rules": { | ||
"unicorn/expiring-todo-comments": "off", | ||
"unicorn/no-null": "error", | ||
@@ -126,3 +138,4 @@ "unicorn/prefer-array-flat": [ | ||
} | ||
] | ||
], | ||
"import/order": "off" | ||
}, | ||
@@ -163,10 +176,2 @@ "overrides": [ | ||
} | ||
}, | ||
{ | ||
"files": [ | ||
"rules/**/*.js" | ||
], | ||
"extends": [ | ||
"plugin:internal-rules/all" | ||
] | ||
} | ||
@@ -173,0 +178,0 @@ ] |
386
readme.md
# 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"> | ||
<img src="https://cloud.githubusercontent.com/assets/170270/18659176/1cc373d0-7f33-11e6-890f-0ba35362ee7e.jpg" width="180" align="right" alt="Unicorn"> | ||
> Various awesome ESLint rules | ||
> More than 100 powerful ESLint rules | ||
@@ -18,6 +18,63 @@ You might want to check out [XO](https://github.com/xojs/xo), which includes this plugin. | ||
## Usage | ||
## Usage (`eslint.config.js`) | ||
Use a [preset config](#preset-configs) or configure each rules in `package.json`. | ||
**Requires ESLint `>=8.23.0`.** | ||
Use a [preset config](#preset-configs-eslintconfigjs) or configure each rule in `eslint.config.js`. | ||
If you don't use the preset, ensure you use the same `languageOptions` config as below. | ||
### ES Module (Recommended) | ||
```js | ||
import eslintPluginUnicorn from 'eslint-plugin-unicorn'; | ||
import * as eslintrc from '@eslint/eslintrc'; | ||
export default [ | ||
{ | ||
languageOptions: { | ||
globals: eslintrc.Legacy.environments.get('es2024'), | ||
}, | ||
plugins: { | ||
unicorn: eslintPluginUnicorn, | ||
}, | ||
rules: { | ||
'unicorn/better-regex': 'error', | ||
'unicorn/…': 'error', | ||
}, | ||
}, | ||
// … | ||
]; | ||
``` | ||
### CommonJS | ||
```js | ||
'use strict'; | ||
const eslintPluginUnicorn = require('eslint-plugin-unicorn'); | ||
const eslintrc = require('@eslint/eslintrc'); | ||
module.exports = [ | ||
{ | ||
languageOptions: { | ||
globals: eslintrc.Legacy.environments.get('es2024'), | ||
}, | ||
plugins: { | ||
unicorn: eslintPluginUnicorn, | ||
}, | ||
rules: { | ||
'unicorn/better-regex': 'error', | ||
'unicorn/…': 'error', | ||
}, | ||
}, | ||
// … | ||
]; | ||
``` | ||
## Usage (legacy: `.eslintrc.*` or `package.json`) | ||
Use a [preset config](#preset-configs-eslintrc-or-packagejson) or configure each rule in `package.json`. | ||
If you don't use the preset, ensure you use the same `env` and `parserOptions` config as below. | ||
```json | ||
@@ -28,3 +85,3 @@ { | ||
"env": { | ||
"es2022": true | ||
"es2024": true | ||
}, | ||
@@ -48,117 +105,125 @@ "parserOptions": { | ||
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 RULES_TABLE part. Run: `npm run generate-rules-table` --> | ||
<!-- RULES_TABLE --> | ||
| 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-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-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-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-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-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(…)` 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-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-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-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` 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-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-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` 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. | | 🔧 | 💡 | | ||
| [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. | ✅ | 🔧 | | | ||
<!-- /RULES_TABLE --> | ||
| 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-unnecessary-polyfills](docs/rules/no-unnecessary-polyfills.md) | Enforce the use of built-in methods instead of unnecessary polyfills. | ✅ | | | | ||
| [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. | ✅ | 🔧 | | | ||
<!-- end auto-generated rules list --> | ||
### Deprecated Rules | ||
@@ -168,4 +233,84 @@ | ||
## Preset configs | ||
## Preset configs (`eslint.config.js`) | ||
See the [ESLint docs](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new) for more information about extending config files. | ||
**Note**: Preset configs will also enable the correct [language options](https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-language-options). | ||
### Recommended config | ||
This plugin exports a [`recommended` config](configs/recommended.js) that enforces good practices. | ||
#### ES Module (Recommended) | ||
```js | ||
import eslintPluginUnicorn from 'eslint-plugin-unicorn'; | ||
export default [ | ||
// … | ||
eslintPluginUnicorn.config['flat/recommended'], | ||
{ | ||
rules: { | ||
'unicorn/better-regex': 'warn', | ||
}, | ||
}, | ||
]; | ||
``` | ||
#### CommonJS | ||
```js | ||
'use strict'; | ||
const eslintPluginUnicorn = require('eslint-plugin-unicorn'); | ||
module.exports = [ | ||
// … | ||
eslintPluginUnicorn.config['flat/recommended'], | ||
{ | ||
rules: { | ||
'unicorn/better-regex': 'warn', | ||
}, | ||
}, | ||
]; | ||
``` | ||
### All config | ||
This plugin exports an [`all` config](configs/all.js) that makes use of all rules (except for deprecated ones). | ||
#### ES Module (Recommended) | ||
```js | ||
import eslintPluginUnicorn from 'eslint-plugin-unicorn'; | ||
export default [ | ||
// … | ||
eslintPluginUnicorn.config['flat/all'], | ||
{ | ||
rules: { | ||
'unicorn/better-regex': 'warn', | ||
}, | ||
}, | ||
]; | ||
``` | ||
#### CommonJS | ||
```js | ||
'use strict'; | ||
const eslintPluginUnicorn = require('eslint-plugin-unicorn'); | ||
module.exports = [ | ||
// … | ||
eslintPluginUnicorn.config['flat/all'], | ||
{ | ||
rules: { | ||
'unicorn/better-regex': 'warn', | ||
}, | ||
}, | ||
]; | ||
``` | ||
## Preset configs (`.eslintrc.*` or `package.json`) | ||
See the [ESLint docs](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending config files. | ||
@@ -183,3 +328,6 @@ | ||
"eslintConfig": { | ||
"extends": "plugin:unicorn/recommended" | ||
"extends": "plugin:unicorn/recommended", | ||
"rules": { | ||
"unicorn/better-regex": "warn" | ||
} | ||
} | ||
@@ -186,0 +334,0 @@ } |
'use strict'; | ||
const { | ||
isLiteral, | ||
isStringLiteral, | ||
isNumberLiteral, | ||
isBigIntLiteral, | ||
isNullLiteral, | ||
isRegexLiteral, | ||
} = require('./literal.js'); | ||
const { | ||
isNewExpression, | ||
isCallExpression, | ||
isCallOrNewExpression, | ||
} = require('./call-or-new-expression.js'); | ||
module.exports = { | ||
isLiteral, | ||
isStringLiteral, | ||
isNumberLiteral, | ||
isBigIntLiteral, | ||
isNullLiteral, | ||
isRegexLiteral, | ||
isArrowFunctionBody: require('./is-arrow-function-body.js'), | ||
isCallExpression, | ||
isCallOrNewExpression, | ||
isEmptyNode: require('./is-empty-node.js'), | ||
isExpressionStatement: require('./is-expression-statement.js'), | ||
isFunction: require('./is-function.js'), | ||
isMemberExpression: require('./is-member-expression.js'), | ||
isMethodCall: require('./is-method-call.js'), | ||
isNewExpression, | ||
isReferenceIdentifier: require('./is-reference-identifier.js'), | ||
isStaticRequire: require('./is-static-require.js'), | ||
isUndefined: require('./is-undefined.js'), | ||
functionTypes: require('./function-types.js'), | ||
}; |
@@ -13,3 +13,3 @@ 'use strict'; | ||
if (additionalEmpty && additionalEmpty(node)) { | ||
if (additionalEmpty?.(node)) { | ||
return true; | ||
@@ -16,0 +16,0 @@ } |
'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_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}', | ||
}; | ||
const newRegExp = [ | ||
newExpressionSelector({name: 'RegExp', minimumArguments: 1}), | ||
'[arguments.0.type="Literal"]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
@@ -28,8 +25,11 @@ const create = context => { | ||
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; | ||
@@ -45,2 +45,3 @@ } | ||
node, | ||
messageId: MESSAGE_ID_PARSE_ERROR, | ||
data: { | ||
@@ -50,3 +51,2 @@ original, | ||
}, | ||
message: 'Problem parsing {{original}}: {{error}}', | ||
}; | ||
@@ -59,3 +59,3 @@ } | ||
return { | ||
const problem = { | ||
node, | ||
@@ -67,9 +67,30 @@ messageId: MESSAGE_ID, | ||
}, | ||
}; | ||
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; | ||
@@ -79,7 +100,3 @@ } | ||
const oldPattern = patternNode.value; | ||
const flags = ( | ||
flagsNode | ||
&& flagsNode.type === 'Literal' | ||
&& typeof flagsNode.value === 'string' | ||
) | ||
const flags = isStringLiteral(flagsNode) | ||
? flagsNode.value | ||
@@ -100,3 +117,3 @@ : ''; | ||
patternNode, | ||
quoteString(newPattern, patternNode.raw.charAt(0)), | ||
escapeString(newPattern, patternNode.raw.charAt(0)), | ||
), | ||
@@ -103,0 +120,0 @@ }; |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
const {renameVariable} = require('./fix/index.js'); | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -12,24 +12,24 @@ const MESSAGE_ID = 'catch-error-name'; | ||
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({method: 'then', argumentsLength: 2}), | ||
methodCallSelector({method: 'catch', argumentsLength: 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; | ||
@@ -54,3 +54,10 @@ /** @param {import('eslint').Rule.RuleContext} context */ | ||
return { | ||
[selector](node) { | ||
Identifier(node) { | ||
if ( | ||
!(node.parent.type === 'CatchClause' && node.parent.param === node) | ||
&& !isPromiseCatchParameter(node) | ||
) { | ||
return; | ||
} | ||
const originalName = node.name; | ||
@@ -60,3 +67,3 @@ | ||
isNameAllowed(originalName) | ||
|| isNameAllowed(originalName.replace(/_+$/g, '')) | ||
|| isNameAllowed(originalName.replaceAll(/_+$/g, '')) | ||
) { | ||
@@ -66,3 +73,3 @@ return; | ||
const scope = context.getScope(); | ||
const scope = context.sourceCode.getScope(node); | ||
const variable = findVariable(scope, node); | ||
@@ -69,0 +76,0 @@ |
'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 => { | ||
@@ -57,21 +41,37 @@ while (expression) { | ||
const create = context => { | ||
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), | ||
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) { | ||
@@ -82,3 +82,3 @@ return; | ||
const {scope, objectPattern} = declaration; | ||
const memberScope = context.getScope(); | ||
const memberScope = sourceCode.getScope(node); | ||
@@ -95,8 +95,8 @@ // Property is destructured outside the current scope | ||
); | ||
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); | ||
@@ -141,3 +141,3 @@ // Member might already be destructured | ||
const {properties} = objectPattern; | ||
const lastProperty = properties[properties.length - 1]; | ||
const lastProperty = properties.at(-1); | ||
@@ -144,0 +144,0 @@ yield fixer.replaceText(node, newMember); |
'use strict'; | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils'); | ||
const getReferences = require('./utils/get-references.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils'); | ||
const { | ||
getReferences, | ||
isNodeMatches, | ||
} = require('./utils/index.js'); | ||
const { | ||
functionTypes, | ||
} = require('./ast/index.js'); | ||
@@ -24,3 +29,3 @@ const MESSAGE_ID = 'consistent-function-scoping'; | ||
// 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; | ||
@@ -96,5 +101,3 @@ } | ||
const isReactHook = scope => | ||
scope.block | ||
&& scope.block.parent | ||
&& scope.block.parent.callee | ||
scope.block?.parent?.callee | ||
&& isNodeMatches(scope.block.parent.callee, reactHooks); | ||
@@ -104,4 +107,3 @@ | ||
scope.type === 'function' | ||
&& scope.block | ||
&& scope.block.type === 'ArrowFunctionExpression' | ||
&& scope.block?.type === 'ArrowFunctionExpression' | ||
&& (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope))); | ||
@@ -113,5 +115,4 @@ | ||
]); | ||
const isIife = node => node | ||
&& iifeFunctionTypes.has(node.type) | ||
&& node.parent | ||
const isIife = node => | ||
iifeFunctionTypes.has(node.type) | ||
&& node.parent.type === 'CallExpression' | ||
@@ -160,3 +161,3 @@ && node.parent.callee === node; | ||
const {checkArrowFunctions} = {checkArrowFunctions: true, ...context.options[0]}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager} = sourceCode; | ||
@@ -166,37 +167,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), | ||
}, | ||
}; | ||
}); | ||
}; | ||
@@ -203,0 +204,0 @@ |
'use strict'; | ||
const {upperFirst} = require('lodash'); | ||
const {upperFirst} = require('./utils/lodash.js'); | ||
@@ -140,5 +140,5 @@ const MESSAGE_ID_INVALID_EXPORT = 'invalidExport'; | ||
if (!nameProperty || !nameProperty.value || nameProperty.value.value !== name) { | ||
if (!nameProperty?.value || nameProperty.value.value !== name) { | ||
yield { | ||
node: nameProperty && nameProperty.value ? nameProperty.value : constructorBodyNode, | ||
node: nameProperty?.value ?? constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.`, | ||
@@ -149,3 +149,3 @@ }; | ||
yield { | ||
node: nameExpression ? nameExpression.expression.right : constructorBodyNode, | ||
node: nameExpression?.expression.right ?? constructorBodyNode, | ||
message: `The \`name\` property should be set to \`${name}\`.`, | ||
@@ -188,7 +188,19 @@ }; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
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), | ||
}); | ||
const create = context => { | ||
context.on('ClassDeclaration', node => customErrorDefinition(context, node)); | ||
context.on('AssignmentExpression', node => { | ||
if (node.right.type === 'ClassExpression') { | ||
return customErrorDefinition(context, node.right); | ||
} | ||
}); | ||
context.on('AssignmentExpression', node => { | ||
if ( | ||
node.left.type === 'MemberExpression' | ||
&& node.left.object.type === 'Identifier' | ||
&& node.left.object.name === 'exports' | ||
) { | ||
return customErrorExport(context, node); | ||
} | ||
}); | ||
}; | ||
@@ -195,0 +207,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {isOpeningBraceToken} = require('eslint-utils'); | ||
const {matches} = require('./selectors/index.js'); | ||
const {isOpeningBraceToken} = require('@eslint-community/eslint-utils'); | ||
@@ -10,38 +9,53 @@ const MESSAGE_ID = 'empty-brace-spaces'; | ||
const selector = matches([ | ||
'BlockStatement[body.length=0]', | ||
'ClassBody[body.length=0]', | ||
'ObjectExpression[properties.length=0]', | ||
'StaticBlock[body.length=0]', | ||
// Experimental https://github.com/tc39/proposal-record-tuple | ||
'RecordExpression[properties.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 => ({ | ||
[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); | ||
const create = context => { | ||
context.on([ | ||
'BlockStatement', | ||
'ClassBody', | ||
'StaticBlock', | ||
], node => { | ||
if (node.body.length > 0) { | ||
return; | ||
} | ||
if (!/^\s+$/.test(textBetween)) { | ||
return getProblem(node, context); | ||
}); | ||
context.on([ | ||
'ObjectExpression', | ||
// Experimental https://github.com/tc39/proposal-record-tuple | ||
'RecordExpression', | ||
], node => { | ||
if (node.properties.length > 0) { | ||
return; | ||
} | ||
return { | ||
loc: { | ||
start: openingBrace.loc.end, | ||
end: closingBrace.loc.start, | ||
}, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => fixer.removeRange([start, end]), | ||
}; | ||
}, | ||
}); | ||
return getProblem(node, context); | ||
}); | ||
}; | ||
@@ -48,0 +62,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const {callOrNewExpressionSelector} = require('./selectors/index.js'); | ||
const {isCallOrNewExpression} = require('./ast/index.js'); | ||
@@ -15,3 +15,3 @@ const MESSAGE_ID_MISSING_MESSAGE = 'missing-message'; | ||
const selector = callOrNewExpressionSelector([ | ||
const builtinErrors = [ | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error | ||
@@ -27,11 +27,19 @@ 'Error', | ||
'AggregateError', | ||
]); | ||
]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](expression) { | ||
if (isShadowed(context.getScope(), expression.callee)) { | ||
const create = context => { | ||
context.on(['CallExpression', 'NewExpression'], expression => { | ||
if (!isCallOrNewExpression(expression, { | ||
names: builtinErrors, | ||
optional: false, | ||
})) { | ||
return; | ||
} | ||
const scope = context.sourceCode.getScope(expression); | ||
if (isShadowed(scope, expression.callee)) { | ||
return; | ||
} | ||
const constructorName = expression.callee.name; | ||
@@ -64,3 +72,3 @@ const messageArgumentIndex = constructorName === 'AggregateError' ? 1 : 0; | ||
const staticResult = getStaticValue(node, context.getScope()); | ||
const staticResult = getStaticValue(node, scope); | ||
@@ -86,4 +94,4 @@ // We don't know the value of `message` | ||
} | ||
}, | ||
}); | ||
}); | ||
}; | ||
@@ -90,0 +98,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {isRegexLiteral, isStringLiteral} = require('./ast/index.js'); | ||
@@ -24,29 +25,29 @@ const MESSAGE_ID = 'escape-case'; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
Literal(node) { | ||
if (typeof node.value !== 'string') { | ||
return; | ||
const create = context => { | ||
context.on('Literal', node => { | ||
if (isStringLiteral(node)) { | ||
return getProblem({ | ||
node, | ||
original: node.raw, | ||
}); | ||
} | ||
}); | ||
return getProblem({ | ||
node, | ||
original: node.raw, | ||
}); | ||
}, | ||
'Literal[regex]'(node) { | ||
return getProblem({ | ||
node, | ||
original: node.raw, | ||
regex: escapePatternWithLowercase, | ||
}); | ||
}, | ||
TemplateElement(node) { | ||
return getProblem({ | ||
node, | ||
original: node.value.raw, | ||
fix: (fixer, fixed) => replaceTemplateElement(fixer, node, fixed), | ||
}); | ||
}, | ||
}); | ||
context.on('Literal', node => { | ||
if (isRegexLiteral(node)) { | ||
return getProblem({ | ||
node, | ||
original: node.raw, | ||
regex: escapePatternWithLowercase, | ||
}); | ||
} | ||
}); | ||
context.on('TemplateElement', node => getProblem({ | ||
node, | ||
original: node.value.raw, | ||
fix: (fixer, fixed) => replaceTemplateElement(fixer, node, fixed), | ||
})); | ||
}; | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
@@ -53,0 +54,0 @@ module.exports = { |
'use strict'; | ||
const path = require('node:path'); | ||
const readPkgUp = require('read-pkg-up'); | ||
@@ -50,87 +51,67 @@ const semver = require('semver'); | ||
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, | ||
}, | ||
@@ -140,52 +121,89 @@ }; | ||
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}; | ||
} | ||
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) { | ||
@@ -265,3 +283,6 @@ return Date.parse(past) < Date.parse(now); | ||
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(); | ||
@@ -287,13 +308,14 @@ const unusedComments = comments | ||
// 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); | ||
@@ -493,3 +515,3 @@ | ||
const withoutWhitespace = unknown.replace(/ /g, ''); | ||
const withoutWhitespace = unknown.replaceAll(' ', ''); | ||
@@ -496,0 +518,0 @@ if (parseArgument(withoutWhitespace).type !== 'unknowns') { |
'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'); | ||
@@ -22,7 +21,7 @@ const TYPE_NON_ZERO = 'non-zero'; | ||
&& node.operator === operator | ||
&& isLiteralValue(node.right, value); | ||
&& isLiteral(node.right, value); | ||
const isCompareLeft = (node, operator, value) => | ||
node.type === 'BinaryExpression' | ||
&& node.operator === operator | ||
&& isLiteralValue(node.left, value); | ||
&& isLiteral(node.left, value); | ||
const nonZeroStyles = new Map([ | ||
@@ -49,4 +48,2 @@ [ | ||
const lengthSelector = memberExpressionSelector(['length', 'size']); | ||
function getLengthCheckNode(node) { | ||
@@ -98,2 +95,11 @@ node = node.parent; | ||
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) { | ||
@@ -105,3 +111,3 @@ const options = { | ||
const nonZeroStyle = nonZeroStyles.get(options['non-zero']); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -140,3 +146,2 @@ function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) { | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: problem.data, | ||
fix, | ||
@@ -151,8 +156,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)) { | ||
@@ -177,3 +189,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; | ||
@@ -180,0 +198,0 @@ node = lengthNode; |
'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'); | ||
@@ -110,3 +110,3 @@ | ||
if (lastWord && lastWord.ignored === isIgnored) { | ||
if (lastWord?.ignored === isIgnored) { | ||
lastWord.word += char; | ||
@@ -148,6 +148,6 @@ } else { | ||
const chosenCasesFunctions = chosenCases.map(case_ => ignoreNumbers(cases[case_].fn)); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') { | ||
return {}; | ||
return; | ||
} | ||
@@ -154,0 +154,0 @@ |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
@@ -4,0 +4,0 @@ function appendArgument(fixer, node, text, sourceCode) { |
@@ -22,2 +22,3 @@ 'use strict'; | ||
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; | ||
@@ -13,0 +13,0 @@ let [start] = firstToken.range; |
'use strict'; | ||
function removeSpacesAfter(indexOrNode, sourceCode, fixer) { | ||
let index = indexOrNode; | ||
if (typeof indexOrNode === 'object' && Array.isArray(indexOrNode.range)) { | ||
index = indexOrNode.range[1]; | ||
function removeSpacesAfter(indexOrNodeOrToken, sourceCode, fixer) { | ||
let index = indexOrNodeOrToken; | ||
if (typeof indexOrNodeOrToken === 'object' && Array.isArray(indexOrNodeOrToken.range)) { | ||
index = indexOrNodeOrToken.range[1]; | ||
} | ||
@@ -8,0 +8,0 @@ |
'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 {callExpressionSelector} = require('./selectors/index.js'); | ||
const {defaultsDeep} = require('./utils/lodash.js'); | ||
const {getStringIfConstant} = require('@eslint-community/eslint-utils'); | ||
const {isCallExpression} = require('./ast/index.js'); | ||
@@ -106,16 +106,8 @@ const MESSAGE_ID = 'importStyle'; | ||
const joinOr = words => 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 | ||
@@ -134,15 +126,2 @@ const defaultStyles = { | ||
const assignedDynamicImportSelector = [ | ||
'VariableDeclarator', | ||
'[init.type="AwaitExpression"]', | ||
'[init.argument.type="ImportExpression"]', | ||
].join(''); | ||
const assignedRequireSelector = [ | ||
'VariableDeclarator', | ||
'[init.type="CallExpression"]', | ||
'[init.callee.type="Identifier"]', | ||
'[init.callee.name="require"]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
@@ -172,2 +151,4 @@ const create = context => { | ||
const {sourceCode} = context; | ||
const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => { | ||
@@ -194,3 +175,3 @@ if (!allowedImportStyles || allowedImportStyles.size === 0) { | ||
const data = { | ||
allowedStyles: joinOr([...allowedImportStyles.keys()]), | ||
allowedStyles: new Intl.ListFormat('en-US', {type: 'disjunction'}).format([...allowedImportStyles.keys()]), | ||
moduleName, | ||
@@ -206,102 +187,113 @@ }; | ||
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); | ||
}); | ||
[assignedDynamicImportSelector](node) { | ||
const assignmentTargetNode = node.id; | ||
const moduleNameNode = node.init.argument.source; | ||
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', argumentsLength: 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); | ||
}); | ||
[assignedRequireSelector](node) { | ||
const assignmentTargetNode = node.id; | ||
const moduleNameNode = node.init.arguments[0]; | ||
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 visitor; | ||
}; | ||
@@ -308,0 +300,0 @@ |
'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 { | ||
@@ -15,53 +14,58 @@ switchCallExpressionToNewExpression, | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
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 => switchCallExpressionToNewExpression(node, sourceCode, fixer), | ||
}; | ||
}, | ||
[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); | ||
}, | ||
@@ -68,0 +72,0 @@ }; |
'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'); | ||
@@ -17,101 +17,124 @@ const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name'; | ||
const iteratorMethods = [ | ||
[ | ||
'every', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'filter', { | ||
extraSelector: '[callee.object.name!="Vue"]', | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'find', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'findIndex', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'flatMap', | ||
], | ||
[ | ||
'forEach', | ||
{ | ||
returnsUndefined: true, | ||
}, | ||
], | ||
[ | ||
'map', | ||
{ | ||
extraSelector: '[callee.object.name!="types"]', | ||
ignore: [ | ||
'String', | ||
'Number', | ||
'BigInt', | ||
'Boolean', | ||
'Symbol', | ||
], | ||
}, | ||
], | ||
[ | ||
'reduce', | ||
{ | ||
parameters: [ | ||
'accumulator', | ||
'element', | ||
'index', | ||
'array', | ||
], | ||
minParameters: 2, | ||
}, | ||
], | ||
[ | ||
'reduceRight', | ||
{ | ||
parameters: [ | ||
'accumulator', | ||
'element', | ||
'index', | ||
'array', | ||
], | ||
minParameters: 2, | ||
}, | ||
], | ||
[ | ||
'some', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
].map(([method, options]) => { | ||
options = { | ||
parameters: ['element', 'index', 'array'], | ||
ignore: [], | ||
minParameters: 1, | ||
extraSelector: '', | ||
returnsUndefined: false, | ||
...options, | ||
}; | ||
return [method, options]; | ||
}); | ||
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; | ||
} | ||
if (isNodeMatches(node.callee.object, ignoredCallee)) { | ||
return false; | ||
} | ||
if (node.callee.object.type === 'CallExpression' && isNodeMatches(node.callee.object.callee, ignoredCallee)) { | ||
return false; | ||
} | ||
const [callback] = node.arguments; | ||
if (callback.type === 'Identifier' && ignore.includes(callback.name)) { | ||
return false; | ||
} | ||
return !test || test(node); | ||
}, | ||
}])); | ||
const ignoredCallee = [ | ||
@@ -137,6 +160,2 @@ // http://bluebirdjs.com/docs/api/promise.map.html | ||
if (type === 'Identifier' && options.ignore.includes(name)) { | ||
return; | ||
} | ||
const problem = { | ||
@@ -163,3 +182,3 @@ node, | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
let nodeText = sourceCode.getText(node); | ||
@@ -185,43 +204,46 @@ if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') { | ||
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 => { | ||
const rules = {}; | ||
for (const [method, options] of iteratorMethods) { | ||
const selector = [ | ||
method === 'reduce' || method === 'reduceRight' ? '' : ':not(AwaitExpression) > ', | ||
methodCallSelector({ | ||
method, | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
}), | ||
options.extraSelector, | ||
ignoredFirstArgumentSelector, | ||
].join(''); | ||
optionalCall: false, | ||
optionalMember: false, | ||
computed: false, | ||
}) | ||
|| node.callee.property.type !== 'Identifier' | ||
) { | ||
return; | ||
} | ||
rules[selector] = node => { | ||
if (isNodeMatches(node.callee.object, ignoredCallee)) { | ||
return; | ||
} | ||
const methodNode = node.callee.property; | ||
const methodName = methodNode.name; | ||
if (!iteratorMethods.has(methodName)) { | ||
return; | ||
} | ||
if (node.callee.object.type === 'CallExpression' && isNodeMatches(node.callee.object.callee, ignoredCallee)) { | ||
return; | ||
} | ||
const [callback] = node.arguments; | ||
const [iterator] = node.arguments; | ||
return getProblem(context, iterator, method, options); | ||
}; | ||
} | ||
if ( | ||
callback.type === 'FunctionExpression' | ||
|| callback.type === 'ArrowFunctionExpression' | ||
// Ignore all `CallExpression`s include `function.bind()` | ||
|| callback.type === 'CallExpression' | ||
|| isNodeValueNotFunction(callback) | ||
) { | ||
return; | ||
} | ||
return rules; | ||
}; | ||
const options = iteratorMethods.get(methodName); | ||
if (!options.test(node)) { | ||
return; | ||
} | ||
return getProblem(context, callback, methodName, options); | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
@@ -228,0 +250,0 @@ module.exports = { |
'use strict'; | ||
const { | ||
isParenthesized, | ||
isArrowToken, | ||
isCommaToken, | ||
@@ -10,4 +9,3 @@ isSemicolonToken, | ||
hasSideEffect, | ||
} = require('eslint-utils'); | ||
const {methodCallSelector, referenceIdentifierSelector} = require('./selectors/index.js'); | ||
} = require('@eslint-community/eslint-utils'); | ||
const {extendFixRange} = require('./fix/index.js'); | ||
@@ -17,3 +15,3 @@ const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {getParentheses} = require('./utils/parentheses.js'); | ||
const {getParentheses, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const isFunctionSelfUsedInside = require('./utils/is-function-self-used-inside.js'); | ||
@@ -23,2 +21,3 @@ const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {fixSpaceAroundKeyword, removeParentheses} = require('./fix/index.js'); | ||
const {isArrowFunctionBody, isMethodCall, isReferenceIdentifier, functionTypes} = require('./ast/index.js'); | ||
@@ -28,12 +27,6 @@ const MESSAGE_ID_ERROR = 'no-array-for-each/error'; | ||
const messages = { | ||
[MESSAGE_ID_ERROR]: '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({ | ||
method: 'forEach', | ||
includeOptionalCall: true, | ||
includeOptionalMember: true, | ||
}); | ||
const continueAbleNodeTypes = new Set([ | ||
@@ -66,4 +59,5 @@ 'WhileStatement', | ||
switch (parent.type) { | ||
case 'IfStatement': | ||
case 'IfStatement': { | ||
return parent.consequent === returnStatement || parent.alternate === returnStatement; | ||
} | ||
@@ -76,7 +70,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; | ||
} | ||
} | ||
@@ -86,33 +82,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 isOptionalArray = callExpression.callee.optional; | ||
const expressionStatement = stripChainExpression(callExpression).parent; | ||
const arrayText = sourceCode.getText(array); | ||
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 '; | ||
const shouldAddParenthesesToArray | ||
= isParenthesized(array, sourceCode) | ||
const shouldAddParenthesesToObject | ||
= isParenthesized(iterableObject, sourceCode) | ||
|| ( | ||
// `1?.forEach()` -> `(1).entries()` | ||
isOptionalArray | ||
&& useEntries | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
isOptionalObject | ||
&& shouldUseEntries | ||
&& shouldAddParenthesesToMemberExpressionObject(iterableObject, sourceCode) | ||
); | ||
text += shouldAddParenthesesToArray ? `(${arrayText})` : arrayText; | ||
text += shouldAddParenthesesToObject ? `(${objectText})` : objectText; | ||
if (useEntries) { | ||
if (shouldUseEntries) { | ||
text += '.entries()'; | ||
@@ -128,13 +124,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]; | ||
@@ -225,3 +211,5 @@ }; | ||
// foo.forEach(bar => bar) | ||
// ^^^^^^^^^^^^^^^^^^ (space after `=>` didn't included) | ||
// ^^^^^^^^^^^^^^^^^^^^^^ | ||
// foo.forEach(bar => (bar)) | ||
// ^^^^^^^^^^^^^^^^^^^^^^ | ||
// foo.forEach(bar => {}) | ||
@@ -259,8 +247,13 @@ // ^^^^^^^^^^^^^^^^^^^^^^ | ||
const expressionStatementLastToken = sourceCode.getLastToken(expressionStatement); | ||
// 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, ' }'); | ||
} | ||
@@ -270,4 +263,4 @@ | ||
if (isOptionalArray) { | ||
yield fixer.insertTextBefore(callExpression, `if (${arrayText}) `); | ||
if (isOptionalObject) { | ||
yield fixer.insertTextBefore(callExpression, `if (${objectText}) `); | ||
} | ||
@@ -290,4 +283,4 @@ | ||
function isFunctionParametersSafeToFix(callbackFunction, {context, scope, array, allIdentifiers}) { | ||
const variables = context.getDeclaredVariables(callbackFunction); | ||
function isFunctionParametersSafeToFix(callbackFunction, {sourceCode, scope, callExpression, allIdentifiers}) { | ||
const variables = sourceCode.getDeclaredVariables(callbackFunction); | ||
@@ -305,3 +298,3 @@ 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) { | ||
@@ -311,4 +304,4 @@ const {name, range: [start, end]} = identifier; | ||
name !== variableName | ||
|| start < arrayStart | ||
|| end > arrayEnd | ||
|| start < callExpressionStart | ||
|| end > callExpressionEnd | ||
) { | ||
@@ -328,17 +321,11 @@ 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}) { | ||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceCode}) { | ||
// Check `CallExpression` | ||
@@ -350,3 +337,7 @@ if (callExpression.optional || callExpression.arguments.length !== 1) { | ||
// Check ancestors, we only fix `ExpressionStatement` | ||
if (stripChainExpression(callExpression).parent.type !== 'ExpressionStatement') { | ||
const callOrChainExpression = stripChainExpression(callExpression); | ||
if ( | ||
callOrChainExpression.parent.type !== 'ExpressionStatement' | ||
&& !isArrowFunctionBody(callOrChainExpression) | ||
) { | ||
return false; | ||
@@ -370,4 +361,8 @@ } | ||
!(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, array: callExpression, allIdentifiers, context}) | ||
|| !isFunctionParametersSafeToFix(callback, {scope, callExpression, allIdentifiers, sourceCode}) | ||
) { | ||
@@ -404,66 +399,79 @@ return false; | ||
const functionInfo = new Map(); | ||
const sourceCode = context.getSourceCode(); | ||
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) { | ||
// TODO: Rename this to iteratable | ||
const array = node.callee; | ||
context.on('ReturnStatement', node => { | ||
const currentFunction = functionStack.at(-1); | ||
if (!currentFunction) { | ||
return; | ||
} | ||
const problem = { | ||
node: array.property, | ||
messageId: MESSAGE_ID_ERROR, | ||
}; | ||
const {returnStatements} = functionInfo.get(currentFunction); | ||
returnStatements.push(node); | ||
}); | ||
if (!isFixable(node, {scope, allIdentifiers, functionInfo, context})) { | ||
yield problem; | ||
continue; | ||
} | ||
context.on('CallExpression', node => { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'forEach', | ||
}) | ||
|| isNodeMatches(node.callee.object, ignoredObjects) | ||
) { | ||
return; | ||
} | ||
const shouldUseSuggestion = array.optional && hasSideEffect(array, sourceCode); | ||
const fix = getFixFunction(node, functionInfo, context); | ||
callExpressions.push({ | ||
node, | ||
scope: sourceCode.getScope(node), | ||
}); | ||
}); | ||
if (shouldUseSuggestion) { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
fix, | ||
}, | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
} | ||
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; | ||
} | ||
}); | ||
}; | ||
@@ -477,3 +485,3 @@ | ||
docs: { | ||
description: 'Prefer `for…of` over `Array#forEach(…)`.', | ||
description: 'Prefer `for…of` over the `forEach` method.', | ||
}, | ||
@@ -480,0 +488,0 @@ fixable: 'code', |
'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'); | ||
@@ -35,2 +36,7 @@ const ERROR = 'error'; | ||
'lodash.findLast', | ||
'_.findLast', | ||
'underscore.findLast', | ||
'R.findLast', | ||
'lodash.findIndex', | ||
@@ -41,2 +47,7 @@ '_.findIndex', | ||
'lodash.findLastIndex', | ||
'_.findLastIndex', | ||
'underscore.findLastIndex', | ||
'R.findLastIndex', | ||
'lodash.flatMap', | ||
@@ -65,19 +76,2 @@ '_.flatMap', | ||
const selector = [ | ||
methodCallSelector({ | ||
methods: [ | ||
'every', | ||
'filter', | ||
'find', | ||
'findIndex', | ||
'flatMap', | ||
'forEach', | ||
'map', | ||
'some', | ||
], | ||
argumentsLength: 2, | ||
}), | ||
notFunctionSelector('arguments.0'), | ||
].join(''); | ||
function removeThisArgument(callExpression, sourceCode) { | ||
@@ -96,3 +90,3 @@ return fixer => removeArgument(fixer, callExpression.arguments[1], sourceCode); | ||
const callbackLastToken = isParenthesized | ||
? callbackParentheses[callbackParentheses.length - 1] | ||
? callbackParentheses.at(-1) | ||
: callback; | ||
@@ -115,11 +109,31 @@ if ( | ||
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; | ||
@@ -126,0 +140,0 @@ const [callback, thisArgument] = callExpression.arguments; |
'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'); | ||
@@ -15,26 +16,17 @@ const ERROR = 'error'; | ||
const arrayPushExpressionStatement = [ | ||
'ExpressionStatement', | ||
methodCallSelector({path: 'expression', method: '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; | ||
} | ||
/* c8 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'); | ||
} | ||
@@ -56,7 +48,10 @@ | ||
]; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[selector](secondExpression) { | ||
const secondCall = secondExpression.expression; | ||
CallExpression(secondCall) { | ||
if (!isArrayPushCall(secondCall)) { | ||
return; | ||
} | ||
const secondCallArray = secondCall.callee.object; | ||
@@ -68,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; | ||
@@ -96,2 +94,4 @@ | ||
const firstExpression = firstCall.parent; | ||
const secondExpression = secondCall.parent; | ||
const shouldKeepSemicolon = !isSemicolonToken(sourceCode.getLastToken(firstExpression)) | ||
@@ -98,0 +98,0 @@ && isSemicolonToken(sourceCode.getLastToken(secondExpression)); |
'use strict'; | ||
const {get} = require('lodash'); | ||
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'); | ||
@@ -11,28 +10,67 @@ const MESSAGE_ID = 'no-reduce'; | ||
const prototypeSelector = method => [ | ||
methodCallSelector(method), | ||
arrayPrototypeMethodSelector({ | ||
path: 'callee.object', | ||
methods: ['reduce', 'reduceRight'], | ||
}), | ||
].join(''); | ||
const selector = matches([ | ||
const cases = [ | ||
// `array.{reduce,reduceRight}()` | ||
[ | ||
methodCallSelector({methods: ['reduce', 'reduceRight'], minimumArguments: 1, maximumArguments: 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, | ||
}, | ||
]; | ||
@@ -57,27 +95,19 @@ const schema = [ | ||
return { | ||
[selector](node) { | ||
const callback = get(node, 'parent.parent.arguments[0]', {}); | ||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {method: node.name}, | ||
}; | ||
* CallExpression(callExpression) { | ||
for (const {test, getMethodNode, isSimpleOperation} of cases) { | ||
if (!test(callExpression)) { | ||
continue; | ||
} | ||
if (!allowSimpleOperations) { | ||
return problem; | ||
} | ||
if (allowSimpleOperations && isSimpleOperation?.(callExpression)) { | ||
continue; | ||
} | ||
if (callback.type === 'ArrowFunctionExpression' && callback.body.type === 'BinaryExpression') { | ||
return; | ||
const methodNode = getMethodNode(callExpression); | ||
yield { | ||
node: methodNode, | ||
messageId: MESSAGE_ID, | ||
data: {method: methodNode.name}, | ||
}; | ||
} | ||
if ((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') { | ||
return; | ||
} | ||
return problem; | ||
}, | ||
@@ -84,0 +114,0 @@ }; |
@@ -6,2 +6,3 @@ 'use strict'; | ||
} = require('./fix/index.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
@@ -15,6 +16,10 @@ const MESSAGE_ID = 'no-await-expression-member'; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
'MemberExpression[object.type="AwaitExpression"]'(memberExpression) { | ||
MemberExpression(memberExpression) { | ||
if (memberExpression.object.type !== 'AwaitExpression') { | ||
return; | ||
} | ||
const {property} = memberExpression; | ||
@@ -30,7 +35,7 @@ const problem = { | ||
&& !memberExpression.optional | ||
&& property.type === 'Literal' | ||
&& (property.value === 0 || property.value === 1) | ||
&& (isLiteral(property, 0) || isLiteral(property, 1)) | ||
&& memberExpression.parent.type === 'VariableDeclarator' | ||
&& memberExpression.parent.init === memberExpression | ||
&& memberExpression.parent.id.type === 'Identifier' | ||
&& !memberExpression.parent.id.typeAnnotation | ||
) { | ||
@@ -58,2 +63,3 @@ problem.fix = function * (fixer) { | ||
&& memberExpression.parent.id.name === property.name | ||
&& !memberExpression.parent.id.typeAnnotation | ||
) { | ||
@@ -60,0 +66,0 @@ problem.fix = function * (fixer) { |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
const {isStringLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -10,16 +10,2 @@ const MESSAGE_ID = 'no-console-spaces'; | ||
const methods = [ | ||
'log', | ||
'debug', | ||
'info', | ||
'warn', | ||
'error', | ||
]; | ||
const selector = methodCallSelector({ | ||
methods, | ||
minimumArguments: 1, | ||
object: 'console', | ||
}); | ||
// Find exactly one leading space, allow exactly one space | ||
@@ -29,7 +15,7 @@ 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) => { | ||
@@ -50,3 +36,21 @@ const index = position === 'leading' | ||
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; | ||
@@ -56,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; | ||
@@ -63,0 +63,0 @@ } |
'use strict'; | ||
const {getPropertyName} = require('eslint-utils'); | ||
const {GlobalReferenceTracker} = require('./utils/global-reference-tracker.js'); | ||
@@ -9,22 +9,6 @@ const MESSAGE_ID = 'no-document-cookie'; | ||
const selector = [ | ||
'AssignmentExpression', | ||
'>', | ||
'MemberExpression.left', | ||
'[object.type="Identifier"]', | ||
'[object.name="document"]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
if (getPropertyName(node, context.getScope()) !== 'cookie') { | ||
return; | ||
} | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
}; | ||
}, | ||
const tracker = new GlobalReferenceTracker({ | ||
object: 'document.cookie', | ||
filter: ({node}) => node.parent.type === 'AssignmentExpression' && node.parent.left === node, | ||
handle: ({node}) => ({node, messageId: MESSAGE_ID}), | ||
}); | ||
@@ -34,3 +18,3 @@ | ||
module.exports = { | ||
create, | ||
create: context => tracker.createListeners(context), | ||
meta: { | ||
@@ -37,0 +21,0 @@ type: 'problem', |
@@ -9,3 +9,3 @@ 'use strict'; | ||
const isDirective = node => node.type === 'ExpressionStatement' && 'directive' in node; | ||
const isDirective = node => node.type === 'ExpressionStatement' && typeof node.directive === 'string'; | ||
const isEmpty = node => isEmptyNode(node, isDirective); | ||
@@ -21,6 +21,6 @@ | ||
const create = context => { | ||
const filename = context.getPhysicalFilename().toLowerCase(); | ||
const filename = context.physicalFilename; | ||
if (!/\.(?:js|mjs|cjs|ts|mts|cts)$/.test(filename)) { | ||
return {}; | ||
if (!/\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i.test(filename)) { | ||
return; | ||
} | ||
@@ -34,3 +34,3 @@ | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const comments = sourceCode.getAllComments(); | ||
@@ -37,0 +37,0 @@ |
'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'); | ||
@@ -9,2 +8,3 @@ const getScopes = require('./utils/get-scopes.js'); | ||
const getReferences = require('./utils/get-references.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
@@ -17,6 +17,6 @@ const MESSAGE_ID = 'no-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; | ||
@@ -108,3 +108,3 @@ const getIndexIdentifierName = forStatement => { | ||
const isLiteralOnePlusIdentifierWithName = (node, identifierName) => { | ||
if (node && node.type === 'BinaryExpression' && node.operator === '+') { | ||
if (node?.type === 'BinaryExpression' && node.operator === '+') { | ||
return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right)) | ||
@@ -268,3 +268,3 @@ || (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left)); | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager, text: sourceCodeText} = sourceCode; | ||
@@ -287,3 +287,3 @@ | ||
const scope = context.getScope(); | ||
const scope = sourceCode.getScope(node); | ||
const staticResult = getStaticValue(arrayIdentifier, scope); | ||
@@ -343,4 +343,4 @@ if (staticResult && !Array.isArray(staticResult.value)) { | ||
}); | ||
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); | ||
@@ -430,5 +430,5 @@ | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
hasSuggestion: true, | ||
}, | ||
}; |
'use strict'; | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {isStringLiteral, isRegexLiteral} = require('./ast/index.js'); | ||
@@ -10,3 +11,3 @@ const MESSAGE_ID = 'no-hex-escape'; | ||
function checkEscape(context, node, value) { | ||
const fixedValue = value.replace(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00'); | ||
const fixedValue = value.replaceAll(/(?<=(?:^|[^\\])(?:\\\\)*\\)x/g, 'u00'); | ||
@@ -28,3 +29,3 @@ if (value !== fixedValue) { | ||
Literal(node) { | ||
if (node.regex || typeof node.value === 'string') { | ||
if (isStringLiteral(node) || isRegexLiteral(node)) { | ||
return checkEscape(context, node, node.raw); | ||
@@ -31,0 +32,0 @@ } |
@@ -12,20 +12,22 @@ 'use strict'; | ||
}; | ||
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) { | ||
BinaryExpression(node) { | ||
if (!( | ||
node.operator === 'instanceof' | ||
&& node.right.type === 'Identifier' | ||
&& node.right.name === 'Array' | ||
)) { | ||
return; | ||
} | ||
const {left, right} = node; | ||
let tokenStore = sourceCode; | ||
let instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken); | ||
if (!instanceofToken && context.parserServices.getTemplateBodyTokenStore) { | ||
tokenStore = context.parserServices.getTemplateBodyTokenStore(); | ||
if (!instanceofToken && sourceCode.parserServices.getTemplateBodyTokenStore) { | ||
tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore(); | ||
instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken); | ||
@@ -32,0 +34,0 @@ } |
'use strict'; | ||
const {getFunctionHeadLocation} = require('eslint-utils'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url.js'); | ||
const {methodCallSelector, matches} = require('./selectors/index.js'); | ||
const {getFunctionHeadLocation} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -11,23 +10,31 @@ const MESSAGE_ID = 'no-invalid-remove-event-listener'; | ||
const removeEventListenerSelector = [ | ||
methodCallSelector({ | ||
method: 'removeEventListener', | ||
minimumArguments: 2, | ||
}), | ||
'[arguments.0.type!="SpreadElement"]', | ||
matches([ | ||
'[arguments.1.type="FunctionExpression"]', | ||
'[arguments.1.type="ArrowFunctionExpression"]', | ||
methodCallSelector({method: 'bind', path: 'arguments.1'}), | ||
]), | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[removeEventListenerSelector](node) { | ||
const listener = node.arguments[1]; | ||
CallExpression(callExpression) { | ||
if (!( | ||
isMethodCall(callExpression, { | ||
method: 'removeEventListener', | ||
minimumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& callExpression.arguments[0].type !== 'SpreadElement' | ||
&& ( | ||
callExpression.arguments[1].type === 'FunctionExpression' | ||
|| callExpression.arguments[1].type === 'ArrowFunctionExpression' | ||
|| isMethodCall(callExpression.arguments[1], { | ||
method: 'bind', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
)) { | ||
return; | ||
} | ||
const [, listener] = callExpression.arguments; | ||
if (['ArrowFunctionExpression', 'FunctionExpression'].includes(listener.type)) { | ||
return { | ||
node: listener, | ||
loc: getFunctionHeadLocation(listener, context.getSourceCode()), | ||
loc: getFunctionHeadLocation(listener, context.sourceCode), | ||
messageId: MESSAGE_ID, | ||
@@ -51,3 +58,2 @@ }; | ||
description: 'Prevent calling `EventTarget#removeEventListener()` with the result of an expression.', | ||
url: getDocumentationUrl(__filename), | ||
}, | ||
@@ -54,0 +60,0 @@ messages, |
@@ -66,3 +66,3 @@ 'use strict'; | ||
// Prevent checking righthand side of destructured object | ||
// Prevent checking right hand side of destructured object | ||
if (parent.key === node && parent.value !== node) { | ||
@@ -117,3 +117,3 @@ return true; | ||
) { | ||
if (parent.parent && parent.parent.type === 'ObjectPattern') { | ||
if (parent.parent.type === 'ObjectPattern') { | ||
const finished = checkObjectPattern(report, node, options); | ||
@@ -150,7 +150,3 @@ if (finished) { | ||
// 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); | ||
@@ -157,0 +153,0 @@ } |
'use strict'; | ||
const {isParenthesized, isNotSemicolonToken} = require('eslint-utils'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {isParenthesized, isNotSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const {needsSemicolon} = require('./utils/index.js'); | ||
const {removeSpacesAfter} = require('./fix/index.js'); | ||
const {matches} = require('./selectors/index.js'); | ||
@@ -12,18 +11,4 @@ const MESSAGE_ID = 'no-lonely-if'; | ||
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 | ||
@@ -116,3 +101,3 @@ // Lower precedence than `&&` | ||
const nextToken = sourceCode.getTokenAfter(outer); | ||
if (needsSemicolon(lastToken, sourceCode, nextToken.value)) { | ||
if (nextToken && needsSemicolon(lastToken, sourceCode, nextToken.value)) { | ||
yield fixer.insertTextBefore(nextToken, ';'); | ||
@@ -126,15 +111,32 @@ } | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
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 { | ||
[selector](node) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
fix: fix(node, sourceCode), | ||
}; | ||
}, | ||
}; | ||
}; | ||
return { | ||
node: ifStatement, | ||
messageId: MESSAGE_ID, | ||
fix: fix(ifStatement, context.sourceCode), | ||
}; | ||
}, | ||
}); | ||
@@ -141,0 +143,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
@@ -11,27 +11,38 @@ const MESSAGE_ID_TOO_DEEP = 'too-deep'; | ||
const nestTernarySelector = level => `:not(ConditionalExpression)${' > ConditionalExpression'.repeat(level)}`; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const create = context => ({ | ||
ConditionalExpression(node) { | ||
if ([ | ||
node.test, | ||
node.consequent, | ||
node.alternate, | ||
].some(node => node.type === 'ConditionalExpression')) { | ||
return; | ||
} | ||
return { | ||
[nestTernarySelector(3)]: node => | ||
// Nesting more than one level not allowed. | ||
({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, ')'), | ||
], | ||
}; | ||
} | ||
}, | ||
}; | ||
}; | ||
const {sourceCode} = context; | ||
const ancestors = sourceCode.getAncestors(node).reverse(); | ||
const nestLevel = ancestors.findIndex(node => node.type !== 'ConditionalExpression'); | ||
if (nestLevel === 1 && !isParenthesized(node, sourceCode)) { | ||
return { | ||
node, | ||
messageId: MESSAGE_ID_SHOULD_PARENTHESIZED, | ||
fix: fixer => [ | ||
fixer.insertTextBefore(node, '('), | ||
fixer.insertTextAfter(node, ')'), | ||
], | ||
}; | ||
} | ||
// Nesting more than one level not allowed | ||
if (nestLevel > 1) { | ||
return { | ||
node: nestLevel > 2 ? ancestors[nestLevel - 3] : node, | ||
messageId: MESSAGE_ID_TOO_DEEP, | ||
}; | ||
} | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
@@ -38,0 +49,0 @@ module.exports = { |
'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'); | ||
@@ -17,9 +17,14 @@ const MESSAGE_ID_ERROR = 'error'; | ||
}; | ||
const newArraySelector = newExpressionSelector({ | ||
name: 'Array', | ||
argumentsLength: 1, | ||
allowSpreadElement: true, | ||
}); | ||
function getProblem(context, node) { | ||
if ( | ||
!isNewExpression(node, { | ||
name: 'Array', | ||
argumentsLength: 1, | ||
allowSpreadElement: true, | ||
}) | ||
) { | ||
return; | ||
} | ||
const problem = { | ||
@@ -32,3 +37,3 @@ node, | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
let text = sourceCode.getText(argumentNode); | ||
@@ -55,3 +60,4 @@ if (isParenthesized(argumentNode, sourceCode)) { | ||
const fromLengthText = `Array.from(${text === 'length' ? '{length}' : `{length: ${text}}`})`; | ||
if (isNumber(argumentNode, context.getScope())) { | ||
const scope = sourceCode.getScope(node); | ||
if (isNumber(argumentNode, scope)) { | ||
problem.fix = fixer => fixer.replaceText(node, fromLengthText); | ||
@@ -62,3 +68,3 @@ return problem; | ||
const onlyElementText = `${maybeSemiColon}[${text}]`; | ||
const result = getStaticValue(argumentNode, context.getScope()); | ||
const result = getStaticValue(argumentNode, scope); | ||
if (result !== null && typeof result.value !== 'number') { | ||
@@ -85,3 +91,3 @@ problem.fix = fixer => fixer.replaceText(node, onlyElementText); | ||
const create = context => ({ | ||
[newArraySelector](node) { | ||
NewExpression(node) { | ||
return getProblem(context, node); | ||
@@ -88,0 +94,0 @@ }, |
'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'); | ||
@@ -13,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}}()`.', | ||
}; | ||
@@ -56,7 +56,11 @@ | ||
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) { | ||
@@ -74,6 +78,6 @@ return { | ||
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), | ||
})), | ||
@@ -80,0 +84,0 @@ }; |
'use strict'; | ||
const { | ||
not, | ||
methodCallSelector, | ||
callExpressionSelector, | ||
} = require('./selectors/index.js'); | ||
isMethodCall, | ||
isCallExpression, | ||
isLiteral, | ||
} = require('./ast/index.js'); | ||
@@ -17,17 +17,2 @@ const ERROR_MESSAGE_ID = 'error'; | ||
const selector = [ | ||
'Literal', | ||
'[raw="null"]', | ||
not([ | ||
// `Object.create(null)`, `Object.create(null, foo)` | ||
`${methodCallSelector({object: 'Object', method: 'create', minimumArguments: 1, maximumArguments: 2})} > .arguments:first-child`, | ||
// `useRef(null)` | ||
`${callExpressionSelector({name: 'useRef', argumentsLength: 1})} > .arguments:first-child`, | ||
// `React.useRef(null)` | ||
`${methodCallSelector({object: 'React', method: 'useRef', argumentsLength: 1})} > .arguments:first-child`, | ||
// `foo.insertBefore(bar, null)` | ||
`${methodCallSelector({method: 'insertBefore', argumentsLength: 2})}[arguments.0.type!="SpreadElement"] > .arguments:nth-child(2)`, | ||
]), | ||
].join(''); | ||
const isLooseEqual = node => node.type === 'BinaryExpression' && ['==', '!='].includes(node.operator); | ||
@@ -44,8 +29,56 @@ const isStrictEqual = node => node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator); | ||
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; | ||
} | ||
const {parent} = node; | ||
const problem = { | ||
@@ -52,0 +85,0 @@ node, |
'use strict'; | ||
const {isFunction} = require('./ast/index.js'); | ||
@@ -10,11 +11,14 @@ const MESSAGE_ID_IDENTIFIER = 'identifier'; | ||
const objectParameterSelector = [ | ||
':function > AssignmentPattern.params', | ||
'[right.type="ObjectExpression"]', | ||
'[right.properties.length>0]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
[objectParameterSelector](node) { | ||
AssignmentPattern(node) { | ||
if (!( | ||
node.right.type === 'ObjectExpression' | ||
&& node.right.properties.length > 0 | ||
&& isFunction(node.parent) | ||
&& node.parent.params.includes(node) | ||
)) { | ||
return; | ||
} | ||
const {left, right} = node; | ||
@@ -21,0 +25,0 @@ |
'use strict'; | ||
const {methodCallSelector, STATIC_REQUIRE_SELECTOR} = require('./selectors/index.js'); | ||
const {isStaticRequire, isMethodCall, isLiteral} = require('./ast/index.js'); | ||
@@ -9,28 +9,9 @@ const MESSAGE_ID = 'no-process-exit'; | ||
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', | ||
methods: ['on', 'once'], | ||
minimumArguments: 1, | ||
}); | ||
const processExitCallSelector = methodCallSelector({ | ||
object: 'process', | ||
method: '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; | ||
@@ -47,33 +28,67 @@ 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, | ||
}; | ||
} | ||
}); | ||
}; | ||
@@ -80,0 +95,0 @@ |
'use strict'; | ||
const {isSemicolonToken} = require('eslint-utils'); | ||
const {isSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const getClassHeadLocation = require('./utils/get-class-head-location.js'); | ||
@@ -12,9 +12,2 @@ const assertToken = require('./utils/assert-token.js'); | ||
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 === '='; | ||
@@ -53,6 +46,4 @@ const isDeclarationOfExportDefaultDeclaration = node => | ||
|| isReadonly | ||
|| typeof accessibility !== 'undefined' | ||
|| accessibility !== undefined | ||
|| (Array.isArray(decorators) && decorators.length > 0) | ||
// TODO: Remove this when we drop support for `@typescript-eslint/parser` v4 | ||
|| key.type === 'TSPrivateIdentifier' | ||
) { | ||
@@ -202,18 +193,22 @@ return false; | ||
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), | ||
}; | ||
}); | ||
} | ||
@@ -220,0 +215,0 @@ |
'use strict'; | ||
const {getStaticValue, getPropertyName} = require('eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {getStaticValue, getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -14,8 +14,5 @@ const MESSAGE_ID_OBJECT = 'no-thenable-object'; | ||
const isStringThen = (node, context) => { | ||
const result = getStaticValue(node, context.getScope()); | ||
const isStringThen = (node, context) => | ||
getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then'; | ||
return result && result.value === 'then'; | ||
}; | ||
const cases = [ | ||
@@ -27,4 +24,13 @@ // `{then() {}}`, | ||
{ | ||
selector: 'ObjectExpression > Property.properties > .key', | ||
test: (node, context) => getPropertyName(node.parent, context.getScope()) === 'then', | ||
selector: 'ObjectExpression', | ||
* getNodes(node, context) { | ||
for (const property of node.properties) { | ||
if ( | ||
property.type === 'Property' | ||
&& getPropertyName(property, context.sourceCode.getScope(property)) === 'then' | ||
) { | ||
yield property.key; | ||
} | ||
} | ||
}, | ||
messageId: MESSAGE_ID_OBJECT, | ||
@@ -37,4 +43,8 @@ }, | ||
{ | ||
selector: ':matches(PropertyDefinition, MethodDefinition) > .key', | ||
test: (node, context) => getPropertyName(node.parent, context.getScope()) === 'then', | ||
selectors: ['PropertyDefinition', 'MethodDefinition'], | ||
* getNodes(node, context) { | ||
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') { | ||
yield node.key; | ||
} | ||
}, | ||
messageId: MESSAGE_ID_CLASS, | ||
@@ -45,4 +55,12 @@ }, | ||
{ | ||
selector: 'AssignmentExpression > MemberExpression.left > .property', | ||
test: (node, context) => getPropertyName(node.parent, context.getScope()) === 'then', | ||
selector: 'MemberExpression', | ||
* getNodes(node, context) { | ||
if (!(node.parent.type === 'AssignmentExpression' && node.parent.left === node)) { | ||
return; | ||
} | ||
if (getPropertyName(node, context.sourceCode.getScope(node)) === 'then') { | ||
yield node.property; | ||
} | ||
}, | ||
messageId: MESSAGE_ID_OBJECT, | ||
@@ -53,26 +71,55 @@ }, | ||
{ | ||
selector: [ | ||
methodCallSelector({ | ||
objects: ['Object', 'Reflect'], | ||
method: 'defineProperty', | ||
minimumArguments: 3, | ||
}), | ||
'[arguments.0.type!="SpreadElement"]', | ||
' > .arguments:nth-child(2)', | ||
].join(''), | ||
test: isStringThen, | ||
selector: 'CallExpression', | ||
* getNodes(node, context) { | ||
if (!( | ||
isMethodCall(node, { | ||
objects: ['Object', 'Reflect'], | ||
method: 'defineProperty', | ||
minimumArguments: 3, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.arguments[0].type !== 'SpreadElement' | ||
)) { | ||
return; | ||
} | ||
const [, secondArgument] = node.arguments; | ||
if (isStringThen(secondArgument, context)) { | ||
yield secondArgument; | ||
} | ||
}, | ||
messageId: MESSAGE_ID_OBJECT, | ||
}, | ||
// `Object.fromEntries(['then', …])` | ||
// `Object.fromEntries([['then', …]])` | ||
{ | ||
selector: [ | ||
methodCallSelector({ | ||
object: 'Object', | ||
method: 'fromEntries', | ||
argumentsLength: 1, | ||
}), | ||
' > ArrayExpression.arguments:nth-child(1)', | ||
' > .elements:nth-child(1)', | ||
].join(''), | ||
test: isStringThen, | ||
selector: 'CallExpression', | ||
* getNodes(node, context) { | ||
if (!( | ||
isMethodCall(node, { | ||
object: 'Object', | ||
method: 'fromEntries', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.arguments[0].type === 'ArrayExpression' | ||
)) { | ||
return; | ||
} | ||
for (const pairs of node.arguments[0].elements) { | ||
if ( | ||
pairs?.type === 'ArrayExpression' | ||
&& pairs.elements[0] | ||
&& pairs.elements[0].type !== 'SpreadElement' | ||
) { | ||
const [key] = pairs.elements; | ||
if (isStringThen(key, context)) { | ||
yield key; | ||
} | ||
} | ||
} | ||
}, | ||
messageId: MESSAGE_ID_OBJECT, | ||
@@ -82,3 +129,12 @@ }, | ||
{ | ||
selector: 'ExportSpecifier.specifiers > Identifier.exported[name="then"]', | ||
selector: 'Identifier', | ||
* getNodes(node) { | ||
if ( | ||
node.name === 'then' | ||
&& node.parent.type === 'ExportSpecifier' | ||
&& node.parent.exported === node | ||
) { | ||
yield node; | ||
} | ||
}, | ||
messageId: MESSAGE_ID_EXPORT, | ||
@@ -89,3 +145,14 @@ }, | ||
{ | ||
selector: 'ExportNamedDeclaration > :matches(FunctionDeclaration, ClassDeclaration).declaration > Identifier[name="then"].id', | ||
selector: 'Identifier', | ||
* getNodes(node) { | ||
if ( | ||
node.name === 'then' | ||
&& (node.parent.type === 'FunctionDeclaration' || node.parent.type === 'ClassDeclaration') | ||
&& node.parent.id === node | ||
&& node.parent.parent.type === 'ExportNamedDeclaration' | ||
&& node.parent.parent.declaration === node.parent | ||
) { | ||
yield node; | ||
} | ||
}, | ||
messageId: MESSAGE_ID_EXPORT, | ||
@@ -95,5 +162,15 @@ }, | ||
{ | ||
selector: 'ExportNamedDeclaration > VariableDeclaration.declaration', | ||
selector: 'VariableDeclaration', | ||
* getNodes(node, context) { | ||
if (!(node.parent.type === 'ExportNamedDeclaration' && node.parent.declaration === node)) { | ||
return; | ||
} | ||
for (const variable of context.sourceCode.getDeclaredVariables(node)) { | ||
if (variable.name === 'then') { | ||
yield * variable.identifiers; | ||
} | ||
} | ||
}, | ||
messageId: MESSAGE_ID_EXPORT, | ||
getNodes: (node, context) => context.getDeclaredVariables(node).flatMap(({name, identifiers}) => name === 'then' ? identifiers : []), | ||
}, | ||
@@ -103,23 +180,12 @@ ]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => Object.fromEntries( | ||
cases.map(({selector, test, messageId, getNodes}) => [ | ||
selector, | ||
function * (node) { | ||
if (getNodes) { | ||
for (const problematicNode of getNodes(node, context)) { | ||
yield {node: problematicNode, messageId}; | ||
} | ||
return; | ||
const create = context => { | ||
for (const {selector, selectors, messageId, getNodes} of cases) { | ||
context.on(selector ?? selectors, function * (node) { | ||
for (const problematicNode of getNodes(node, context)) { | ||
yield {node: problematicNode, messageId}; | ||
} | ||
}); | ||
} | ||
}; | ||
if (test && !test(node, context)) { | ||
return; | ||
} | ||
yield {node, messageId}; | ||
}, | ||
]), | ||
); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
@@ -126,0 +192,0 @@ module.exports = { |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const MESSAGE_ID = 'no-this-assignment'; | ||
@@ -9,27 +7,22 @@ const messages = { | ||
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 = () => ({ | ||
[selector](node) { | ||
const variable = node.type === 'AssignmentExpression' ? node.left : node.id; | ||
return { | ||
node, | ||
data: {name: variable.name}, | ||
messageId: MESSAGE_ID, | ||
}; | ||
}, | ||
}); | ||
const create = context => { | ||
context.on('VariableDeclarator', node => getProblem(node.id, node.init)); | ||
context.on('AssignmentExpression', node => getProblem(node.left, node.right)); | ||
}; | ||
@@ -36,0 +29,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'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'); | ||
@@ -16,9 +16,11 @@ const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
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; | ||
@@ -25,0 +27,0 @@ } |
@@ -5,4 +5,4 @@ 'use strict'; | ||
getParenthesizedRange, | ||
} = require('./utils/parentheses.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
toLocation, | ||
} = require('./utils/index.js'); | ||
@@ -14,15 +14,12 @@ const MESSAGE_ID_ERROR = 'no-unreadable-iife'; | ||
const selector = [ | ||
'CallExpression', | ||
' > ', | ||
'ArrowFunctionExpression.callee', | ||
' > ', | ||
':not(BlockStatement).body', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
const sourceCode = context.getSourceCode(); | ||
if (!isParenthesized(node, sourceCode)) { | ||
CallExpression(callExpression) { | ||
const {sourceCode} = context; | ||
if ( | ||
callExpression.callee.type !== 'ArrowFunctionExpression' | ||
|| callExpression.callee.body.type === 'BlockStatement' | ||
|| !isParenthesized(callExpression.callee.body, sourceCode) | ||
) { | ||
return; | ||
@@ -32,4 +29,4 @@ } | ||
return { | ||
node, | ||
loc: toLocation(getParenthesizedRange(node, sourceCode), sourceCode), | ||
node: callExpression, | ||
loc: toLocation(getParenthesizedRange(callExpression.callee.body, sourceCode), sourceCode), | ||
messageId: MESSAGE_ID_ERROR, | ||
@@ -36,0 +33,0 @@ }; |
@@ -14,9 +14,7 @@ 'use strict'; | ||
const isMemberExpressionCall = memberExpression => | ||
memberExpression.parent | ||
&& memberExpression.parent.type === 'CallExpression' | ||
memberExpression.parent.type === 'CallExpression' | ||
&& memberExpression.parent.callee === memberExpression; | ||
const isMemberExpressionAssignment = memberExpression => | ||
memberExpression.parent | ||
&& memberExpression.parent.type === 'AssignmentExpression'; | ||
memberExpression.parent.type === 'AssignmentExpression'; | ||
@@ -86,2 +84,3 @@ const isMemberExpressionComputedBeyondPrediction = memberExpression => | ||
const create = context => { | ||
const {sourceCode} = context; | ||
const getPropertyDisplayName = property => { | ||
@@ -96,3 +95,3 @@ if (property.key.type === 'Identifier') { | ||
return context.getSourceCode().getText(property.key); | ||
return sourceCode.getText(property.key); | ||
}; | ||
@@ -219,4 +218,4 @@ | ||
return { | ||
'Program:exit'() { | ||
const scopes = getScopes(context.getScope()); | ||
'Program:exit'(program) { | ||
const scopes = getScopes(sourceCode.getScope(program)); | ||
for (const scope of scopes) { | ||
@@ -223,0 +222,0 @@ if (scope.type === 'global') { |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const { | ||
@@ -15,26 +14,25 @@ isParenthesized, | ||
const selector = [ | ||
'ObjectExpression', | ||
' > ', | ||
'SpreadElement.properties', | ||
' > ', | ||
'LogicalExpression.argument', | ||
matches([ | ||
'[operator="||"]', | ||
'[operator="??"]', | ||
]), | ||
' > ', | ||
'ObjectExpression[properties.length=0].right', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](emptyObject) { | ||
ObjectExpression(node) { | ||
if (!( | ||
node.properties.length === 0 | ||
&& node.parent.type === 'LogicalExpression' | ||
&& node.parent.right === node | ||
&& (node.parent.operator === '||' || node.parent.operator === '??') | ||
&& node.parent.parent.type === 'SpreadElement' | ||
&& node.parent.parent.argument === node.parent | ||
&& node.parent.parent.parent.type === 'ObjectExpression' | ||
&& node.parent.parent.parent.properties.includes(node.parent.parent) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: emptyObject, | ||
node, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const logicalExpression = emptyObject.parent; | ||
const {sourceCode} = context; | ||
const logicalExpression = node.parent; | ||
const {left} = logicalExpression; | ||
@@ -41,0 +39,0 @@ const isLeftObjectParenthesized = isParenthesized(left, sourceCode); |
'use strict'; | ||
const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js'); | ||
const isSameReference = require('./utils/is-same-reference.js'); | ||
const {getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {isMethodCall, isMemberExpression} = require('./ast/index.js'); | ||
const { | ||
getParenthesizedRange, | ||
isSameReference, | ||
isLogicalExpression, | ||
} = require('./utils/index.js'); | ||
@@ -11,25 +14,9 @@ const messages = { | ||
const logicalExpressionSelector = [ | ||
'LogicalExpression', | ||
matches(['[operator="||"]', '[operator="&&"]']), | ||
].join(''); | ||
// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule. | ||
const lengthCompareZeroSelector = [ | ||
logicalExpressionSelector, | ||
' > ', | ||
'BinaryExpression', | ||
memberExpressionSelector({path: 'left', property: 'length'}), | ||
'[right.type="Literal"]', | ||
'[right.raw="0"]', | ||
].join(''); | ||
const zeroLengthCheckSelector = [ | ||
lengthCompareZeroSelector, | ||
'[operator="==="]', | ||
].join(''); | ||
const nonZeroLengthCheckSelector = [ | ||
lengthCompareZeroSelector, | ||
matches(['[operator=">"]', '[operator="!=="]']), | ||
].join(''); | ||
const arraySomeCallSelector = methodCallSelector('some'); | ||
const arrayEveryCallSelector = methodCallSelector('every'); | ||
const isLengthCompareZero = node => | ||
node.type === 'BinaryExpression' | ||
&& node.right.type === 'Literal' | ||
&& node.right.raw === '0' | ||
&& isMemberExpression(node.left, {property: 'length', optional: false}) | ||
&& isLogicalExpression(node.parent); | ||
@@ -87,17 +74,33 @@ function flatLogicalExpression(node) { | ||
return { | ||
[zeroLengthCheckSelector](node) { | ||
zeroLengthChecks.add(node); | ||
BinaryExpression(node) { | ||
if (isLengthCompareZero(node)) { | ||
const {operator} = node; | ||
if (operator === '===') { | ||
zeroLengthChecks.add(node); | ||
} else if (operator === '>' || operator === '!==') { | ||
nonZeroLengthChecks.add(node); | ||
} | ||
} | ||
}, | ||
[nonZeroLengthCheckSelector](node) { | ||
nonZeroLengthChecks.add(node); | ||
CallExpression(node) { | ||
if ( | ||
isMethodCall(node, { | ||
optionalCall: false, | ||
optionalMember: false, | ||
computed: false, | ||
}) | ||
&& node.callee.property.type === 'Identifier' | ||
) { | ||
if (node.callee.property.name === 'some') { | ||
arraySomeCalls.add(node); | ||
} else if (node.callee.property.name === 'every') { | ||
arrayEveryCalls.add(node); | ||
} | ||
} | ||
}, | ||
[arraySomeCallSelector](node) { | ||
arraySomeCalls.add(node); | ||
LogicalExpression(node) { | ||
if (isLogicalExpression(node)) { | ||
logicalExpressions.push(node); | ||
} | ||
}, | ||
[arrayEveryCallSelector](node) { | ||
arrayEveryCalls.add(node); | ||
}, | ||
[logicalExpressionSelector](node) { | ||
logicalExpressions.push(node); | ||
}, | ||
* 'Program:exit'() { | ||
@@ -119,3 +122,3 @@ const nodes = new Set( | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {left, right} = node.parent; | ||
@@ -122,0 +125,0 @@ const leftRange = getParenthesizedRange(left, sourceCode); |
'use strict'; | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
const {getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {getParenthesizedRange} = require('./utils/index.js'); | ||
const {isFunction, isMethodCall} = require('./ast/index.js'); | ||
@@ -12,19 +12,2 @@ const MESSAGE_ID_RESOLVE = 'resolve'; | ||
const selector = [ | ||
methodCallSelector({ | ||
object: 'Promise', | ||
methods: ['resolve', 'reject'], | ||
}), | ||
matches([ | ||
'ArrowFunctionExpression > .body', | ||
'ReturnStatement > .argument', | ||
'YieldExpression[delegate!=true] > .argument', | ||
]), | ||
].join(''); | ||
const functionTypes = new Set([ | ||
'ArrowFunctionExpression', | ||
'FunctionDeclaration', | ||
'FunctionExpression', | ||
]); | ||
function getFunctionNode(node) { | ||
@@ -34,3 +17,3 @@ let isInTryStatement = false; | ||
for (; node; node = node.parent) { | ||
if (functionTypes.has(node.type)) { | ||
if (isFunction(node)) { | ||
functionNode = node; | ||
@@ -106,3 +89,3 @@ break; | ||
const {callee, parent, arguments: [errorOrValue]} = callExpression; | ||
if (errorOrValue && errorOrValue.type === 'SpreadElement') { | ||
if (errorOrValue?.type === 'SpreadElement') { | ||
return; | ||
@@ -128,3 +111,3 @@ } | ||
if (errorOrValue && errorOrValue.type === 'SequenceExpression') { | ||
if (errorOrValue?.type === 'SequenceExpression') { | ||
text = `(${text})`; | ||
@@ -162,3 +145,3 @@ } | ||
} else { | ||
if (errorOrValue && errorOrValue.type === 'ObjectExpression') { | ||
if (errorOrValue?.type === 'ObjectExpression') { | ||
text = `(${text})`; | ||
@@ -181,6 +164,31 @@ } | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[selector](callExpression) { | ||
CallExpression(callExpression) { | ||
if (!( | ||
isMethodCall(callExpression, { | ||
object: 'Promise', | ||
methods: ['resolve', 'reject'], | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& ( | ||
( | ||
callExpression.parent.type === 'ArrowFunctionExpression' | ||
&& callExpression.parent.body === callExpression | ||
) | ||
|| ( | ||
callExpression.parent.type === 'ReturnStatement' | ||
&& callExpression.parent.argument === callExpression | ||
) | ||
|| ( | ||
callExpression.parent.type === 'YieldExpression' | ||
&& !callExpression.parent.delegate && callExpression.parent.argument === callExpression | ||
) | ||
) | ||
)) { | ||
return; | ||
} | ||
const {functionNode, isInTryStatement} = getFunctionNode(callExpression); | ||
@@ -187,0 +195,0 @@ if (!functionNode || !(functionNode.async || isPromiseCallback(functionNode))) { |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const typedArray = require('./shared/typed-array.js'); | ||
const { | ||
matches, | ||
newExpressionSelector, | ||
methodCallSelector, | ||
} = require('./selectors/index.js'); | ||
const typedArray = require('./shared/typed-array.js'); | ||
const {removeParentheses, fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
removeParentheses, | ||
fixSpaceAroundKeyword, | ||
addParenthesizesToReturnOrThrowExpression, | ||
} = require('./fix/index.js'); | ||
const { | ||
isParenthesized, | ||
isOnSameLine, | ||
} = require('./utils/index.js'); | ||
const {isNewExpression, isMethodCall, isCallOrNewExpression} = require('./ast/index.js'); | ||
@@ -15,2 +19,3 @@ const SPREAD_IN_LIST = 'spread-in-list'; | ||
const ITERABLE_TO_ARRAY_IN_YIELD_STAR = 'iterable-to-array-in-yield-star'; | ||
const CLONE_ARRAY = 'clone-array'; | ||
const messages = { | ||
@@ -21,40 +26,10 @@ [SPREAD_IN_LIST]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.', | ||
[ITERABLE_TO_ARRAY_IN_YIELD_STAR]: '`yield*` can delegate iterable, it\'s unnecessary to convert to an array.', | ||
[CLONE_ARRAY]: 'Unnecessarily cloning an array.', | ||
}; | ||
const uselessSpreadInListSelector = matches([ | ||
'ArrayExpression > SpreadElement.elements > ArrayExpression.argument', | ||
'ObjectExpression > SpreadElement.properties > ObjectExpression.argument', | ||
'CallExpression > SpreadElement.arguments > ArrayExpression.argument', | ||
'NewExpression > SpreadElement.arguments > ArrayExpression.argument', | ||
]); | ||
const isSingleArraySpread = node => | ||
node.type === 'ArrayExpression' | ||
&& node.elements.length === 1 | ||
&& node.elements[0]?.type === 'SpreadElement'; | ||
const iterableToArraySelector = [ | ||
'ArrayExpression', | ||
'[elements.length=1]', | ||
'[elements.0.type="SpreadElement"]', | ||
].join(''); | ||
const uselessIterableToArraySelector = matches([ | ||
[ | ||
matches([ | ||
newExpressionSelector({names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1}), | ||
newExpressionSelector({names: typedArray, minimumArguments: 1}), | ||
methodCallSelector({ | ||
object: 'Promise', | ||
methods: ['all', 'allSettled', 'any', 'race'], | ||
argumentsLength: 1, | ||
}), | ||
methodCallSelector({ | ||
objects: ['Array', ...typedArray], | ||
method: 'from', | ||
argumentsLength: 1, | ||
}), | ||
methodCallSelector({object: 'Object', method: 'fromEntries', argumentsLength: 1}), | ||
]), | ||
' > ', | ||
`${iterableToArraySelector}.arguments:first-child`, | ||
].join(''), | ||
`ForOfStatement > ${iterableToArraySelector}.right`, | ||
`YieldExpression[delegate=true] > ${iterableToArraySelector}.argument`, | ||
]); | ||
const parentDescriptions = { | ||
@@ -86,126 +61,311 @@ ArrayExpression: 'array literal', | ||
function * unwrapSingleArraySpread(fixer, arrayExpression, sourceCode) { | ||
const [ | ||
openingBracketToken, | ||
spreadToken, | ||
thirdToken, | ||
] = sourceCode.getFirstTokens(arrayExpression, 3); | ||
// `[...value]` | ||
// ^ | ||
yield fixer.remove(openingBracketToken); | ||
// `[...value]` | ||
// ^^^ | ||
yield fixer.remove(spreadToken); | ||
const [ | ||
commaToken, | ||
closingBracketToken, | ||
] = sourceCode.getLastTokens(arrayExpression, 2); | ||
// `[...value]` | ||
// ^ | ||
yield fixer.remove(closingBracketToken); | ||
// `[...value,]` | ||
// ^ | ||
if (isCommaToken(commaToken)) { | ||
yield fixer.remove(commaToken); | ||
} | ||
/* | ||
```js | ||
function foo() { | ||
return [ | ||
...value, | ||
]; | ||
} | ||
``` | ||
*/ | ||
const {parent} = arrayExpression; | ||
if ( | ||
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement') | ||
&& parent.argument === arrayExpression | ||
&& !isOnSameLine(openingBracketToken, thirdToken) | ||
&& !isParenthesized(arrayExpression, sourceCode) | ||
) { | ||
yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode); | ||
return; | ||
} | ||
yield * fixSpaceAroundKeyword(fixer, arrayExpression, sourceCode); | ||
} | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[uselessSpreadInListSelector](spreadObject) { | ||
const spreadElement = spreadObject.parent; | ||
const spreadToken = sourceCode.getFirstToken(spreadElement); | ||
const parentType = spreadElement.parent.type; | ||
// Useless spread in list | ||
context.on(['ArrayExpression', 'ObjectExpression'], node => { | ||
if (!( | ||
node.parent.type === 'SpreadElement' | ||
&& node.parent.argument === node | ||
&& ( | ||
( | ||
node.type === 'ObjectExpression' | ||
&& node.parent.parent.type === 'ObjectExpression' | ||
&& node.parent.parent.properties.includes(node.parent) | ||
) | ||
|| ( | ||
node.type === 'ArrayExpression' | ||
&& ( | ||
( | ||
node.parent.parent.type === 'ArrayExpression' | ||
&& node.parent.parent.elements.includes(node.parent) | ||
) | ||
|| ( | ||
isCallOrNewExpression(node.parent.parent) | ||
&& node.parent.parent.arguments.includes(node.parent) | ||
) | ||
) | ||
) | ||
) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: spreadToken, | ||
messageId: SPREAD_IN_LIST, | ||
data: { | ||
argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object', | ||
parentDescription: parentDescriptions[parentType], | ||
}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
// `[...[foo]]` | ||
// ^^^ | ||
yield fixer.remove(spreadToken); | ||
const spreadObject = node; | ||
const spreadElement = spreadObject.parent; | ||
const spreadToken = sourceCode.getFirstToken(spreadElement); | ||
const parentType = spreadElement.parent.type; | ||
// `[...(( [foo] ))]` | ||
// ^^ ^^ | ||
yield * removeParentheses(spreadObject, fixer, sourceCode); | ||
return { | ||
node: spreadToken, | ||
messageId: SPREAD_IN_LIST, | ||
data: { | ||
argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object', | ||
parentDescription: parentDescriptions[parentType], | ||
}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
// `[...[foo]]` | ||
// ^^^ | ||
yield fixer.remove(spreadToken); | ||
// `[...[foo]]` | ||
// ^ | ||
const firstToken = sourceCode.getFirstToken(spreadObject); | ||
yield fixer.remove(firstToken); | ||
// `[...(( [foo] ))]` | ||
// ^^ ^^ | ||
yield * removeParentheses(spreadObject, fixer, sourceCode); | ||
const [ | ||
penultimateToken, | ||
lastToken, | ||
] = sourceCode.getLastTokens(spreadObject, 2); | ||
// `[...[foo]]` | ||
// ^ | ||
const firstToken = sourceCode.getFirstToken(spreadObject); | ||
yield fixer.remove(firstToken); | ||
// `[...[foo]]` | ||
// ^ | ||
yield fixer.remove(lastToken); | ||
const [ | ||
penultimateToken, | ||
lastToken, | ||
] = sourceCode.getLastTokens(spreadObject, 2); | ||
// `[...[foo,]]` | ||
// ^ | ||
if (isCommaToken(penultimateToken)) { | ||
yield fixer.remove(penultimateToken); | ||
} | ||
// `[...[foo]]` | ||
// ^ | ||
yield fixer.remove(lastToken); | ||
if (parentType !== 'CallExpression' && parentType !== 'NewExpression') { | ||
return; | ||
// `[...[foo,]]` | ||
// ^ | ||
if (isCommaToken(penultimateToken)) { | ||
yield fixer.remove(penultimateToken); | ||
} | ||
if (parentType !== 'CallExpression' && parentType !== 'NewExpression') { | ||
return; | ||
} | ||
const commaTokens = getCommaTokens(spreadObject, sourceCode); | ||
for (const [index, commaToken] of commaTokens.entries()) { | ||
if (spreadObject.elements[index]) { | ||
continue; | ||
} | ||
const commaTokens = getCommaTokens(spreadObject, sourceCode); | ||
for (const [index, commaToken] of commaTokens.entries()) { | ||
if (spreadObject.elements[index]) { | ||
continue; | ||
} | ||
// `call(...[foo, , bar])` | ||
// ^ Replace holes with `undefined` | ||
yield fixer.insertTextBefore(commaToken, 'undefined'); | ||
} | ||
}, | ||
}; | ||
}); | ||
// `call([foo, , bar])` | ||
// ^ Replace holes with `undefined` | ||
yield fixer.insertTextBefore(commaToken, 'undefined'); | ||
} | ||
}, | ||
}; | ||
}, | ||
[uselessIterableToArraySelector](array) { | ||
const {parent} = array; | ||
let parentDescription = ''; | ||
let messageId = ITERABLE_TO_ARRAY; | ||
switch (parent.type) { | ||
case 'ForOfStatement': | ||
messageId = ITERABLE_TO_ARRAY_IN_FOR_OF; | ||
break; | ||
case 'YieldExpression': | ||
messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR; | ||
break; | ||
case 'NewExpression': | ||
parentDescription = `new ${parent.callee.name}(…)`; | ||
break; | ||
case 'CallExpression': | ||
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`; | ||
break; | ||
// No default | ||
// Useless iterable to array | ||
context.on('ArrayExpression', arrayExpression => { | ||
if (!isSingleArraySpread(arrayExpression)) { | ||
return; | ||
} | ||
const {parent} = arrayExpression; | ||
if (!( | ||
(parent.type === 'ForOfStatement' && parent.right === arrayExpression) | ||
|| (parent.type === 'YieldExpression' && parent.delegate && parent.argument === arrayExpression) | ||
|| ( | ||
( | ||
isNewExpression(parent, {names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1}) | ||
|| isNewExpression(parent, {names: typedArray, minimumArguments: 1}) | ||
|| isMethodCall(parent, { | ||
object: 'Promise', | ||
methods: ['all', 'allSettled', 'any', 'race'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isMethodCall(parent, { | ||
objects: ['Array', ...typedArray], | ||
method: 'from', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isMethodCall(parent, { | ||
object: 'Object', | ||
method: 'fromEntries', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
&& parent.arguments[0] === arrayExpression | ||
) | ||
)) { | ||
return; | ||
} | ||
let parentDescription = ''; | ||
let messageId = ITERABLE_TO_ARRAY; | ||
switch (parent.type) { | ||
case 'ForOfStatement': { | ||
messageId = ITERABLE_TO_ARRAY_IN_FOR_OF; | ||
break; | ||
} | ||
return { | ||
node: array, | ||
messageId, | ||
data: {parentDescription}, | ||
* fix(fixer) { | ||
if (parent.type === 'ForOfStatement') { | ||
yield * fixSpaceAroundKeyword(fixer, array, sourceCode); | ||
} | ||
case 'YieldExpression': { | ||
messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR; | ||
break; | ||
} | ||
const [ | ||
openingBracketToken, | ||
spreadToken, | ||
] = sourceCode.getFirstTokens(array, 2); | ||
case 'NewExpression': { | ||
parentDescription = `new ${parent.callee.name}(…)`; | ||
break; | ||
} | ||
// `[...iterable]` | ||
// ^ | ||
yield fixer.remove(openingBracketToken); | ||
case 'CallExpression': { | ||
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`; | ||
break; | ||
} | ||
// No default | ||
} | ||
// `[...iterable]` | ||
// ^^^ | ||
yield fixer.remove(spreadToken); | ||
return { | ||
node: arrayExpression, | ||
messageId, | ||
data: {parentDescription}, | ||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode), | ||
}; | ||
}); | ||
const [ | ||
commaToken, | ||
closingBracketToken, | ||
] = sourceCode.getLastTokens(array, 2); | ||
// Useless array clone | ||
context.on('ArrayExpression', arrayExpression => { | ||
if (!isSingleArraySpread(arrayExpression)) { | ||
return; | ||
} | ||
// `[...iterable]` | ||
// ^ | ||
yield fixer.remove(closingBracketToken); | ||
const node = arrayExpression.elements[0].argument; | ||
if (!( | ||
// Array methods returns a new array | ||
isMethodCall(node, { | ||
methods: [ | ||
'concat', | ||
'copyWithin', | ||
'filter', | ||
'flat', | ||
'flatMap', | ||
'map', | ||
'slice', | ||
'splice', | ||
'toReversed', | ||
'toSorted', | ||
'toSpliced', | ||
'with', | ||
], | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// `String#split()` | ||
|| isMethodCall(node, { | ||
method: 'split', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// `Object.keys()` and `Object.values()` | ||
|| isMethodCall(node, { | ||
object: 'Object', | ||
methods: ['keys', 'values'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// `await Promise.all()` and `await Promise.allSettled` | ||
|| ( | ||
node.type === 'AwaitExpression' | ||
&& isMethodCall(node.argument, { | ||
object: 'Promise', | ||
methods: ['all', 'allSettled'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
) | ||
// `Array.from()`, `Array.of()` | ||
|| isMethodCall(node, { | ||
object: 'Array', | ||
methods: ['from', 'of'], | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// `new Array()` | ||
|| isNewExpression(node, {name: 'Array'}) | ||
)) { | ||
return; | ||
} | ||
// `[...iterable,]` | ||
// ^ | ||
if (isCommaToken(commaToken)) { | ||
yield fixer.remove(commaToken); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
const problem = { | ||
node: arrayExpression, | ||
messageId: CLONE_ARRAY, | ||
}; | ||
if ( | ||
// `[...new Array(1)]` -> `new Array(1)` is not safe to fix since there are holes | ||
isNewExpression(node, {name: 'Array'}) | ||
// `[...foo.slice(1)]` -> `foo.slice(1)` is not safe to fix since `foo` can be a string | ||
|| ( | ||
node.type === 'CallExpression' | ||
&& node.callee.type === 'MemberExpression' | ||
&& node.callee.property.type === 'Identifier' | ||
&& node.callee.property.name === 'slice' | ||
) | ||
) { | ||
return problem; | ||
} | ||
return Object.assign(problem, { | ||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode), | ||
}); | ||
}); | ||
}; | ||
@@ -212,0 +372,0 @@ |
@@ -16,3 +16,3 @@ 'use strict'; | ||
const create = context => ({ | ||
* 'SwitchStatement[cases.length>1]'(switchStatement) { | ||
* SwitchStatement(switchStatement) { | ||
const {cases} = switchStatement; | ||
@@ -29,3 +29,3 @@ | ||
// We only check cases where the last case is the `default` case | ||
if (defaultCase !== cases[cases.length - 1]) { | ||
if (defaultCase !== cases.at(-1)) { | ||
return; | ||
@@ -48,3 +48,3 @@ } | ||
node, | ||
loc: getSwitchCaseHeadLocation(node, context.getSourceCode()), | ||
loc: getSwitchCaseHeadLocation(node, context.sourceCode), | ||
messageId: MESSAGE_ID_ERROR, | ||
@@ -51,0 +51,0 @@ suggest: [ |
'use strict'; | ||
const {isCommaToken} = require('eslint-utils'); | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const {replaceNodeOrTokenAndSpacesBefore} = require('./fix/index.js'); | ||
const {isUndefined, isFunction} = require('./ast/index.js'); | ||
@@ -10,30 +11,2 @@ const messageId = 'no-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([ | ||
@@ -68,3 +41,2 @@ 'is', | ||
&& node.computed === false | ||
&& node.property | ||
&& node.property.type === 'Identifier' | ||
@@ -76,12 +48,24 @@ ) { | ||
return compareFunctionNames.has(name) | ||
// `array.push(undefined)` | ||
|| name === 'push' | ||
// `array.unshift(undefined)` | ||
|| name === 'unshift' | ||
// `array.includes(undefined)` | ||
|| name === 'includes' | ||
// `set.add(undefined)` | ||
|| name === 'add' | ||
// `set.has(undefined)` | ||
|| name === 'has' | ||
// `map.set(foo, undefined)` | ||
|| name === 'set' | ||
// `array.push(undefined)` | ||
|| name === 'push' | ||
// `array.unshift(undefined)` | ||
|| name === 'unshift' | ||
// `React.createContext(undefined)` | ||
|| name === 'createContext'; | ||
|| name === 'createContext' | ||
// `setState(undefined)` | ||
|| /^set[A-Z]/.test(name) | ||
// https://vuejs.org/api/reactivity-core.html#ref | ||
|| name === 'ref'; | ||
}; | ||
@@ -104,8 +88,13 @@ | ||
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; | ||
@@ -118,9 +107,9 @@ } | ||
messageId, | ||
fix: fixer => fix(node, fixer), | ||
fix, | ||
}; | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const options = { | ||
checkArguments: true, | ||
checkArrowFunctionBody: true, | ||
...context.options[0], | ||
@@ -132,79 +121,158 @@ }; | ||
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), | ||
); | ||
} | ||
}); | ||
// `() => undefined` | ||
if (options.checkArrowFunctionBody) { | ||
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, | ||
); | ||
} | ||
}); | ||
} | ||
const argumentNodes = node.arguments; | ||
// `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, | ||
); | ||
} | ||
}); | ||
// Ignore arguments in `Function#bind()`, but not `this` argument | ||
if (isFunctionBindCall(node) && argumentNodes.length !== 1) { | ||
return; | ||
} | ||
// `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; | ||
const undefinedArguments = []; | ||
for (let index = argumentNodes.length - 1; index >= 0; index--) { | ||
const node = argumentNodes[index]; | ||
if (isUndefined(node)) { | ||
undefinedArguments.unshift(node); | ||
} else { | ||
break; | ||
} | ||
} | ||
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, '?') | ||
); | ||
} | ||
}, | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
if (undefinedArguments.length === 0) { | ||
return; | ||
if (!options.checkArguments) { | ||
return; | ||
} | ||
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; | ||
} | ||
} | ||
const firstUndefined = undefinedArguments[0]; | ||
const lastUndefined = undefinedArguments[undefinedArguments.length - 1]; | ||
if (undefinedArguments.length === 0) { | ||
return; | ||
} | ||
return { | ||
messageId, | ||
loc: { | ||
start: firstUndefined.loc.start, | ||
end: lastUndefined.loc.end, | ||
}, | ||
fix(fixer) { | ||
let start = firstUndefined.range[0]; | ||
let end = lastUndefined.range[1]; | ||
const firstUndefined = undefinedArguments[0]; | ||
const lastUndefined = undefinedArguments.at(-1); | ||
const previousArgument = argumentNodes[argumentNodes.length - undefinedArguments.length - 1]; | ||
return { | ||
messageId, | ||
loc: { | ||
start: firstUndefined.loc.start, | ||
end: lastUndefined.loc.end, | ||
}, | ||
fix(fixer) { | ||
let start = firstUndefined.range[0]; | ||
let end = lastUndefined.range[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]; | ||
} | ||
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; | ||
}); | ||
}; | ||
@@ -220,2 +288,5 @@ | ||
}, | ||
checkArrowFunctionBody: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
@@ -222,0 +293,0 @@ }, |
'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'); | ||
@@ -18,3 +19,3 @@ const MESSAGE_ZERO_FRACTION = 'zero-fraction'; | ||
Literal(node) { | ||
if (!isNumber(node)) { | ||
if (!isNumberLiteral(node)) { | ||
return; | ||
@@ -31,3 +32,3 @@ } | ||
const {before, dotAndFractions, after} = match.groups; | ||
const fixedDotAndFractions = dotAndFractions.replace(/[.0_]+$/g, ''); | ||
const fixedDotAndFractions = dotAndFractions.replaceAll(/[.0_]+$/g, ''); | ||
const formatted = ((before + fixedDotAndFractions) || '0') + after; | ||
@@ -43,3 +44,3 @@ | ||
const start = end - (raw.length - formatted.length); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
@@ -46,0 +47,0 @@ loc: toLocation([start, end], sourceCode), |
'use strict'; | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {isNumber, isBigInt} = require('./utils/numeric.js'); | ||
const {isNumberLiteral, isBigIntLiteral} = require('./ast/index.js'); | ||
@@ -25,5 +25,5 @@ const MESSAGE_ID = 'number-literal-case'; | ||
let fixed = raw; | ||
if (isNumber(node)) { | ||
if (isNumberLiteral(node)) { | ||
fixed = fix(raw); | ||
} else if (isBigInt(node)) { | ||
} else if (isBigIntLiteral(node)) { | ||
fixed = fix(raw.slice(0, -1)) + 'n'; | ||
@@ -30,0 +30,0 @@ } |
'use strict'; | ||
const numeric = require('./utils/numeric.js'); | ||
const {isBigIntLiteral} = require('./ast/index.js'); | ||
@@ -108,3 +109,3 @@ const MESSAGE_ID = 'numeric-separators-style'; | ||
let suffix = ''; | ||
if (numeric.isBigInt(node)) { | ||
if (isBigIntLiteral(node)) { | ||
number = raw.slice(0, -1); | ||
@@ -114,3 +115,3 @@ suffix = 'n'; | ||
const strippedNumber = number.replace(/_/g, ''); | ||
const strippedNumber = number.replaceAll('_', ''); | ||
const {prefix, data} = numeric.getPrefix(strippedNumber); | ||
@@ -117,0 +118,0 @@ |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const eventTypes = require('./shared/dom-events.js'); | ||
const {STATIC_REQUIRE_SOURCE_SELECTOR} = require('./selectors/index.js'); | ||
const {isUndefined, isNullLiteral, isStaticRequire} = require('./ast/index.js'); | ||
@@ -50,14 +50,4 @@ const MESSAGE_ID = 'prefer-add-event-listener'; | ||
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 */ | ||
@@ -86,4 +76,8 @@ const create = context => { | ||
[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; | ||
@@ -93,4 +87,4 @@ } | ||
'ImportDeclaration > Literal'(node) { | ||
if (!isDisabled && excludedPackages.has(node.value)) { | ||
Literal(node) { | ||
if (node.parent.type === 'ImportDeclaration' && !isDisabled && excludedPackages.has(node.value)) { | ||
isDisabled = true; | ||
@@ -109,3 +103,3 @@ } | ||
const {left: memberExpression, right: assignedExpression} = node; | ||
const {left: memberExpression, right: assignedExpression, operator} = node; | ||
@@ -148,4 +142,8 @@ if ( | ||
extra = extraMessages.error; | ||
} else { | ||
fix = fixer => fixCode(fixer, context.getSourceCode(), node, memberExpression); | ||
} else if ( | ||
operator === '=' | ||
&& node.parent.type === 'ExpressionStatement' | ||
&& node.parent.expression === node | ||
) { | ||
fix = fixer => fixCode(fixer, context.sourceCode, node, memberExpression); | ||
} | ||
@@ -152,0 +150,0 @@ |
'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 avoidCapture = require('./utils/avoid-capture.js'); | ||
const getScopes = require('./utils/get-scopes.js'); | ||
const singular = require('./utils/singular.js'); | ||
const { | ||
extendFixRange, | ||
@@ -18,5 +9,15 @@ removeMemberExpressionProperty, | ||
} = 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'; | ||
@@ -31,2 +32,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(…)`.', | ||
@@ -39,66 +42,10 @@ // Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case | ||
const filterMethodSelectorOptions = { | ||
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({ | ||
method: 'shift', | ||
argumentsLength: 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` | ||
@@ -214,7 +161,5 @@ // - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)` | ||
const isAccessingZeroIndex = node => | ||
node.parent | ||
&& node.parent.type === 'MemberExpression' | ||
node.parent.type === 'MemberExpression' | ||
&& node.parent.computed === true | ||
&& node.parent.object === node | ||
&& node.parent.property | ||
&& node.parent.property.type === 'Literal' | ||
@@ -229,4 +174,4 @@ && node.parent.property.raw === '0'; | ||
&& left.type === 'ArrayPattern' | ||
&& left.elements | ||
&& left.elements.length === 1 | ||
&& left.elements[0] | ||
&& left.elements[0].type !== 'RestElement'; | ||
@@ -237,95 +182,235 @@ }; | ||
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, getScopes(scope)); | ||
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} */ | ||
@@ -337,8 +422,9 @@ module.exports = { | ||
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, | ||
}, | ||
}; |
'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'); | ||
@@ -11,11 +11,2 @@ | ||
const selector = [ | ||
methodCallSelector('flat'), | ||
matches([ | ||
'[arguments.length=0]', | ||
'[arguments.length=1][arguments.0.type="Literal"][arguments.0.raw="1"]', | ||
]), | ||
methodCallSelector({path: 'callee.object', method: 'map'}), | ||
].join(''); | ||
const ignored = ['React.Children', 'Children']; | ||
@@ -25,3 +16,27 @@ | ||
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; | ||
@@ -32,3 +47,3 @@ if (isNodeMatches(mapCallExpression.callee.object, ignored)) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const mapProperty = mapCallExpression.callee.property; | ||
@@ -35,0 +50,0 @@ |
'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'); | ||
@@ -19,17 +23,26 @@ const MESSAGE_ID = 'prefer-array-flat'; | ||
const isEmptyArrayExpression = node => | ||
node.type === 'ArrayExpression' | ||
&& node.elements.length === 0; | ||
// `array.flatMap(x => x)` | ||
const arrayFlatMap = { | ||
selector: [ | ||
methodCallSelector({ | ||
testFunction(node) { | ||
if (!isMethodCall(node, { | ||
method: 'flatMap', | ||
argumentsLength: 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, | ||
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, | ||
@@ -40,54 +53,50 @@ description: 'Array#flatMap()', | ||
// `array.reduce((a, b) => a.concat(b), [])` | ||
// `array.reduce((a, b) => [...a, ...b], [])` | ||
const arrayReduce = { | ||
selector: [ | ||
methodCallSelector({ | ||
testFunction(node) { | ||
if (!isMethodCall(node, { | ||
method: 'reduce', | ||
argumentsLength: 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({ | ||
method: 'concat', | ||
argumentsLength: 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()', | ||
}; | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return false; | ||
} | ||
// `array.reduce((a, b) => [...a, ...b], [])` | ||
const arrayReduce2 = { | ||
selector: [ | ||
methodCallSelector({ | ||
method: 'reduce', | ||
argumentsLength: 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, | ||
@@ -97,12 +106,15 @@ description: 'Array#reduce()', | ||
// `[].concat(maybeArray)` and `[].concat(...array)` | ||
// `[].concat(maybeArray)` | ||
// `[].concat(...array)` | ||
const emptyArrayConcat = { | ||
selector: [ | ||
methodCallSelector({ | ||
testFunction(node) { | ||
return isMethodCall(node, { | ||
method: 'concat', | ||
argumentsLength: 1, | ||
allowSpreadElement: true, | ||
}), | ||
emptyArraySelector('callee.object'), | ||
].join(''), | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isEmptyArrayExpression(node.callee.object); | ||
}, | ||
getArrayNode(node) { | ||
@@ -120,15 +132,25 @@ const argumentNode = node.arguments[0]; | ||
const arrayPrototypeConcat = { | ||
selector: [ | ||
methodCallSelector({ | ||
methods: ['apply', 'call'], | ||
argumentsLength: 2, | ||
allowSpreadElement: true, | ||
}), | ||
emptyArraySelector('arguments.0'), | ||
arrayPrototypeMethodSelector({ | ||
path: 'callee.object', | ||
method: 'concat', | ||
}), | ||
].join(''), | ||
testFunction: node => node.arguments[1].type !== 'SpreadElement' || node.callee.property.name === 'call', | ||
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) { | ||
@@ -147,6 +169,2 @@ const argumentNode = node.arguments[1]; | ||
]; | ||
const anyCall = { | ||
selector: callExpressionSelector({argumentsLength: 1}), | ||
getArrayNode: node => node.arguments[0], | ||
}; | ||
@@ -190,4 +208,2 @@ function fix(node, array, sourceCode, shouldSwitchToArray) { | ||
const functions = [...configFunctions, ...lodashFlattenFunctions]; | ||
const sourceCode = context.getSourceCode(); | ||
const listeners = {}; | ||
@@ -197,8 +213,10 @@ const cases = [ | ||
arrayReduce, | ||
arrayReduce2, | ||
emptyArrayConcat, | ||
arrayPrototypeConcat, | ||
{ | ||
...anyCall, | ||
testFunction: node => isNodeMatches(node.callee, functions), | ||
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()}()`, | ||
@@ -208,33 +226,35 @@ }, | ||
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,0 +261,0 @@ |
'use strict'; | ||
const simpleArraySearchRule = require('./shared/simple-array-search-rule.js'); | ||
const {messages, createListeners} = simpleArraySearchRule({ | ||
const indexOfOverFindIndexRule = simpleArraySearchRule({ | ||
method: 'findIndex', | ||
@@ -9,14 +9,25 @@ 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', | ||
hasSuggestions: true, | ||
messages, | ||
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 isLiteralValue = require('./utils/is-literal-value.js'); | ||
const { | ||
isBooleanNode, | ||
getParenthesizedRange, | ||
isNodeValueNotFunction, | ||
} = require('./utils/index.js'); | ||
const {removeMemberExpressionProperty} = require('./fix/index.js'); | ||
const {isLiteral, isUndefined, isMethodCall, isMemberExpression} = require('./ast/index.js'); | ||
@@ -13,13 +15,7 @@ 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_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({ | ||
method: 'find', | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
}); | ||
const isCheckingUndefined = node => | ||
@@ -37,4 +33,3 @@ node.parent.type === 'BinaryExpression' | ||
) | ||
&& node.parent.right.type === 'Identifier' | ||
&& node.parent.right.name === 'undefined' | ||
&& isUndefined(node.parent.right) | ||
) | ||
@@ -47,30 +42,29 @@ || ( | ||
// eslint-disable-next-line unicorn/no-null | ||
&& isLiteralValue(node.parent.right, null) | ||
&& isLiteral(node.parent.right, null) | ||
) | ||
); | ||
const arrayFilterCallSelector = [ | ||
'BinaryExpression', | ||
'[right.type="Literal"]', | ||
'[right.raw="0"]', | ||
// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule. | ||
matches(['[operator=">"]', '[operator="!=="]']), | ||
' > ', | ||
`${memberExpressionSelector('length')}.left`, | ||
' > ', | ||
`${methodCallSelector('filter')}.object`, | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[arrayFindCallSelector](findCall) { | ||
const isCompare = isCheckingUndefined(findCall); | ||
if (!isCompare && !isBooleanNode(findCall)) { | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
methods: ['find', 'findLast'], | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const findProperty = findCall.callee.property; | ||
const isCompare = isCheckingUndefined(callExpression); | ||
if (!isCompare && !isBooleanNode(callExpression)) { | ||
return; | ||
} | ||
const methodNode = callExpression.callee.property; | ||
return { | ||
node: findProperty, | ||
node: methodNode, | ||
messageId: ERROR_ID_ARRAY_SOME, | ||
data: {method: methodNode.name}, | ||
suggest: [ | ||
@@ -80,3 +74,3 @@ { | ||
* fix(fixer) { | ||
yield fixer.replaceText(findProperty, 'some'); | ||
yield fixer.replaceText(methodNode, 'some'); | ||
@@ -87,6 +81,6 @@ if (!isCompare) { | ||
const parenthesizedRange = getParenthesizedRange(findCall, context.getSourceCode()); | ||
yield fixer.replaceTextRange([parenthesizedRange[1], findCall.parent.range[1]], ''); | ||
const parenthesizedRange = getParenthesizedRange(callExpression, context.sourceCode); | ||
yield fixer.replaceTextRange([parenthesizedRange[1], callExpression.parent.range[1]], ''); | ||
if (findCall.parent.operator === '!=' || findCall.parent.operator === '!==') { | ||
if (callExpression.parent.operator === '!=' || callExpression.parent.operator === '!==') { | ||
return; | ||
@@ -101,3 +95,24 @@ } | ||
}, | ||
[arrayFilterCallSelector](filterCall) { | ||
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; | ||
@@ -111,4 +126,4 @@ return { | ||
const sourceCode = context.getSourceCode(); | ||
const lengthNode = filterCall.parent; | ||
const {sourceCode} = context; | ||
const lengthNode = binaryExpression.left; | ||
/* | ||
@@ -121,3 +136,2 @@ Remove `.length` | ||
const compareNode = lengthNode.parent; | ||
/* | ||
@@ -130,3 +144,3 @@ Remove `> 0` | ||
getParenthesizedRange(lengthNode, sourceCode)[1], | ||
compareNode.range[1], | ||
binaryExpression.range[1], | ||
]); | ||
@@ -146,3 +160,3 @@ | ||
docs: { | ||
description: 'Prefer `.some(…)` over `.filter(…).length` check and `.find(…)`.', | ||
description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`.', | ||
}, | ||
@@ -149,0 +163,0 @@ fixable: 'code', |
'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 { | ||
@@ -8,7 +7,7 @@ isParenthesized, | ||
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'); | ||
isNodeMatchesNameOrPath, | ||
needsSemicolon, | ||
shouldAddParenthesesToMemberExpressionObject, | ||
isLeftHandSide, | ||
} = require('./utils/index.js'); | ||
const { | ||
@@ -18,4 +17,4 @@ getNegativeIndexLengthNode, | ||
} = 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'); | ||
@@ -39,10 +38,2 @@ const MESSAGE_ID_NEGATIVE_INDEX = 'negative-index'; | ||
const indexAccess = [ | ||
'MemberExpression', | ||
'[optional!=true]', | ||
'[computed!=false]', | ||
notLeftHandSideSelector(), | ||
].join(''); | ||
const sliceCall = methodCallSelector({method: 'slice', minimumArguments: 1, maximumArguments: 2}); | ||
const stringCharAt = methodCallSelector({method: 'charAt', argumentsLength: 1}); | ||
const isArguments = node => node.type === 'Identifier' && node.name === 'arguments'; | ||
@@ -63,3 +54,3 @@ | ||
&& parent.object === node | ||
&& isLiteralValue(parent.property, 0); | ||
&& isLiteral(parent.property, 0); | ||
}; | ||
@@ -153,150 +144,204 @@ | ||
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; | ||
} | ||
const problem = { | ||
node: indexNode, | ||
messageId: lengthNode ? MESSAGE_ID_NEGATIVE_INDEX : MESSAGE_ID_INDEX, | ||
}; | ||
// 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; | ||
} | ||
} | ||
if (isArguments(node.object)) { | ||
return problem; | ||
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); | ||
} | ||
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]]); | ||
} | ||
} | ||
const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken); | ||
yield fixer.replaceText(openingBracketToken, '.at('); | ||
const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken); | ||
yield fixer.replaceText(openingBracketToken, '.at('); | ||
const closingBracketToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken); | ||
yield fixer.replaceText(closingBracketToken, ')'); | ||
}; | ||
const closingBracketToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken); | ||
yield fixer.replaceText(closingBracketToken, ')'); | ||
}; | ||
return problem; | ||
}, | ||
[stringCharAt](node) { | ||
const [indexNode] = node.arguments; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.callee.object); | ||
return problem; | ||
}); | ||
// `String#charAt` don't care about index value, we assume it's always number | ||
if (!lengthNode && !checkAllIndexAccess) { | ||
return; | ||
} | ||
// `string.charAt` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'charAt', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
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 [indexNode] = node.arguments; | ||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.callee.object); | ||
yield fixer.replaceText(node.callee.property, 'at'); | ||
}, | ||
}], | ||
}; | ||
}, | ||
[sliceCall](sliceCall) { | ||
const result = checkSliceCall(sliceCall); | ||
if (!result) { | ||
return; | ||
} | ||
// `String#charAt` don't care about index value, we assume it's always number | ||
if (!lengthNode && !checkAllIndexAccess) { | ||
return; | ||
} | ||
const {safeToFix, firstElementGetMethod} = result; | ||
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); | ||
} | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
function * fix(fixer) { | ||
// `.slice` to `.at` | ||
yield fixer.replaceText(sliceCall.callee.property, 'at'); | ||
yield fixer.replaceText(node.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]); | ||
} | ||
// `.slice()` | ||
context.on('CallExpression', sliceCall => { | ||
if (!isMethodCall(sliceCall, { | ||
method: 'slice', | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
// Remove `[0]`, `.shift()`, or `.pop()` | ||
if (firstElementGetMethod === 'zero-index') { | ||
yield removeMemberExpressionProperty(fixer, sliceCall.parent, sourceCode); | ||
} else { | ||
yield * removeMethodCall(fixer, sliceCall.parent.parent, sourceCode); | ||
} | ||
} | ||
const result = checkSliceCall(sliceCall); | ||
if (!result) { | ||
return; | ||
} | ||
const problem = { | ||
node: sliceCall.callee.property, | ||
messageId: MESSAGE_ID_SLICE, | ||
}; | ||
const {safeToFix, firstElementGetMethod} = result; | ||
if (safeToFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_ID, fix}]; | ||
/** @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]); | ||
} | ||
return problem; | ||
}, | ||
[callExpressionSelector({argumentsLength: 1})](node) { | ||
const matchedFunction = getLastFunctions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)); | ||
if (!matchedFunction) { | ||
return; | ||
// Remove `[0]`, `.shift()`, or `.pop()` | ||
if (firstElementGetMethod === 'zero-index') { | ||
yield removeMemberExpressionProperty(fixer, sliceCall.parent, sourceCode); | ||
} else { | ||
yield * removeMethodCall(fixer, sliceCall.parent.parent, sourceCode); | ||
} | ||
} | ||
const problem = { | ||
node: node.callee, | ||
messageId: MESSAGE_ID_GET_LAST_FUNCTION, | ||
data: {description: matchedFunction.trim()}, | ||
}; | ||
const problem = { | ||
node: sliceCall.callee.property, | ||
messageId: MESSAGE_ID_SLICE, | ||
}; | ||
const [array] = node.arguments; | ||
if (safeToFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_ID, fix}]; | ||
} | ||
if (isArguments(array)) { | ||
return problem; | ||
} | ||
return problem; | ||
}); | ||
problem.fix = function (fixer) { | ||
let fixed = getParenthesizedText(array, sourceCode); | ||
context.on('CallExpression', node => { | ||
if (!isCallExpression(node, {argumentsLength: 1, optional: false})) { | ||
return; | ||
} | ||
if ( | ||
!isParenthesized(array, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
} | ||
const matchedFunction = getLastFunctions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)); | ||
if (!matchedFunction) { | ||
return; | ||
} | ||
fixed = `${fixed}.at(-1)`; | ||
const problem = { | ||
node: node.callee, | ||
messageId: MESSAGE_ID_GET_LAST_FUNCTION, | ||
data: {description: matchedFunction.trim()}, | ||
}; | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
if (needsSemicolon(tokenBefore, sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
const [array] = node.arguments; | ||
return fixer.replaceText(node, fixed); | ||
}; | ||
if (isArguments(array)) { | ||
return problem; | ||
} | ||
return problem; | ||
}, | ||
}; | ||
problem.fix = function (fixer) { | ||
let fixed = getParenthesizedText(array, sourceCode); | ||
if ( | ||
!isParenthesized(array, sourceCode) | ||
&& shouldAddParenthesesToMemberExpressionObject(array, sourceCode) | ||
) { | ||
fixed = `(${fixed})`; | ||
} | ||
fixed = `${fixed}.at(-1)`; | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
if (needsSemicolon(tokenBefore, sourceCode, fixed)) { | ||
fixed = `;${fixed}`; | ||
} | ||
return fixer.replaceText(node, fixed); | ||
}; | ||
return problem; | ||
}); | ||
} | ||
@@ -303,0 +348,0 @@ |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -7,40 +7,51 @@ const messages = { | ||
'error/fromCharCode': 'Prefer `String.fromCodePoint()` over `String.fromCharCode()`.', | ||
'suggestion/charCodeAt': 'Use `String#codePointAt()`.', | ||
'suggestion/fromCharCode': 'Use `String.fromCodePoint()`.', | ||
'suggestion/codePointAt': 'Use `String#codePointAt()`.', | ||
'suggestion/fromCodePoint': 'Use `String.fromCodePoint()`.', | ||
}; | ||
const cases = [ | ||
{ | ||
selector: methodCallSelector('charCodeAt'), | ||
replacement: 'codePointAt', | ||
}, | ||
{ | ||
selector: methodCallSelector({object: 'String', method: 'fromCharCode'}), | ||
replacement: 'fromCodePoint', | ||
}, | ||
]; | ||
const getReplacement = node => { | ||
if (isMethodCall(node, { | ||
method: 'charCodeAt', | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return 'codePointAt'; | ||
} | ||
if (isMethodCall(node, { | ||
object: 'String', | ||
method: 'fromCharCode', | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return 'fromCodePoint'; | ||
} | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => Object.fromEntries( | ||
cases.map(({selector, replacement}) => [ | ||
selector, | ||
node => { | ||
const method = node.callee.property; | ||
const methodName = method.name; | ||
const fix = fixer => fixer.replaceText(method, replacement); | ||
const create = () => ({ | ||
CallExpression(node) { | ||
const replacement = getReplacement(node); | ||
return { | ||
node: method, | ||
messageId: `error/${methodName}`, | ||
suggest: [ | ||
{ | ||
messageId: `suggestion/${methodName}`, | ||
fix, | ||
}, | ||
], | ||
}; | ||
}, | ||
]), | ||
); | ||
if (!replacement) { | ||
return; | ||
} | ||
const method = node.callee.property; | ||
const methodName = method.name; | ||
const fix = fixer => fixer.replaceText(method, replacement); | ||
return { | ||
node: method, | ||
messageId: `error/${methodName}`, | ||
suggest: [ | ||
{ | ||
messageId: `suggestion/${replacement}`, | ||
fix, | ||
}, | ||
], | ||
}; | ||
}, | ||
}); | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
@@ -47,0 +58,0 @@ module.exports = { |
'use strict'; | ||
const { | ||
matches, | ||
methodCallSelector, | ||
newExpressionSelector, | ||
callExpressionSelector, | ||
} = require('./selectors/index.js'); | ||
isMethodCall, | ||
isCallExpression, | ||
isNewExpression, | ||
} = require('./ast/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
@@ -19,38 +18,3 @@ | ||
const createNewDateSelector = path => newExpressionSelector({path, name: 'Date', argumentsLength: 0}); | ||
const operatorsSelector = (...operators) => matches(operators.map(operator => `[operator="${operator}"]`)); | ||
// `new Date()` | ||
const newDateSelector = createNewDateSelector(); | ||
// `new Date().{getTime,valueOf}()` | ||
const methodsSelector = [ | ||
methodCallSelector({ | ||
methods: ['getTime', 'valueOf'], | ||
argumentsLength: 0, | ||
}), | ||
createNewDateSelector('callee.object'), | ||
].join(''); | ||
// `{Number,BigInt}(new Date())` | ||
const builtinObjectSelector = [ | ||
callExpressionSelector({names: ['Number', 'BigInt'], argumentsLength: 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}); | ||
@@ -72,32 +36,88 @@ const getProblem = (node, problem, sourceCode) => ({ | ||
const create = context => ({ | ||
[methodsSelector](node) { | ||
const method = node.callee.property; | ||
return getProblem(node, { | ||
node: method, | ||
messageId: MESSAGE_ID_METHOD, | ||
data: {method: method.name}, | ||
}); | ||
}, | ||
[builtinObjectSelector](node) { | ||
const {name} = node.callee; | ||
if (name === 'Number') { | ||
return getProblem(node, { | ||
messageId: MESSAGE_ID_NUMBER, | ||
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}, | ||
}); | ||
} | ||
return getProblem(node.arguments[0]); | ||
// `{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(callExpression, { | ||
messageId: MESSAGE_ID_NUMBER, | ||
}); | ||
} | ||
return getProblem(callExpression.arguments[0]); | ||
} | ||
}, | ||
[unaryExpressionsSelector](node) { | ||
return getProblem( | ||
node.operator === '-' ? node.argument : node, | ||
{}, | ||
context.getSourceCode(), | ||
); | ||
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, | ||
); | ||
} | ||
}, | ||
[assignmentExpressionSelector](node) { | ||
return getProblem(node); | ||
AssignmentExpression(assignmentExpression) { | ||
if ( | ||
assignmentExpression.operator !== '-=' | ||
&& assignmentExpression.operator !== '*=' | ||
&& assignmentExpression.operator !== '/=' | ||
&& assignmentExpression.operator !== '%=' | ||
&& assignmentExpression.operator !== '**=' | ||
) { | ||
return; | ||
} | ||
if (isNewDate(assignmentExpression.right)) { | ||
return getProblem(assignmentExpression.right); | ||
} | ||
}, | ||
[binaryExpressionSelector](node) { | ||
return getProblem(node); | ||
* 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); | ||
} | ||
} | ||
}, | ||
@@ -104,0 +124,0 @@ }); |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const {functionTypes} = require('./ast/index.js'); | ||
@@ -7,12 +8,2 @@ const MESSAGE_ID = 'preferDefaultParameters'; | ||
const assignmentSelector = [ | ||
'ExpressionStatement', | ||
'[expression.type="AssignmentExpression"]', | ||
].join(''); | ||
const declarationSelector = [ | ||
'VariableDeclaration', | ||
'[declarations.0.type="VariableDeclarator"]', | ||
].join(''); | ||
const isDefaultExpression = (left, right) => | ||
@@ -85,3 +76,3 @@ left | ||
const isLastParameter = (parameters, parameter) => { | ||
const lastParameter = parameters[parameters.length - 1]; | ||
const lastParameter = parameters.at(-1); | ||
@@ -132,7 +123,7 @@ // See 'default-param-last' rule | ||
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); | ||
@@ -154,3 +145,3 @@ if (!currentFunction || !isDefaultExpression(left, right)) { | ||
const variable = findVariable(context.getScope(), secondId); | ||
const variable = findVariable(sourceCode.getScope(node), secondId); | ||
@@ -196,20 +187,21 @@ // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1122 | ||
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); | ||
} | ||
}); | ||
}; | ||
@@ -216,0 +208,0 @@ |
'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'); | ||
@@ -9,14 +9,18 @@ const MESSAGE_ID = 'prefer-dom-node-append'; | ||
}; | ||
const selector = [ | ||
methodCallSelector({ | ||
method: 'appendChild', | ||
argumentsLength: 1, | ||
}), | ||
notDomNodeSelector('callee.object'), | ||
notDomNodeSelector('arguments.0'), | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
[selector](node) { | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'appendChild', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
}) | ||
|| isNodeValueNotDomNode(node.callee.object) | ||
|| isNodeValueNotDomNode(node.arguments[0]) | ||
) { | ||
return; | ||
} | ||
const fix = isValueNotUsable(node) | ||
@@ -23,0 +27,0 @@ ? fixer => fixer.replaceText(node.callee.property, 'append') |
'use strict'; | ||
const isValidVariableName = require('./utils/is-valid-variable-name.js'); | ||
const quoteString = require('./utils/quote-string.js'); | ||
const {methodCallSelector, matches} = 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'); | ||
@@ -11,34 +15,28 @@ const MESSAGE_ID = 'prefer-dom-node-dataset'; | ||
const selector = [ | ||
matches([ | ||
methodCallSelector({method: 'setAttribute', argumentsLength: 2}), | ||
methodCallSelector({methods: ['getAttribute', 'removeAttribute', 'hasAttribute'], argumentsLength: 1}), | ||
]), | ||
'[arguments.0.type="Literal"]', | ||
].join(''); | ||
const dashToCamelCase = string => string.replaceAll(/-[a-z]/g, s => s[1].toUpperCase()); | ||
const dashToCamelCase = string => string.replace(/-[a-z]/g, s => s[1].toUpperCase()); | ||
function getFix(callExpression, context) { | ||
const method = callExpression.callee.property.name; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
const [nameNode] = node.arguments; | ||
let attributeName = nameNode.value; | ||
// `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; | ||
} | ||
if (typeof attributeName !== 'string') { | ||
return; | ||
} | ||
// `element.setAttribute(…)` returns `undefined`, but `AssignmentExpression` returns value of RHS | ||
if (method === 'setAttribute' && !isValueNotUsable(callExpression)) { | ||
return; | ||
} | ||
attributeName = attributeName.toLowerCase(); | ||
if (method === 'removeAttribute' && !isExpressionStatement(callExpression.parent)) { | ||
return; | ||
} | ||
if (!attributeName.startsWith('data-')) { | ||
return; | ||
} | ||
const method = node.callee.property.name; | ||
const name = dashToCamelCase(attributeName.slice(5)); | ||
const sourceCode = context.getSourceCode(); | ||
return fixer => { | ||
const [nameNode] = callExpression.arguments; | ||
const name = dashToCamelCase(nameNode.value.toLowerCase().slice(5)); | ||
const {sourceCode} = context; | ||
let text = ''; | ||
const datasetText = `${sourceCode.getText(node.callee.object)}.dataset`; | ||
const datasetText = `${sourceCode.getText(callExpression.callee.object)}.dataset`; | ||
switch (method) { | ||
@@ -48,6 +46,6 @@ case 'setAttribute': | ||
case 'removeAttribute': { | ||
text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`; | ||
text = isIdentifierName(name) ? `.${name}` : `[${escapeString(name, nameNode.raw.charAt(0))}]`; | ||
text = `${datasetText}${text}`; | ||
if (method === 'setAttribute') { | ||
text += ` = ${sourceCode.getText(node.arguments[1])}`; | ||
text += ` = ${sourceCode.getText(callExpression.arguments[1])}`; | ||
} else if (method === 'removeAttribute') { | ||
@@ -64,13 +62,47 @@ text = `delete ${text}`; | ||
case 'hasAttribute': | ||
text = `Object.hasOwn(${datasetText}, ${quoteString(name, nameNode.raw.charAt(0))})`; | ||
case 'hasAttribute': { | ||
text = `Object.hasOwn(${datasetText}, ${escapeString(name, nameNode.raw.charAt(0))})`; | ||
break; | ||
} | ||
// No default | ||
} | ||
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, | ||
node: callExpression, | ||
messageId: MESSAGE_ID, | ||
data: {method}, | ||
fix: fixer => fixer.replaceText(node, text), | ||
data: {method: callExpression.callee.property.name}, | ||
fix: getFix(callExpression, context), | ||
}; | ||
@@ -77,0 +109,0 @@ }, |
'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,20 +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({ | ||
method: 'removeChild', | ||
argumentsLength: 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; | ||
@@ -40,3 +55,5 @@ const childNode = node.arguments[0]; | ||
const fix = fixer => { | ||
const isOptionalParentNode = isMemberExpressionOptionalObject(parentNode); | ||
const createFix = (optional = false) => fixer => { | ||
let childNodeText = getParenthesizedText(childNode, sourceCode); | ||
@@ -54,16 +71,38 @@ if ( | ||
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; | ||
@@ -70,0 +109,0 @@ }, |
'use strict'; | ||
const {memberExpressionSelector} = require('./selectors/index.js'); | ||
const {isMemberExpression} = require('./ast/index.js'); | ||
@@ -11,17 +11,15 @@ const ERROR = 'error'; | ||
const memberExpressionPropertySelector = `${memberExpressionSelector('innerText')} > .property`; | ||
const destructuringSelector = [ | ||
'ObjectPattern', | ||
' > ', | ||
'Property.properties', | ||
'[kind="init"]', | ||
'[computed!=true]', | ||
' > ', | ||
'Identifier.key', | ||
'[name="innerText"]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
[memberExpressionPropertySelector](node) { | ||
MemberExpression(memberExpression) { | ||
if ( | ||
!isMemberExpression(memberExpression, { | ||
property: 'innerText', | ||
}) | ||
) { | ||
return; | ||
} | ||
const node = memberExpression.property; | ||
return { | ||
@@ -38,3 +36,15 @@ node, | ||
}, | ||
[destructuringSelector](node) { | ||
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 { | ||
@@ -41,0 +51,0 @@ node, |
@@ -6,3 +6,6 @@ 'use strict'; | ||
isClosingBraceToken, | ||
} = require('eslint-utils'); | ||
} = require('@eslint-community/eslint-utils'); | ||
const { | ||
isStringLiteral, | ||
} = require('./ast/index.js'); | ||
@@ -22,6 +25,9 @@ const MESSAGE_ID_ERROR = 'error'; | ||
switch (node.type) { | ||
case 'Identifier': | ||
case 'Identifier': { | ||
return Symbol.for(node.name); | ||
case 'Literal': | ||
} | ||
case 'Literal': { | ||
return node.value; | ||
} | ||
// No default | ||
@@ -147,3 +153,3 @@ } | ||
if (exportDeclaration) { | ||
const lastSpecifier = exportDeclaration.specifiers[exportDeclaration.specifiers.length - 1]; | ||
const lastSpecifier = exportDeclaration.specifiers.at(-1); | ||
@@ -173,6 +179,6 @@ // `export {} from 'foo';` | ||
function getExported(identifier, context, sourceCode) { | ||
function getExported(identifier, sourceCode) { | ||
const {parent} = identifier; | ||
switch (parent.type) { | ||
case 'ExportDefaultDeclaration': | ||
case 'ExportDefaultDeclaration': { | ||
return { | ||
@@ -184,4 +190,5 @@ node: parent, | ||
}; | ||
} | ||
case 'ExportSpecifier': | ||
case 'ExportSpecifier': { | ||
return { | ||
@@ -193,2 +200,3 @@ node: parent, | ||
}; | ||
} | ||
@@ -205,3 +213,3 @@ case 'VariableDeclarator': { | ||
&& parent.parent.parent.type === 'ExportNamedDeclaration' | ||
&& isVariableUnused(parent, context) | ||
&& isVariableUnused(parent, sourceCode) | ||
) { | ||
@@ -222,4 +230,4 @@ return { | ||
function isVariableUnused(node, context) { | ||
const variables = context.getDeclaredVariables(node); | ||
function isVariableUnused(node, sourceCode) { | ||
const variables = sourceCode.getDeclaredVariables(node); | ||
@@ -248,3 +256,3 @@ /* c8 ignore next 3 */ | ||
switch (specifier.type) { | ||
case 'ImportDefaultSpecifier': | ||
case 'ImportDefaultSpecifier': { | ||
return { | ||
@@ -255,4 +263,5 @@ name: DEFAULT_SPECIFIER_NAME, | ||
}; | ||
} | ||
case 'ImportSpecifier': | ||
case 'ImportSpecifier': { | ||
return { | ||
@@ -263,4 +272,5 @@ name: getSpecifierName(specifier.imported), | ||
}; | ||
} | ||
case 'ImportNamespaceSpecifier': | ||
case 'ImportNamespaceSpecifier': { | ||
return { | ||
@@ -271,2 +281,3 @@ name: NAMESPACE_SPECIFIER_NAME, | ||
}; | ||
} | ||
@@ -277,6 +288,6 @@ // No default | ||
function getExports(imported, context, sourceCode) { | ||
function getExports(imported, sourceCode) { | ||
const exports = []; | ||
for (const {identifier} of imported.variable.references) { | ||
const exported = getExported(identifier, context, sourceCode); | ||
const exported = getExported(identifier, sourceCode); | ||
@@ -320,3 +331,3 @@ if (!exported) { | ||
function create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {ignoreUsedVariables} = {ignoreUsedVariables: false, ...context.options[0]}; | ||
@@ -327,12 +338,16 @@ const importDeclarations = new Set(); | ||
return { | ||
'ImportDeclaration[specifiers.length>0]'(node) { | ||
importDeclarations.add(node); | ||
ImportDeclaration(node) { | ||
if (node.specifiers.length > 0) { | ||
importDeclarations.add(node); | ||
} | ||
}, | ||
// `ExportAllDeclaration` and `ExportDefaultDeclaration` can't be reused | ||
'ExportNamedDeclaration[source.type="Literal"]'(node) { | ||
exportDeclarations.push(node); | ||
ExportNamedDeclaration(node) { | ||
if (isStringLiteral(node.source)) { | ||
exportDeclarations.push(node); | ||
} | ||
}, | ||
* 'Program:exit'(program) { | ||
for (const importDeclaration of importDeclarations) { | ||
let variables = context.getDeclaredVariables(importDeclaration); | ||
let variables = sourceCode.getDeclaredVariables(importDeclaration); | ||
@@ -345,3 +360,3 @@ if (variables.some(variable => variable.defs.length !== 1 || variable.defs[0].parent !== importDeclaration)) { | ||
const imported = getImported(variable, sourceCode); | ||
const exports = getExports(imported, context, sourceCode); | ||
const exports = getExports(imported, sourceCode); | ||
@@ -348,0 +363,0 @@ return { |
'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'); | ||
@@ -14,7 +14,7 @@ const MESSAGE_ID = 'prefer-includes'; | ||
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; | ||
@@ -47,4 +47,6 @@ const dotToken = sourceCode.getTokenBefore(memberExpressionNode.property); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
BinaryExpression(node) { | ||
const create = context => { | ||
includesOverSomeRule.listen(context); | ||
context.on('BinaryExpression', node => { | ||
const {left, right, operator} = node; | ||
@@ -80,5 +82,4 @@ | ||
} | ||
}, | ||
...includesOverSomeRule.createListeners(context), | ||
}); | ||
}); | ||
}; | ||
@@ -85,0 +86,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {findVariable, getStaticValue, getPropertyName} = require('eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {findVariable, getStaticValue, getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {removeArgument} = require('./fix/index.js'); | ||
@@ -11,11 +11,2 @@ | ||
const jsonParseArgumentSelector = [ | ||
methodCallSelector({ | ||
object: 'JSON', | ||
method: 'parse', | ||
argumentsLength: 1, | ||
}), | ||
' > .arguments:first-child', | ||
].join(''); | ||
const getAwaitExpressionArgument = node => { | ||
@@ -63,6 +54,4 @@ while (node.type === 'AwaitExpression') { | ||
const isUtf8EncodingStringNode = (node, scope) => { | ||
const staticValue = getStaticValue(node, scope); | ||
return staticValue && isUtf8EncodingString(staticValue.value); | ||
}; | ||
const isUtf8EncodingStringNode = (node, scope) => | ||
isUtf8EncodingString(getStaticValue(node, scope)?.value); | ||
@@ -76,2 +65,3 @@ const isUtf8EncodingString = value => { | ||
// eslint-disable-next-line unicorn/text-encoding-identifier-case | ||
return value === 'utf8' || value === 'utf-8'; | ||
@@ -114,4 +104,16 @@ }; | ||
const create = context => ({ | ||
[jsonParseArgumentSelector](node) { | ||
const scope = context.getScope(); | ||
CallExpression(callExpression) { | ||
if (!(isMethodCall(callExpression, { | ||
object: 'JSON', | ||
method: 'parse', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}))) { | ||
return; | ||
} | ||
let [node] = callExpression.arguments; | ||
const {sourceCode} = context; | ||
const scope = sourceCode.getScope(node); | ||
node = getIdentifierDeclaration(node, scope); | ||
@@ -145,3 +147,3 @@ if ( | ||
messageId: MESSAGE_ID, | ||
fix: fixer => removeArgument(fixer, charsetNode, context.getSourceCode()), | ||
fix: fixer => removeArgument(fixer, charsetNode, sourceCode), | ||
}; | ||
@@ -148,0 +150,0 @@ }, |
'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'); | ||
@@ -17,7 +18,4 @@ const MESSAGE_ID = 'prefer-keyboard-event-key'; | ||
const isPropertyNamedAddEventListener = node => | ||
node | ||
&& node.type === 'CallExpression' | ||
&& node.callee | ||
node?.type === 'CallExpression' | ||
&& node.callee.type === 'MemberExpression' | ||
&& node.callee.property | ||
&& node.callee.property.name === 'addEventListener'; | ||
@@ -27,10 +25,10 @@ | ||
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], | ||
event: callback.params[0], | ||
references, | ||
@@ -40,4 +38,5 @@ }; | ||
default: | ||
default: { | ||
return {}; | ||
} | ||
} | ||
@@ -47,6 +46,3 @@ }; | ||
const isPropertyOf = (node, eventNode) => | ||
node | ||
&& node.parent | ||
&& node.parent.type === 'MemberExpression' | ||
&& node.parent.object | ||
node?.parent?.type === 'MemberExpression' | ||
&& node.parent.object === eventNode; | ||
@@ -92,4 +88,3 @@ | ||
&& (operator === '==' || operator === '===') | ||
&& right.type === 'Literal' | ||
&& typeof right.value === 'number' | ||
&& isNumberLiteral(right) | ||
) | ||
@@ -110,3 +105,3 @@ ) { | ||
fixer.replaceText(node, 'key'), | ||
fixer.replaceText(right, quoteString(key)), | ||
fixer.replaceText(right, escapeString(key)), | ||
]; | ||
@@ -124,3 +119,11 @@ }; | ||
const create = context => ({ | ||
'Identifier:matches([name="keyCode"], [name="charCode"], [name="which"])'(node) { | ||
Identifier(node) { | ||
if ( | ||
node.name !== 'keyCode' | ||
&& node.name !== 'charCode' | ||
&& node.name !== 'which' | ||
) { | ||
return; | ||
} | ||
// Normal case when usage is direct -> `event.keyCode` | ||
@@ -142,3 +145,3 @@ const {event, references} = getEventNodeAndReferences(context, node); | ||
// Destructured case | ||
const propertyName = node.value && node.value.name; | ||
const propertyName = node.value.name; | ||
if (!keys.has(propertyName)) { | ||
@@ -157,6 +160,3 @@ return; | ||
); | ||
const initObject | ||
= nearestVariableDeclarator | ||
&& nearestVariableDeclarator.init | ||
&& nearestVariableDeclarator.init; | ||
const initObject = nearestVariableDeclarator?.init; | ||
@@ -163,0 +163,0 @@ // Make sure initObject is a reference of eventVariable |
'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'); | ||
@@ -14,23 +15,11 @@ const ERROR_BITWISE = 'error-bitwise'; | ||
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; | ||
@@ -43,62 +32,66 @@ 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 (operator === '|') { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_BITWISE, | ||
data: { | ||
operator, | ||
value: right.raw, | ||
}, | ||
fix, | ||
}, | ||
]; | ||
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 { | ||
problem.fix = fix; | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
yield fixer.replaceText(node, fixed); | ||
} | ||
}; | ||
if (operator === '|') { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_BITWISE, | ||
fix, | ||
}, | ||
]; | ||
} else { | ||
problem.fix = fix; | ||
} | ||
} | ||
return problem; | ||
}, | ||
[bitwiseNotUnaryExpressionSelector]: node => ({ | ||
node, | ||
messageId: ERROR_BITWISE_NOT, | ||
* fix(fixer) { | ||
yield fixer.replaceText(node, mathTruncFunctionCall(node.argument.argument)); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}, | ||
}), | ||
}; | ||
return problem; | ||
}); | ||
// 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) { | ||
yield fixer.replaceText(node, mathTruncFunctionCall(node.argument.argument)); | ||
yield * fixSpaceAroundKeyword(fixer, node, sourceCode); | ||
}, | ||
}; | ||
} | ||
}); | ||
}; | ||
@@ -105,0 +98,0 @@ |
'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'); | ||
@@ -12,16 +12,2 @@ const messages = { | ||
const replaceChildOrInsertBeforeSelector = [ | ||
methodCallSelector({ | ||
methods: ['replaceChild', 'insertBefore'], | ||
argumentsLength: 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 disallowedMethods = new Map([ | ||
@@ -59,15 +45,2 @@ ['replaceChild', 'replaceWith'], | ||
const insertAdjacentTextOrInsertAdjacentElementSelector = [ | ||
methodCallSelector({ | ||
methods: ['insertAdjacentText', 'insertAdjacentElement'], | ||
argumentsLength: 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([ | ||
@@ -91,4 +64,5 @@ ['beforebegin', 'before'], | ||
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); | ||
@@ -110,3 +84,3 @@ const fix = method === 'insertAdjacentElement' && !isValueNotUsable(node) | ||
preferredMethod, | ||
position: context.getSource(positionNode), | ||
position: sourceCode.getText(positionNode), | ||
content, | ||
@@ -119,11 +93,43 @@ }, | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[replaceChildOrInsertBeforeSelector](node) { | ||
return checkForReplaceChildOrInsertBefore(context, node); | ||
}, | ||
[insertAdjacentTextOrInsertAdjacentElementSelector](node) { | ||
return checkForInsertAdjacentTextOrInsertAdjacentElement(context, node); | ||
}, | ||
}); | ||
const create = context => { | ||
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); | ||
} | ||
}); | ||
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} */ | ||
@@ -130,0 +136,0 @@ module.exports = { |
'use strict'; | ||
const {getParenthesizedText} = require('./utils/parentheses.js'); | ||
const { | ||
getParenthesizedText, | ||
getParenthesizedRange, | ||
isSameReference, | ||
} = require('./utils/index.js'); | ||
const {isLiteral, isMethodCall} = require('./ast/index.js'); | ||
const {replaceNodeOrTokenAndSpacesBefore, removeParentheses} = require('./fix/index.js'); | ||
@@ -60,3 +66,3 @@ const MESSAGE_ID = 'prefer-modern-math-apis'; | ||
}, | ||
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`), | ||
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.sourceCode)})`), | ||
}; | ||
@@ -94,3 +100,3 @@ }; | ||
node, | ||
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`), | ||
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.sourceCode)})`), | ||
}; | ||
@@ -107,2 +113,18 @@ }; | ||
const isPlusExpression = node => node.type === 'BinaryExpression' && node.operator === '+'; | ||
const isPow2Expression = node => | ||
node.type === 'BinaryExpression' | ||
&& ( | ||
// `x * x` | ||
(node.operator === '*' && isSameReference(node.left, node.right)) | ||
// `x ** 2` | ||
|| (node.operator === '**' && isLiteral(node.right, 2)) | ||
); | ||
const flatPlusExpression = node => | ||
isPlusExpression(node) | ||
? [node.left, node.right].flatMap(child => flatPlusExpression(child)) | ||
: [node]; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
@@ -113,2 +135,54 @@ const create = context => { | ||
return { | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
object: 'Math', | ||
method: 'sqrt', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const expressions = flatPlusExpression(callExpression.arguments[0]); | ||
if (expressions.some(expression => !isPow2Expression(expression))) { | ||
return; | ||
} | ||
const replacementMethod = expressions.length === 1 ? 'abs' : 'hypot'; | ||
const plusExpressions = new Set(expressions.length === 1 ? [] : expressions.map(expression => expression.parent)); | ||
return { | ||
node: callExpression.callee.property, | ||
messageId: MESSAGE_ID, | ||
data: { | ||
replacement: `Math.${replacementMethod}(…)`, | ||
description: 'Math.sqrt(…)', | ||
}, | ||
* fix(fixer) { | ||
const {sourceCode} = context; | ||
// `Math.sqrt` -> `Math.{hypot,abs}` | ||
yield fixer.replaceText(callExpression.callee.property, replacementMethod); | ||
// `a ** 2 + b ** 2` -> `a, b` | ||
for (const expression of plusExpressions) { | ||
const plusToken = sourceCode.getTokenAfter(expression.left, token => token.type === 'Punctuator' && token.value === '+'); | ||
yield * replaceNodeOrTokenAndSpacesBefore(plusToken, ',', fixer, sourceCode); | ||
yield * removeParentheses(expression, fixer, sourceCode); | ||
} | ||
// `x ** 2` => `x` | ||
// `x * a` => `x` | ||
for (const expression of expressions) { | ||
yield fixer.removeRange([ | ||
getParenthesizedRange(expression.left, sourceCode)[1], | ||
expression.range[1], | ||
]); | ||
} | ||
}, | ||
}; | ||
}, | ||
BinaryExpression(node) { | ||
@@ -115,0 +189,0 @@ nodes.push(node); |
'use strict'; | ||
const {isOpeningParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken} = require('@eslint-community/eslint-utils'); | ||
const isShadowed = require('./utils/is-shadowed.js'); | ||
const isStaticRequire = require('./utils/is-static-require.js'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
const {referenceIdentifierSelector} = require('./selectors/index.js'); | ||
const {isStaticRequire, isReferenceIdentifier, isFunction} = require('./ast/index.js'); | ||
const { | ||
@@ -32,10 +31,2 @@ removeParentheses, | ||
const identifierSelector = referenceIdentifierSelector([ | ||
'exports', | ||
'require', | ||
'module', | ||
'__filename', | ||
'__dirname', | ||
]); | ||
function fixRequireCall(node, sourceCode) { | ||
@@ -173,3 +164,12 @@ if (!isStaticRequire(node.parent) || node.parent.callee !== node) { | ||
&& 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) { | ||
@@ -220,27 +220,32 @@ return function * (fixer) { | ||
function create(context) { | ||
const filename = context.getFilename().toLowerCase(); | ||
const filename = context.filename.toLowerCase(); | ||
if (filename.endsWith('.cjs')) { | ||
return {}; | ||
return; | ||
} | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
'ExpressionStatement[directive="use strict"]'(node) { | ||
const problem = {node, messageId: ERROR_USE_STRICT_DIRECTIVE}; | ||
const fix = function * (fixer) { | ||
yield fixer.remove(node); | ||
yield removeSpacesAfter(node, sourceCode, fixer); | ||
}; | ||
context.on('ExpressionStatement', node => { | ||
if (node.directive !== 'use strict') { | ||
return; | ||
} | ||
if (filename.endsWith('.mjs')) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [{messageId: SUGGESTION_USE_STRICT_DIRECTIVE, fix}]; | ||
} | ||
const problem = {node, messageId: ERROR_USE_STRICT_DIRECTIVE}; | ||
const fix = function * (fixer) { | ||
yield fixer.remove(node); | ||
yield removeSpacesAfter(node, sourceCode, fixer); | ||
}; | ||
return problem; | ||
}, | ||
'ReturnStatement:not(:function ReturnStatement)'(node) { | ||
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 { | ||
@@ -250,26 +255,47 @@ node: sourceCode.getFirstToken(node), | ||
}; | ||
}, | ||
[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, | ||
}]; | ||
@@ -279,47 +305,36 @@ 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; | ||
} | ||
break; | ||
} | ||
default: | ||
} | ||
default: | ||
} | ||
return problem; | ||
}, | ||
}; | ||
return problem; | ||
}); | ||
} | ||
@@ -326,0 +341,0 @@ |
'use strict'; | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils'); | ||
const {not} = require('./selectors/index.js'); | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils'); | ||
const {functionTypes} = require('./ast/index.js'); | ||
@@ -11,12 +11,10 @@ const MESSAGE_ID = 'prefer-native-coercion-functions'; | ||
const nativeCoercionFunctionNames = new Set(['String', 'Number', 'BigInt', 'Boolean', 'Symbol']); | ||
const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findIndex', 'some']); | ||
const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findLast', 'findIndex', 'findLastIndex', 'some']); | ||
const isNativeCoercionFunctionCall = (node, firstArgumentName) => | ||
node | ||
&& node.type === 'CallExpression' | ||
node?.type === 'CallExpression' | ||
&& !node.optional | ||
&& node.callee.type === 'Identifier' | ||
&& nativeCoercionFunctionNames.has(node.callee.name) | ||
&& node.arguments[0] | ||
&& node.arguments[0].type === 'Identifier' | ||
&& node.arguments[0]?.type === 'Identifier' | ||
&& node.arguments[0].name === firstArgumentName; | ||
@@ -37,4 +35,3 @@ | ||
&& node.body.body[0].type === 'ReturnStatement' | ||
&& node.body.body[0].argument | ||
&& node.body.body[0].argument.type === 'Identifier' | ||
&& node.body.body[0].argument?.type === 'Identifier' | ||
&& node.body.body[0].argument.name === node.params[0].name | ||
@@ -77,15 +74,2 @@ ); | ||
const functionsSelector = [ | ||
':function', | ||
'[async!=true]', | ||
'[generator!=true]', | ||
'[params.length>0]', | ||
'[params.0.type="Identifier"]', | ||
not([ | ||
'MethodDefinition[kind="constructor"] > .value', | ||
'MethodDefinition[kind="set"] > .value', | ||
'Property[kind="set"] > .value', | ||
]), | ||
].join(''); | ||
function getArrayCallbackProblem(node) { | ||
@@ -138,4 +122,23 @@ if (!isArrayIdentityCallback(node)) { | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[functionsSelector](node) { | ||
const create = context => { | ||
context.on(functionTypes, node => { | ||
if ( | ||
node.async | ||
|| node.generator | ||
|| node.params.length === 0 | ||
|| node.params[0].type !== 'Identifier' | ||
|| ( | ||
( | ||
( | ||
node.parent.type === 'MethodDefinition' | ||
&& (node.parent.kind === 'constructor' || node.parent.kind === 'set') | ||
) | ||
|| (node.parent.type === 'Property' && node.parent.kind === 'set') | ||
) | ||
&& node.parent.value === node | ||
) | ||
) { | ||
return; | ||
} | ||
let problem = getArrayCallbackProblem(node) || getCoercionFunctionProblem(node); | ||
@@ -147,3 +150,3 @@ | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {replacementFunction, fix} = problem; | ||
@@ -173,4 +176,4 @@ | ||
return problem; | ||
}, | ||
}); | ||
}); | ||
}; | ||
@@ -177,0 +180,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const { | ||
@@ -8,2 +7,3 @@ getNegativeIndexLengthNode, | ||
const typedArray = require('./shared/typed-array.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
@@ -32,2 +32,9 @@ const MESSAGE_ID = 'prefer-negative-index'; | ||
[ | ||
'subarray', | ||
{ | ||
argumentsIndexes: [0, 1], | ||
supportObjects: new Set(typedArray), | ||
}, | ||
], | ||
[ | ||
'splice', | ||
@@ -42,2 +49,11 @@ { | ||
[ | ||
'toSpliced', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
]), | ||
}, | ||
], | ||
[ | ||
'at', | ||
@@ -48,5 +64,17 @@ { | ||
'Array', | ||
'String', | ||
...typedArray, | ||
]), | ||
}, | ||
], | ||
[ | ||
'with', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
...typedArray, | ||
]), | ||
}, | ||
], | ||
]); | ||
@@ -59,3 +87,2 @@ | ||
type === 'MemberExpression' | ||
&& property | ||
&& property.type === 'Identifier' | ||
@@ -99,3 +126,3 @@ ) { | ||
if ( | ||
// [].{slice,splice} | ||
// `[].{slice,splice,toSpliced,at,with}` | ||
( | ||
@@ -105,6 +132,6 @@ parentCallee.type === 'ArrayExpression' | ||
) | ||
// ''.slice | ||
// `''.slice` | ||
|| ( | ||
method === 'slice' | ||
&& isLiteralValue(parentCallee, '') | ||
&& isLiteral(parentCallee, '') | ||
) | ||
@@ -142,3 +169,7 @@ // {Array,String...}.prototype.slice | ||
const create = context => ({ | ||
'CallExpression[callee.type="MemberExpression"]'(node) { | ||
CallExpression(node) { | ||
if (node.callee.type !== 'MemberExpression') { | ||
return; | ||
} | ||
const parsed = parse(node); | ||
@@ -170,3 +201,3 @@ | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
for (const node of removableNodes) { | ||
@@ -186,3 +217,3 @@ yield removeLengthNode(node, fixer, sourceCode); | ||
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.', | ||
}, | ||
@@ -189,0 +220,0 @@ fixable: 'code', |
'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'); | ||
@@ -11,53 +11,40 @@ const MESSAGE_ID = 'prefer-node-protocol'; | ||
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; | ||
} | ||
if ( | ||
typeof value !== 'string' | ||
|| value.startsWith('node:') | ||
|| !isBuiltinModule(value) | ||
) { | ||
return; | ||
} | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {moduleName: value}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => replaceStringLiteral(fixer, node, 'node:', 0, 0), | ||
}; | ||
}, | ||
}; | ||
}; | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
checkRequire: { | ||
type: 'boolean', | ||
default: false, | ||
}, | ||
}, | ||
return { | ||
node, | ||
messageId: MESSAGE_ID, | ||
data: {moduleName: value}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
fix: fixer => replaceStringLiteral(fixer, node, 'node:', 0, 0), | ||
}; | ||
}, | ||
]; | ||
}); | ||
@@ -73,5 +60,4 @@ /** @type {import('eslint').Rule.RuleModule} */ | ||
fixable: 'code', | ||
schema, | ||
messages, | ||
}, | ||
}; |
'use strict'; | ||
const isShadowed = require('./utils/is-shadowed.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, | ||
@@ -28,100 +26,74 @@ 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; | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
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 = function * (fixer) { | ||
yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); | ||
yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); | ||
}; | ||
} else { | ||
problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); | ||
} | ||
reported.add(node); | ||
return problem; | ||
}, | ||
}; | ||
return tracker.createListeners(context); | ||
}; | ||
@@ -128,0 +100,0 @@ |
'use strict'; | ||
const {isCommaToken, isArrowToken, isClosingParenToken} = require('eslint-utils'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url.js'); | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
const {isCommaToken, isArrowToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
const {isMethodCall, isLiteral} = require('./ast/index.js'); | ||
const {removeParentheses} = require('./fix/index.js'); | ||
const {getParentheses, getParenthesizedText} = require('./utils/parentheses.js'); | ||
const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js'); | ||
const { | ||
getParentheses, | ||
getParenthesizedText, | ||
isNodeMatchesNameOrPath, | ||
isSameIdentifier, | ||
} = require('./utils/index.js'); | ||
const {isCallExpression} = require('./ast/call-or-new-expression.js'); | ||
@@ -16,70 +20,69 @@ const MESSAGE_ID_REDUCE = 'reduce'; | ||
const createEmptyObjectSelector = path => { | ||
const prefix = path ? `${path}.` : ''; | ||
return matches([ | ||
// `{}` | ||
`[${prefix}type="ObjectExpression"][${prefix}properties.length=0]`, | ||
// `Object.create(null)` | ||
[ | ||
methodCallSelector({path, object: 'Object', method: 'create', argumentsLength: 1}), | ||
`[${prefix}arguments.0.type="Literal"]`, | ||
`[${prefix}arguments.0.raw="null"]`, | ||
].join(''), | ||
]); | ||
}; | ||
const isEmptyObject = node => | ||
// `{}` | ||
(node.type === 'ObjectExpression' && node.properties.length === 0) | ||
// `Object.create(null)` | ||
|| ( | ||
isMethodCall(node, { | ||
object: 'Object', | ||
method: 'create', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
// eslint-disable-next-line unicorn/no-null | ||
&& isLiteral(node.arguments[0], null) | ||
); | ||
const createArrowCallbackSelector = path => { | ||
const prefix = path ? `${path}.` : ''; | ||
return [ | ||
`[${prefix}type="ArrowFunctionExpression"]`, | ||
`[${prefix}async!=true]`, | ||
`[${prefix}generator!=true]`, | ||
`[${prefix}params.length>=1]`, | ||
`[${prefix}params.0.type="Identifier"]`, | ||
].join(''); | ||
}; | ||
const isArrowFunctionCallback = node => | ||
node.type === 'ArrowFunctionExpression' | ||
&& !node.async | ||
&& node.params.length > 0 | ||
&& node.params[0].type === 'Identifier'; | ||
const createPropertySelector = path => { | ||
const prefix = path ? `${path}.` : ''; | ||
return [ | ||
`[${prefix}type="Property"]`, | ||
`[${prefix}kind="init"]`, | ||
`[${prefix}method!=true]`, | ||
].join(''); | ||
}; | ||
const isProperty = node => | ||
node.type === 'Property' | ||
&& node.kind === 'init' | ||
&& !node.method; | ||
// - `pairs.reduce(…, {})` | ||
// - `pairs.reduce(…, Object.create(null))` | ||
const arrayReduceWithEmptyObject = [ | ||
methodCallSelector({method: 'reduce', argumentsLength: 2}), | ||
createEmptyObjectSelector('arguments.1'), | ||
].join(''); | ||
const isArrayReduceWithEmptyObject = node => | ||
isMethodCall(node, { | ||
method: 'reduce', | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isEmptyObject(node.arguments[1]); | ||
const fixableArrayReduceCases = [ | ||
{ | ||
selector: [ | ||
arrayReduceWithEmptyObject, | ||
// () => Object.assign(object, {key}) | ||
createArrowCallbackSelector('arguments.0'), | ||
methodCallSelector({path: 'arguments.0.body', object: 'Object', method: 'assign', argumentsLength: 2}), | ||
'[arguments.0.body.arguments.0.type="Identifier"]', | ||
'[arguments.0.body.arguments.1.type="ObjectExpression"]', | ||
'[arguments.0.body.arguments.1.properties.length=1]', | ||
createPropertySelector('arguments.0.body.arguments.1.properties.0'), | ||
].join(''), | ||
test: callback => callback.params[0].name === callback.body.arguments[0].name, | ||
test: callExpression => | ||
isArrayReduceWithEmptyObject(callExpression) | ||
// `() => Object.assign(object, {key})` | ||
&& isArrowFunctionCallback(callExpression.arguments[0]) | ||
&& isMethodCall(callExpression.arguments[0].body, { | ||
object: 'Object', | ||
method: 'assign', | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& callExpression.arguments[0].body.arguments[1].type === 'ObjectExpression' | ||
&& callExpression.arguments[0].body.arguments[1].properties.length === 1 | ||
&& isProperty(callExpression.arguments[0].body.arguments[1].properties[0]) | ||
&& isSameIdentifier(callExpression.arguments[0].params[0], callExpression.arguments[0].body.arguments[0]), | ||
getProperty: callback => callback.body.arguments[1].properties[0], | ||
}, | ||
{ | ||
selector: [ | ||
arrayReduceWithEmptyObject, | ||
// () => ({...object, key}) | ||
createArrowCallbackSelector('arguments.0'), | ||
'[arguments.0.body.type="ObjectExpression"]', | ||
'[arguments.0.body.properties.length=2]', | ||
'[arguments.0.body.properties.0.type="SpreadElement"]', | ||
'[arguments.0.body.properties.0.argument.type="Identifier"]', | ||
createPropertySelector('arguments.0.body.properties.1'), | ||
].join(''), | ||
test: callback => callback.params[0].name === callback.body.properties[0].argument.name, | ||
test: callExpression => | ||
isArrayReduceWithEmptyObject(callExpression) | ||
// `() => ({...object, key})` | ||
&& isArrowFunctionCallback(callExpression.arguments[0]) | ||
&& callExpression.arguments[0].body.type === 'ObjectExpression' | ||
&& callExpression.arguments[0].body.properties.length === 2 | ||
&& callExpression.arguments[0].body.properties[0].type === 'SpreadElement' | ||
&& isProperty(callExpression.arguments[0].body.properties[1]) | ||
&& isSameIdentifier(callExpression.arguments[0].params[0], callExpression.arguments[0].body.properties[0].argument), | ||
getProperty: callback => callback.body.properties[1], | ||
@@ -94,16 +97,9 @@ }, | ||
]; | ||
const anyCall = [ | ||
'CallExpression', | ||
'[optional!=true]', | ||
'[arguments.length=1]', | ||
'[arguments.0.type!="SpreadElement"]', | ||
' > .callee', | ||
].join(''); | ||
function fixReduceAssignOrSpread({sourceCode, node, property}) { | ||
function fixReduceAssignOrSpread({sourceCode, callExpression, property}) { | ||
const removeInitObject = fixer => { | ||
const initObject = node.arguments[1]; | ||
const initObject = callExpression.arguments[1]; | ||
const parentheses = getParentheses(initObject, sourceCode); | ||
const firstToken = parentheses[0] || initObject; | ||
const lastToken = parentheses[parentheses.length - 1] || initObject; | ||
const lastToken = parentheses.at(-1) || initObject; | ||
const startToken = sourceCode.getTokenBefore(firstToken); | ||
@@ -116,3 +112,3 @@ const [start] = startToken.range; | ||
function * removeFirstParameter(fixer) { | ||
const parameters = node.arguments[0].params; | ||
const parameters = callExpression.arguments[0].params; | ||
const [firstParameter] = parameters; | ||
@@ -151,3 +147,3 @@ const tokenAfter = sourceCode.getTokenAfter(firstParameter); | ||
function * replaceFunctionBody(fixer) { | ||
const functionBody = node.arguments[0].body; | ||
const functionBody = callExpression.arguments[0].body; | ||
const {keyText, valueText} = getKeyValueText(); | ||
@@ -160,7 +156,7 @@ yield fixer.replaceText(functionBody, `[${keyText}, ${valueText}]`); | ||
// Wrap `array.reduce()` with `Object.fromEntries()` | ||
yield fixer.insertTextBefore(node, 'Object.fromEntries('); | ||
yield fixer.insertTextAfter(node, ')'); | ||
yield fixer.insertTextBefore(callExpression, 'Object.fromEntries('); | ||
yield fixer.insertTextAfter(callExpression, ')'); | ||
// Switch `.reduce` to `.map` | ||
yield fixer.replaceText(node.callee.property, 'map'); | ||
yield fixer.replaceText(callExpression.callee.property, 'map'); | ||
@@ -180,2 +176,3 @@ // Remove empty object | ||
function create(context) { | ||
const {sourceCode} = context; | ||
const {functions: configFunctions} = { | ||
@@ -186,57 +183,50 @@ functions: [], | ||
const functions = [...configFunctions, ...lodashFromPairsFunctions]; | ||
const sourceCode = context.getSourceCode(); | ||
const listeners = {}; | ||
const arrayReduce = new Map(); | ||
for (const {selector, test, getProperty} of fixableArrayReduceCases) { | ||
listeners[selector] = node => { | ||
const [callbackFunction] = node.arguments; | ||
if (!test(callbackFunction)) { | ||
return; | ||
return { | ||
* CallExpression(callExpression) { | ||
for (const {test, getProperty} of fixableArrayReduceCases) { | ||
if (!test(callExpression)) { | ||
continue; | ||
} | ||
const [callbackFunction] = callExpression.arguments; | ||
const [firstParameter] = callbackFunction.params; | ||
const variables = sourceCode.getDeclaredVariables(callbackFunction); | ||
const firstParameterVariable = variables.find(variable => variable.identifiers.length === 1 && variable.identifiers[0] === firstParameter); | ||
if (!firstParameterVariable || firstParameterVariable.references.length !== 1) { | ||
continue; | ||
} | ||
yield { | ||
node: callExpression.callee.property, | ||
messageId: MESSAGE_ID_REDUCE, | ||
fix: fixReduceAssignOrSpread({ | ||
sourceCode, | ||
callExpression, | ||
property: getProperty(callbackFunction), | ||
}), | ||
}; | ||
} | ||
const [firstParameter] = callbackFunction.params; | ||
const variables = context.getDeclaredVariables(callbackFunction); | ||
const firstParameterVariable = variables.find(variable => variable.identifiers.length === 1 && variable.identifiers[0] === firstParameter); | ||
if (!firstParameterVariable || firstParameterVariable.references.length !== 1) { | ||
if (!isCallExpression(callExpression, { | ||
argumentsLength: 1, | ||
optional: false, | ||
})) { | ||
return; | ||
} | ||
arrayReduce.set( | ||
node, | ||
// The fix function | ||
fixReduceAssignOrSpread({ | ||
sourceCode, | ||
node, | ||
property: getProperty(callbackFunction), | ||
}), | ||
); | ||
}; | ||
} | ||
listeners['Program:exit'] = () => { | ||
for (const [node, fix] of arrayReduce.entries()) { | ||
context.report({ | ||
node: node.callee.property, | ||
messageId: MESSAGE_ID_REDUCE, | ||
fix, | ||
}); | ||
} | ||
const functionNode = callExpression.callee; | ||
for (const nameOrPath of functions) { | ||
const functionName = nameOrPath.trim(); | ||
if (isNodeMatchesNameOrPath(functionNode, functionName)) { | ||
yield { | ||
node: functionNode, | ||
messageId: MESSAGE_ID_FUNCTION, | ||
data: {functionName}, | ||
fix: fixer => fixer.replaceText(functionNode, 'Object.fromEntries'), | ||
}; | ||
} | ||
} | ||
}, | ||
}; | ||
listeners[anyCall] = node => { | ||
if (!isNodeMatches(node, functions)) { | ||
return; | ||
} | ||
const functionName = functions.find(nameOrPath => isNodeMatchesNameOrPath(node, nameOrPath)).trim(); | ||
context.report({ | ||
node, | ||
messageId: MESSAGE_ID_FUNCTION, | ||
data: {functionName}, | ||
fix: fixer => fixer.replaceText(node, 'Object.fromEntries'), | ||
}); | ||
}; | ||
return listeners; | ||
} | ||
@@ -264,3 +254,2 @@ | ||
description: 'Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object.', | ||
url: getDocumentationUrl(__filename), | ||
}, | ||
@@ -267,0 +256,0 @@ fixable: 'code', |
'use strict'; | ||
const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
const assertToken = require('./utils/assert-token.js'); | ||
@@ -12,13 +12,13 @@ | ||
const selector = [ | ||
'CatchClause', | ||
' > ', | ||
'.param', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
const variables = context.getDeclaredVariables(node.parent); | ||
CatchClause(catchClause) { | ||
const node = catchClause.param; | ||
if (!node) { | ||
return; | ||
} | ||
const {sourceCode} = context; | ||
const variables = sourceCode.getDeclaredVariables(node.parent); | ||
if (variables.some(variable => variable.references.length > 0)) { | ||
@@ -35,3 +35,2 @@ return; | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
@@ -38,0 +37,0 @@ assertToken(tokenBefore, { |
'use strict'; | ||
const {getPropertyName} = require('eslint-utils'); | ||
const { | ||
methodCallSelector, | ||
emptyObjectSelector, | ||
emptyArraySelector, | ||
matches, | ||
} = require('./selectors/index.js'); | ||
const {getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isMemberExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -16,47 +11,61 @@ const messages = { | ||
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', method: 'apply', minimumArguments: 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}, | ||
* fix(fixer) { | ||
yield fixer.replaceText(node.object, `${constructorName}.prototype`); | ||
yield fixer.replaceText(objectNode, `${constructorName}.prototype`); | ||
if ( | ||
node.object | ||
&& ( | ||
node.object.type === 'ArrayExpression' | ||
|| node.object.type === 'ObjectExpression' | ||
) | ||
objectNode.type === 'ArrayExpression' | ||
|| objectNode.type === 'ObjectExpression' | ||
) { | ||
yield * fixSpaceAroundKeyword(fixer, node.parent.parent, context.getSourceCode()); | ||
yield * fixSpaceAroundKeyword(fixer, callExpression, sourceCode); | ||
} | ||
@@ -63,0 +72,0 @@ }, |
'use strict'; | ||
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js'); | ||
const {isNodeValueNotDomNode} = require('./utils/index.js'); | ||
const {isMethodCall, isStringLiteral, isNullLiteral} = require('./ast/index.js'); | ||
@@ -9,10 +10,2 @@ const MESSAGE_ID = 'prefer-query-selector'; | ||
const selector = [ | ||
methodCallSelector({ | ||
methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'], | ||
argumentsLength: 1, | ||
}), | ||
notDomNodeSelector('callee.object'), | ||
].join(''); | ||
const disallowedIdentifierNames = new Map([ | ||
@@ -29,3 +22,3 @@ ['getElementById', 'querySelector'], | ||
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}`; | ||
@@ -68,17 +61,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 => { | ||
@@ -107,3 +94,15 @@ if (node.type === 'Literal') { | ||
const create = () => ({ | ||
[selector](node) { | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'], | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| isNodeValueNotDomNode(node.callee.object) | ||
) { | ||
return; | ||
} | ||
const method = node.callee.property.name; | ||
@@ -110,0 +109,0 @@ const preferredSelector = disallowedIdentifierNames.get(method); |
'use strict'; | ||
const {getPropertyName} = require('eslint-utils'); | ||
const isLiteralValue = require('./utils/is-literal-value.js'); | ||
const {not, methodCallSelector} = require('./selectors/index.js'); | ||
const {getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {isNullLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -11,11 +10,5 @@ const MESSAGE_ID = 'prefer-reflect-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) | ||
isNullLiteral(argument1) | ||
|| argument1.type === 'ThisExpression' | ||
@@ -53,4 +46,3 @@ ) | ||
&& getPropertyName(node.callee.object.object) === 'prototype' | ||
&& node.callee.object.object.object | ||
&& node.callee.object.object.object.type === 'Identifier' | ||
&& node.callee.object.object.object?.type === 'Identifier' | ||
&& node.callee.object.object.object.name === 'Function' | ||
@@ -71,4 +63,16 @@ && node.arguments.length === 3 | ||
const create = context => ({ | ||
[selector](node) { | ||
const sourceCode = context.getSourceCode(); | ||
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); | ||
@@ -75,0 +79,0 @@ if (fix) { |
'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(…)`.', | ||
[SUGGESTION]: 'Switch to `RegExp#test(…)`.', | ||
}; | ||
@@ -18,5 +22,7 @@ | ||
type: REGEXP_EXEC, | ||
selector: methodCallSelector({ | ||
test: node => isMethodCall(node, { | ||
method: 'exec', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -32,5 +38,7 @@ getNodes: node => ({ | ||
type: STRING_MATCH, | ||
selector: methodCallSelector({ | ||
test: node => isMethodCall(node, { | ||
method: 'match', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -71,28 +79,35 @@ getNodes: node => ({ | ||
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 | ||
); | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => Object.fromEntries( | ||
cases.map(checkCase => [ | ||
checkCase.selector, | ||
node => { | ||
if (!isBooleanNode(node)) { | ||
return; | ||
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); | ||
@@ -102,3 +117,3 @@ const {methodNode, regexpNode} = nodes; | ||
if (regexpNode.type === 'Literal' && !regexpNode.regex) { | ||
return; | ||
continue; | ||
} | ||
@@ -111,20 +126,23 @@ | ||
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; | ||
} | ||
}, | ||
}); | ||
@@ -140,4 +158,5 @@ /** @type {import('eslint').Rule.RuleModule} */ | ||
fixable: 'code', | ||
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'); | ||
@@ -18,72 +13,25 @@ const MESSAGE_ID_ERROR = 'error'; | ||
// `[]` | ||
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', | ||
methods: ['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({ | ||
methods: [ | ||
'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 => { | ||
/* c8 ignore next 3 */ | ||
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' | ||
@@ -130,5 +78,44 @@ && !callee.computed | ||
const create = context => ({ | ||
[selector](node) { | ||
const variable = findVariable(context.getScope(), node); | ||
Identifier(node) { | ||
const {parent} = node; | ||
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 variable = findVariable(context.sourceCode.getScope(node), node); | ||
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342 | ||
@@ -178,5 +165,2 @@ // But can't reproduce, just ignore this case | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: { | ||
name: node.name, | ||
}, | ||
fix, | ||
@@ -183,0 +167,0 @@ }, |
'use strict'; | ||
const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('eslint-utils'); | ||
const {methodCallSelector, not} = 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 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 { | ||
replaceNodeOrTokenAndSpacesBefore, | ||
removeSpacesAfter, | ||
removeMethodCall, | ||
} = require('./fix/index.js'); | ||
getParenthesizedRange, | ||
getParenthesizedText, | ||
needsSemicolon, | ||
shouldAddParenthesesToSpreadElementArgument, | ||
isNodeMatches, | ||
isMethodNamed, | ||
} = require('./utils/index.js'); | ||
const {removeMethodCall} = require('./fix/index.js'); | ||
const {isLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -18,2 +17,3 @@ 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'; | ||
@@ -29,2 +29,3 @@ const SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE = 'argument-is-spreadable'; | ||
[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(\'\')`.', | ||
@@ -38,32 +39,2 @@ [SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE]: 'First argument is an `array`.', | ||
const arrayFromCallSelector = [ | ||
methodCallSelector({ | ||
object: 'Array', | ||
method: 'from', | ||
minimumArguments: 1, | ||
maximumArguments: 3, | ||
}), | ||
// Allow `Array.from({length})` | ||
'[arguments.0.type!="ObjectExpression"]', | ||
].join(''); | ||
const arrayConcatCallSelector = [ | ||
methodCallSelector('concat'), | ||
not( | ||
[ | ||
'Literal', | ||
'TemplateLiteral', | ||
].map(type => `[callee.object.type="${type}"]`), | ||
), | ||
].join(''); | ||
const arraySliceCallSelector = [ | ||
methodCallSelector({ | ||
method: 'slice', | ||
minimumArguments: 0, | ||
maximumArguments: 1, | ||
}), | ||
'[callee.object.type!="ArrayExpression"]', | ||
].join(''); | ||
const ignoredSliceCallee = [ | ||
@@ -77,7 +48,2 @@ 'arrayBuffer', | ||
const stringSplitCallSelector = methodCallSelector({ | ||
method: 'split', | ||
argumentsLength: 1, | ||
}); | ||
const isArrayLiteral = node => node.type === 'ArrayExpression'; | ||
@@ -115,3 +81,3 @@ const isArrayLiteralHasTrailingComma = (node, sourceCode) => { | ||
.filter(({node, isArrayLiteral}) => (!isArrayLiteral || node.elements.length > 0)); | ||
const lastArgument = nonEmptyArguments[nonEmptyArguments.length - 1]; | ||
const lastArgument = nonEmptyArguments.at(-1); | ||
@@ -271,9 +237,2 @@ let text = nonEmptyArguments | ||
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) { | ||
@@ -287,11 +246,3 @@ // 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); | ||
}; | ||
@@ -330,8 +281,39 @@ } | ||
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 { | ||
@@ -342,145 +324,201 @@ node, | ||
}; | ||
}, | ||
[arrayConcatCallSelector](node) { | ||
const {object} = node.callee; | ||
} | ||
}); | ||
if (isClassName(object)) { | ||
return; | ||
} | ||
// `array.concat()` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'concat', | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const scope = context.getScope(); | ||
const staticResult = getStaticValue(object, scope); | ||
const {object} = node.callee; | ||
const scope = sourceCode.getScope(object); | ||
if (staticResult && !Array.isArray(staticResult.value)) { | ||
return; | ||
} | ||
if (isNotArray(object, scope)) { | ||
return; | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_CONCAT, | ||
}; | ||
const staticResult = getStaticValue(object, scope); | ||
if (staticResult && !Array.isArray(staticResult.value)) { | ||
return; | ||
} | ||
const fixableArguments = getConcatFixableArguments(node.arguments, scope); | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_CONCAT, | ||
}; | ||
if (fixableArguments.length > 0 || node.arguments.length === 0) { | ||
problem.fix = fixConcat(node, sourceCode, fixableArguments); | ||
return problem; | ||
} | ||
const fixableArguments = getConcatFixableArguments(node.arguments, scope); | ||
const [firstArgument, ...restArguments] = node.arguments; | ||
if (firstArgument.type === 'SpreadElement') { | ||
return problem; | ||
} | ||
if (fixableArguments.length > 0 || node.arguments.length === 0) { | ||
problem.fix = fixConcat(node, sourceCode, fixableArguments); | ||
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, | ||
}, | ||
]; | ||
const [firstArgument, ...restArguments] = node.arguments; | ||
if (firstArgument.type === 'SpreadElement') { | ||
return problem; | ||
} | ||
if (!hasSideEffect(firstArgument, sourceCode)) { | ||
suggestions.push({ | ||
messageId: SUGGESTION_CONCAT_TEST_ARGUMENT, | ||
testArgument: true, | ||
}); | ||
} | ||
const fixableArgumentsAfterFirstArgument = getConcatFixableArguments(restArguments, scope); | ||
const suggestions = [ | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE, | ||
isSpreadable: true, | ||
}, | ||
{ | ||
messageId: SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE, | ||
isSpreadable: false, | ||
}, | ||
]; | ||
problem.suggest = suggestions.map(({messageId, isSpreadable, testArgument}) => ({ | ||
messageId, | ||
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)) { | ||
return; | ||
} | ||
if (isNodeMatches(node.callee.object, ignoredSliceCallee)) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_SLICE, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}; | ||
}, | ||
[stringSplitCallSelector](node) { | ||
const [separator] = node.arguments; | ||
if (!isLiteralValue(separator, '')) { | ||
return; | ||
} | ||
const [firstArgument] = node.arguments; | ||
if (firstArgument && !isLiteral(firstArgument, 0)) { | ||
return; | ||
} | ||
const string = node.callee.object; | ||
const staticValue = getStaticValue(string, context.getScope()); | ||
let hasSameResult = false; | ||
if (staticValue) { | ||
const {value} = staticValue; | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_SLICE, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}; | ||
}); | ||
if (typeof value !== 'string') { | ||
return; | ||
} | ||
// `array.toSpliced()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'toSpliced', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.callee.object.type !== 'ArrayExpression' | ||
)) { | ||
return; | ||
} | ||
// eslint-disable-next-line unicorn/prefer-spread | ||
const resultBySplit = value.split(''); | ||
const resultBySpread = [...value]; | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_ARRAY_TO_SPLICED, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}; | ||
}); | ||
hasSameResult = resultBySplit.length === resultBySpread.length | ||
&& resultBySplit.every((character, index) => character === resultBySpread[index]); | ||
} | ||
// `string.split()` | ||
context.on('CallExpression', node => { | ||
if (!isMethodCall(node, { | ||
method: 'split', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const problem = { | ||
node: node.callee.property, | ||
messageId: ERROR_STRING_SPLIT, | ||
}; | ||
const [separator] = node.arguments; | ||
if (!isLiteral(separator, '')) { | ||
return; | ||
} | ||
if (hasSameResult) { | ||
problem.fix = methodCallToSpread(node, sourceCode); | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_USE_SPREAD, | ||
fix: methodCallToSpread(node, sourceCode), | ||
}, | ||
]; | ||
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 problem; | ||
}, | ||
}; | ||
// 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; | ||
}); | ||
}; | ||
@@ -494,3 +532,3 @@ | ||
docs: { | ||
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` and `String#split(\'\')`.', | ||
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split(\'\')`.', | ||
}, | ||
@@ -497,0 +535,0 @@ fixable: 'code', |
'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({ | ||
method: 'replace', | ||
argumentsLength: 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; | ||
} | ||
function removeEscapeCharacters(regexString) { | ||
let fixedString = regexString; | ||
let index = 0; | ||
do { | ||
index = fixedString.indexOf('\\', index); | ||
const parts = tree.type === 'alternative' ? tree.body : [tree]; | ||
if (parts.some(part => part.type !== 'value')) { | ||
return; | ||
} | ||
if (index >= 0) { | ||
fixedString = fixedString.slice(0, index) + fixedString.slice(index + 1); | ||
index++; | ||
} | ||
} while (index >= 0); | ||
// TODO: Preserve escape | ||
const string = String.fromCodePoint(...parts.map(part => part.codePoint)); | ||
return fixedString; | ||
return escapeString(string); | ||
} | ||
const isRegExpWithGlobalFlag = (node, scope) => { | ||
if (isRegexLiteral(node)) { | ||
return node.regex.flags.includes('g'); | ||
} | ||
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 = () => ({ | ||
[selector](node) { | ||
const {arguments: arguments_, callee} = node; | ||
const [search] = arguments_; | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if (!isMethodCall(node, { | ||
methods: ['replace', 'replaceAll'], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
if (!isRegexWithGlobalFlag(search) || !isLiteralCharactersOnly(search)) { | ||
const { | ||
arguments: [pattern], | ||
callee: {property}, | ||
} = node; | ||
if (!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))) { | ||
return; | ||
} | ||
const methodName = property.name; | ||
const patternReplacement = getPatternReplacement(pattern); | ||
if (methodName === 'replaceAll') { | ||
if (!patternReplacement) { | ||
return; | ||
} | ||
return { | ||
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, | ||
messageId: MESSAGE_ID, | ||
fix: fixer => [ | ||
fixer.insertTextAfter(callee, 'All'), | ||
fixer.replaceText(search, quoteString(removeEscapeCharacters(search.regex.pattern))), | ||
], | ||
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); | ||
}, | ||
}; | ||
@@ -63,0 +131,0 @@ }, |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const isNumber = require('./utils/is-number.js'); | ||
const {replaceArgument} = require('./fix/index.js'); | ||
const {isNumberLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -15,12 +15,4 @@ const MESSAGE_ID_SUBSTR = 'substr'; | ||
const selector = methodCallSelector({ | ||
methods: ['substr', 'substring'], | ||
includeOptionalMember: true, | ||
includeOptionalCall: true, | ||
}); | ||
const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number'; | ||
const getNumericValue = node => { | ||
if (isLiteralNumber(node)) { | ||
if (isNumberLiteral(node)) { | ||
return node.value; | ||
@@ -36,4 +28,3 @@ } | ||
const isLengthProperty = node => ( | ||
node | ||
&& node.type === 'MemberExpression' | ||
node?.type === 'MemberExpression' | ||
&& node.computed === false | ||
@@ -52,4 +43,4 @@ && node.property.type === 'Identifier' | ||
const scope = context.getScope(); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const scope = sourceCode.getScope(node); | ||
const firstArgumentStaticResult = getStaticValue(firstArgument, scope); | ||
@@ -59,4 +50,4 @@ const secondArgumentRange = getParenthesizedRange(secondArgument, sourceCode); | ||
if (firstArgumentStaticResult && firstArgumentStaticResult.value === 0) { | ||
if (isLiteralNumber(secondArgument) || isLengthProperty(secondArgument)) { | ||
if (firstArgumentStaticResult?.value === 0) { | ||
if (isNumberLiteral(secondArgument) || isLengthProperty(secondArgument)) { | ||
return; | ||
@@ -75,3 +66,3 @@ } | ||
if (argumentNodes.every(node => isLiteralNumber(node))) { | ||
if (argumentNodes.every(node => isNumberLiteral(node))) { | ||
yield replaceSecondArgument(firstArgument.value + secondArgument.value); | ||
@@ -81,3 +72,3 @@ return; | ||
if (argumentNodes.every(node => isNumber(node, context.getScope()))) { | ||
if (argumentNodes.every(node => isNumber(node, scope))) { | ||
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); | ||
@@ -93,3 +84,3 @@ | ||
function * fixSubstringArguments({node, fixer, context, abort}) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const [firstArgument, secondArgument] = node.arguments; | ||
@@ -156,3 +147,7 @@ | ||
const create = context => ({ | ||
[selector](node) { | ||
CallExpression(node) { | ||
if (!isMethodCall(node, {methods: ['substr', 'substring']})) { | ||
return; | ||
} | ||
const method = node.callee.property.name; | ||
@@ -159,0 +154,0 @@ |
'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'); | ||
@@ -29,7 +29,2 @@ const MESSAGE_STARTS_WITH = 'prefer-starts-with'; | ||
const regexTestSelector = [ | ||
methodCallSelector({method: 'test', argumentsLength: 1}), | ||
'[callee.object.regex]', | ||
].join(''); | ||
const checkRegex = ({pattern, flags}) => { | ||
@@ -65,6 +60,18 @@ if (flags.includes('i') || flags.includes('m')) { | ||
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; | ||
@@ -88,3 +95,3 @@ const {regex} = regexNode; | ||
if (!isString) { | ||
const staticValue = getStaticValue(target, context.getScope()); | ||
const staticValue = getStaticValue(target, sourceCode.getScope(target)); | ||
@@ -109,3 +116,3 @@ if (staticValue) { | ||
// Goal: `(target ?? '').startsWith(pattern)` | ||
case FIX_TYPE_NULLISH_COALESCING: | ||
case FIX_TYPE_NULLISH_COALESCING: { | ||
if ( | ||
@@ -127,5 +134,6 @@ !isTargetParenthesized | ||
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 | ||
@@ -135,9 +143,12 @@ 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 ( | ||
@@ -150,2 +161,3 @@ !isRegexParenthesized | ||
} | ||
} | ||
} | ||
@@ -162,3 +174,3 @@ | ||
// Replace argument with result.string | ||
yield fixer.replaceTextRange(getParenthesizedRange(target, sourceCode), quoteString(result.string)); | ||
yield fixer.replaceTextRange(getParenthesizedRange(target, sourceCode), escapeString(result.string)); | ||
} | ||
@@ -165,0 +177,0 @@ |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -9,15 +9,14 @@ const MESSAGE_ID = 'prefer-string-trim-start-end'; | ||
const selector = [ | ||
methodCallSelector({ | ||
methods: ['trimLeft', 'trimRight'], | ||
argumentsLength: 0, | ||
includeOptionalMember: true, | ||
}), | ||
' > .callee', | ||
' > .property', | ||
].join(' '); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
[selector](node) { | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
methods: ['trimLeft', 'trimRight'], | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
})) { | ||
return; | ||
} | ||
const node = callExpression.callee.property; | ||
const method = node.name; | ||
@@ -24,0 +23,0 @@ const replacement = method === 'trimLeft' ? 'trimStart' : 'trimEnd'; |
'use strict'; | ||
const {hasSideEffect} = require('eslint-utils'); | ||
const {hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const isSameReference = require('./utils/is-same-reference.js'); | ||
@@ -165,9 +165,11 @@ const getIndentString = require('./utils/get-indent-string.js'); | ||
case 'ReturnStatement': | ||
case 'ThrowStatement': | ||
case 'ThrowStatement': { | ||
return false; | ||
} | ||
case 'IfStatement': | ||
case 'IfStatement': { | ||
return !node.alternate | ||
|| shouldInsertBreakStatement(node.consequent) | ||
|| shouldInsertBreakStatement(node.alternate); | ||
} | ||
@@ -179,4 +181,5 @@ case 'BlockStatement': { | ||
default: | ||
default: { | ||
return true; | ||
} | ||
} | ||
@@ -193,3 +196,3 @@ } | ||
const lastStatement = ifStatements[ifStatements.length - 1].statement; | ||
const lastStatement = ifStatements.at(-1).statement; | ||
if (lastStatement.alternate) { | ||
@@ -211,5 +214,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': { | ||
@@ -258,3 +263,3 @@ yield fixer.insertTextAfter(firstStatement, `\n${indent}default:\n${indent}// Do nothing`); | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const ifStatements = new Set(); | ||
@@ -265,7 +270,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); | ||
} | ||
}, | ||
@@ -272,0 +279,0 @@ * 'Program:exit'() { |
'use strict'; | ||
const {isParenthesized} = require('eslint-utils'); | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
@@ -14,12 +14,4 @@ const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
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) { | ||
@@ -50,3 +42,3 @@ /* c8 ignore next 3 */ | ||
const onlySingleLine = context.options[0] === 'only-single-line'; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const scopeToNamesGeneratedByFixer = new WeakMap(); | ||
@@ -179,3 +171,12 @@ const isSafeName = (name, scopes) => scopes.every(scope => { | ||
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); | ||
@@ -207,3 +208,3 @@ const alternate = getNodeBody(node.alternate); | ||
const scope = context.getScope(); | ||
const scope = sourceCode.getScope(node); | ||
problem.fix = function * (fixer) { | ||
@@ -210,0 +211,0 @@ const testText = getText(node.test); |
'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'); | ||
@@ -16,27 +16,21 @@ const ERROR_PROMISE = 'promise'; | ||
const promiseMethods = ['then', 'catch', 'finally']; | ||
const promisePrototypeMethods = ['then', 'catch', 'finally']; | ||
const isTopLevelCallExpression = node => { | ||
if (node.type !== 'CallExpression') { | ||
return false; | ||
} | ||
const topLevelCallExpression = 'CallExpression:not(:function *)'; | ||
const iife = [ | ||
topLevelCallExpression, | ||
matches([ | ||
'[callee.type="FunctionExpression"]', | ||
'[callee.type="ArrowFunctionExpression"]', | ||
]), | ||
'[callee.async!=false]', | ||
'[callee.generator!=true]', | ||
].join(''); | ||
const promise = [ | ||
topLevelCallExpression, | ||
memberExpressionSelector({ | ||
path: 'callee', | ||
properties: promiseMethods, | ||
includeOptional: true, | ||
}), | ||
].join(''); | ||
const identifier = [ | ||
topLevelCallExpression, | ||
'[callee.type="Identifier"]', | ||
].join(''); | ||
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 => | ||
@@ -47,6 +41,6 @@ node.parent.type === 'MemberExpression' | ||
&& node.parent.property.type === 'Identifier' | ||
&& promiseMethods.includes(node.parent.property.name) | ||
&& promisePrototypeMethods.includes(node.parent.property.name) | ||
&& node.parent.parent.type === 'CallExpression' | ||
&& node.parent.parent.callee === node.parent; | ||
const isAwaitArgument = node => { | ||
const isAwaitExpressionArgument = node => { | ||
if (node.parent.type === 'ChainExpression') { | ||
@@ -59,32 +53,62 @@ node = node.parent; | ||
// `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, | ||
}) | ||
&& 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) { | ||
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) { | ||
CallExpression(node) { | ||
if ( | ||
!isTopLevelCallExpression(node) | ||
|| isPromiseMethodCalleeObject(node) | ||
|| isAwaitExpressionArgument(node) | ||
|| isInPromiseMethods(node) | ||
) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_PROMISE, | ||
}; | ||
}, | ||
[iife](node) { | ||
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) { | ||
return; | ||
// Promises | ||
if (isMemberExpression(node.callee, { | ||
properties: promisePrototypeMethods, | ||
computed: false, | ||
})) { | ||
return { | ||
node: node.callee.property, | ||
messageId: ERROR_PROMISE, | ||
}; | ||
} | ||
return { | ||
node, | ||
loc: getFunctionHeadLocation(node.callee, context.getSourceCode()), | ||
messageId: ERROR_IIFE, | ||
}; | ||
}, | ||
[identifier](node) { | ||
if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) { | ||
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(context.getScope(), node.callee); | ||
const variable = findVariable(sourceCode.getScope(node), node.callee); | ||
if (!variable || variable.defs.length !== 1) { | ||
@@ -99,9 +123,4 @@ return; | ||
if ( | ||
!( | ||
( | ||
value.type === 'ArrowFunctionExpression' | ||
|| value.type === 'FunctionExpression' | ||
|| value.type === 'FunctionDeclaration' | ||
) && !value.generator && value.async | ||
) | ||
!value | ||
|| !(isFunction(value) && !value.generator && value.async) | ||
) { | ||
@@ -108,0 +127,0 @@ return; |
'use strict'; | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
@@ -9,3 +9,3 @@ const MESSAGE_ID = 'prefer-type-error'; | ||
const tcIdentifiers = new Set([ | ||
const typeCheckIdentifiers = new Set([ | ||
'isArguments', | ||
@@ -50,3 +50,3 @@ 'isArray', | ||
const tcGlobalIdentifiers = new Set([ | ||
const typeCheckGlobalIdentifiers = new Set([ | ||
'isNaN', | ||
@@ -56,7 +56,2 @@ 'isFinite', | ||
const selector = [ | ||
'ThrowStatement', | ||
newExpressionSelector({name: 'Error', path: 'argument'}), | ||
].join(''); | ||
const isTypecheckingIdentifier = (node, callExpression, isMemberExpression) => | ||
@@ -67,4 +62,4 @@ callExpression !== undefined | ||
&& ( | ||
(isMemberExpression === true && tcIdentifiers.has(node.name)) | ||
|| (isMemberExpression === false && tcGlobalIdentifiers.has(node.name)) | ||
(isMemberExpression === true && typeCheckIdentifiers.has(node.name)) | ||
|| (isMemberExpression === false && typeCheckGlobalIdentifiers.has(node.name)) | ||
); | ||
@@ -88,9 +83,15 @@ | ||
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 ( | ||
@@ -100,3 +101,5 @@ node.operator === 'typeof' | ||
); | ||
case 'BinaryExpression': | ||
} | ||
case 'BinaryExpression': { | ||
return ( | ||
@@ -107,3 +110,5 @@ node.operator === 'instanceof' | ||
); | ||
case 'LogicalExpression': | ||
} | ||
case 'LogicalExpression': { | ||
return ( | ||
@@ -113,4 +118,7 @@ isTypecheckingExpression(node.left, callExpression) | ||
); | ||
default: | ||
} | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -123,5 +131,6 @@ }; | ||
const create = () => ({ | ||
[selector](node) { | ||
ThrowStatement(node) { | ||
if ( | ||
isLone(node) | ||
isNewExpression(node.argument, {name: 'Error'}) | ||
&& isLone(node) | ||
&& node.parent.parent | ||
@@ -128,0 +137,0 @@ && isTypechecking(node.parent.parent) |
'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,6 @@ const cartesianProductSamples = require('./utils/cartesian-product-samples.js'); | ||
const getVariableIdentifiers = require('./utils/get-variable-identifiers.js'); | ||
const isStaticRequire = require('./utils/is-static-require.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'); | ||
@@ -145,2 +144,12 @@ const MESSAGE_ID_REPLACE = 'replace'; | ||
// `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 { | ||
@@ -220,3 +229,9 @@ total, | ||
const shouldFix = variable => !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', | ||
); | ||
@@ -329,3 +344,3 @@ const isDefaultOrNamespaceImportName = identifier => { | ||
const options = prepareOptions(context.options[0]); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
@@ -439,3 +454,8 @@ // A `class` declaration produces two variables in two scopes: | ||
if (variableReplacements.total === 1 && shouldFix(variable) && variableReplacements.samples[0]) { | ||
if ( | ||
variableReplacements.total === 1 | ||
&& shouldFix(variable) | ||
&& variableReplacements.samples[0] | ||
&& !variable.references.some(reference => reference.vueUsedInTemplate) | ||
) { | ||
const [replacement] = variableReplacements.samples; | ||
@@ -527,3 +547,3 @@ | ||
'Program:exit'() { | ||
'Program:exit'(program) { | ||
if (!options.checkVariables) { | ||
@@ -533,3 +553,3 @@ return; | ||
checkScope(context.getScope()); | ||
checkScope(context.sourceCode.getScope(program)); | ||
}, | ||
@@ -536,0 +556,0 @@ }; |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {isNewExpression, isStringLiteral} = require('./ast/index.js'); | ||
const {replaceStringLiteral} = require('./fix/index.js'); | ||
@@ -15,11 +15,2 @@ | ||
const templateLiteralSelector = [ | ||
newExpressionSelector({name: 'URL', argumentsLength: 2}), | ||
' > TemplateLiteral.arguments:first-child', | ||
].join(''); | ||
const literalSelector = [ | ||
newExpressionSelector({name: 'URL', argumentsLength: 2}), | ||
' > Literal.arguments:first-child', | ||
].join(''); | ||
const DOT_SLASH = './'; | ||
@@ -41,3 +32,3 @@ const TEST_URL_BASES = [ | ||
function canAddDotSlash(node, context) { | ||
function canAddDotSlash(node, sourceCode) { | ||
const url = node.value; | ||
@@ -49,7 +40,6 @@ if (url.startsWith(DOT_SLASH) || url.startsWith('.') || url.startsWith('/')) { | ||
const baseNode = node.parent.arguments[1]; | ||
const staticValueResult = getStaticValue(baseNode, context.getScope()); | ||
const staticValueResult = getStaticValue(baseNode, sourceCode.getScope(node)); | ||
if ( | ||
staticValueResult | ||
&& typeof staticValueResult.value === 'string' | ||
typeof staticValueResult?.value === 'string' | ||
&& isSafeToAddDotSlash(url, [staticValueResult.value]) | ||
@@ -63,3 +53,3 @@ ) { | ||
function canRemoveDotSlash(node, context) { | ||
function canRemoveDotSlash(node, sourceCode) { | ||
const rawValue = node.raw.slice(1, -1); | ||
@@ -71,7 +61,6 @@ if (!rawValue.startsWith(DOT_SLASH)) { | ||
const baseNode = node.parent.arguments[1]; | ||
const staticValueResult = getStaticValue(baseNode, context.getScope()); | ||
const staticValueResult = getStaticValue(baseNode, sourceCode.getScope(node)); | ||
if ( | ||
staticValueResult | ||
&& typeof staticValueResult.value === 'string' | ||
typeof staticValueResult?.value === 'string' | ||
&& isSafeToRemoveDotSlash(node.value, [staticValueResult.value]) | ||
@@ -85,4 +74,4 @@ ) { | ||
function addDotSlash(node, context) { | ||
if (!canAddDotSlash(node, context)) { | ||
function addDotSlash(node, sourceCode) { | ||
if (!canAddDotSlash(node, sourceCode)) { | ||
return; | ||
@@ -94,4 +83,4 @@ } | ||
function removeDotSlash(node, context) { | ||
if (!canRemoveDotSlash(node, context)) { | ||
function removeDotSlash(node, sourceCode) { | ||
if (!canRemoveDotSlash(node, sourceCode)) { | ||
return; | ||
@@ -111,3 +100,10 @@ } | ||
if (style === 'never') { | ||
listeners[templateLiteralSelector] = function (node) { | ||
listeners.TemplateLiteral = function (node) { | ||
if (!( | ||
isNewExpression(node.parent, {name: 'URL', argumentsLength: 2}) | ||
&& node.parent.arguments[0] === node | ||
)) { | ||
return; | ||
} | ||
const firstPart = node.quasis[0]; | ||
@@ -134,8 +130,13 @@ if (!firstPart.value.raw.startsWith(DOT_SLASH)) { | ||
listeners[literalSelector] = function (node) { | ||
if (typeof node.value !== 'string') { | ||
listeners.Literal = function (node) { | ||
if (!( | ||
isStringLiteral(node) | ||
&& isNewExpression(node.parent, {name: 'URL', argumentsLength: 2}) | ||
&& node.parent.arguments[0] === node | ||
)) { | ||
return; | ||
} | ||
const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node, context); | ||
const {sourceCode} = context; | ||
const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node, sourceCode); | ||
@@ -142,0 +143,0 @@ if (!fix) { |
'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'); | ||
@@ -10,36 +11,43 @@ const MESSAGE_ID = 'require-array-join-separator'; | ||
const selector = matches([ | ||
// `foo.join()` | ||
methodCallSelector({ | ||
method: 'join', | ||
argumentsLength: 0, | ||
includeOptionalMember: true, | ||
}), | ||
// `[].join.call(foo)` and `Array.prototype.join.call(foo)` | ||
[ | ||
methodCallSelector({method: 'call', argumentsLength: 1}), | ||
arrayPrototypeMethodSelector({path: 'callee.object', method: '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} */ | ||
@@ -46,0 +54,0 @@ module.exports = { |
'use strict'; | ||
const {methodCallSelector, not} = require('./selectors/index.js'); | ||
const {appendArgument} = require('./fix/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -10,33 +10,35 @@ const MESSAGE_ID = 'require-number-to-fixed-digits-argument'; | ||
const mathToFixed = [ | ||
methodCallSelector({ | ||
method: 'toFixed', | ||
argumentsLength: 0, | ||
}), | ||
not('[callee.object.type="NewExpression"]'), | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
return { | ||
[mathToFixed](node) { | ||
const [ | ||
openingParenthesis, | ||
closingParenthesis, | ||
] = sourceCode.getLastTokens(node, 2); | ||
const create = context => ({ | ||
CallExpression(node) { | ||
if ( | ||
!isMethodCall(node, { | ||
method: 'toFixed', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
|| node.callee.object.type === 'NewExpression' | ||
) { | ||
return; | ||
} | ||
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 {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} */ | ||
@@ -43,0 +45,0 @@ module.exports = { |
'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({method: 'postMessage', argumentsLength: 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('\'*\''); | ||
@@ -50,5 +51,5 @@ return { | ||
messageId: ERROR, | ||
suggest: suggestions.map(({messageId, data, code}) => ({ | ||
messageId, | ||
data, | ||
suggest: replacements.map(code => ({ | ||
messageId: SUGGESTION, | ||
data: {code}, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
@@ -55,0 +56,0 @@ fix: fixer => appendArgument(fixer, node, code, sourceCode), |
@@ -87,2 +87,5 @@ /* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */ | ||
}, | ||
elems: { | ||
elements: true, | ||
}, | ||
env: { | ||
@@ -181,2 +184,3 @@ environment: true, | ||
res: { | ||
resource: true, | ||
response: true, | ||
@@ -245,2 +249,4 @@ result: true, | ||
getInitialProps: true, | ||
getServerSideProps: true, | ||
getStaticProps: true, | ||
// React PropTypes | ||
@@ -247,0 +253,0 @@ // https://reactjs.org/docs/typechecking-with-proptypes.html |
'use strict'; | ||
const isSameReference = require('../utils/is-same-reference.js'); | ||
const {getParenthesizedRange} = require('../utils/parentheses.js'); | ||
const {isNumberLiteral} = require('../ast/index.js'); | ||
@@ -12,4 +13,3 @@ const isLengthMemberExpression = node => | ||
const isLiteralPositiveNumber = node => | ||
node.type === 'Literal' | ||
&& typeof node.value === 'number' | ||
isNumberLiteral(node) | ||
&& node.value > 0; | ||
@@ -16,0 +16,0 @@ |
'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,6 +39,7 @@ | ||
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.', | ||
findLastIndex: 'Use `.lastIndexOf()` instead of `findLastIndex() when looking for the index of an item.`', | ||
some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.`, | ||
@@ -54,76 +53,78 @@ }; | ||
const selector = [ | ||
methodCallSelector({ | ||
method, | ||
argumentsLength: 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 escapeString = require('./utils/escape-string.js'); | ||
const escapeTemplateElementRaw = require('./utils/escape-template-element-raw.js'); | ||
@@ -73,64 +73,63 @@ const {replaceTemplateElement} = require('./fix/index.js'); | ||
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 = { | ||
node, | ||
message, | ||
data: messageData, | ||
}; | ||
}, | ||
}; | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' | ||
? fixer => fixer.replaceText( | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' | ||
? fixer => { | ||
const [quote] = raw; | ||
return fixer.replaceText( | ||
node, | ||
quoteString(fixed, raw[0]), | ||
) | ||
: fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed), | ||
node.parent.type === 'JSXAttribute' ? quote + fixed + quote : escapeString(fixed, quote), | ||
); | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
data: messageData, | ||
fix, | ||
}, | ||
]; | ||
} | ||
: fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed), | ||
); | ||
return problem; | ||
}, | ||
}; | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
fix, | ||
}, | ||
]; | ||
} | ||
return problem; | ||
}); | ||
}; | ||
@@ -137,0 +136,0 @@ |
@@ -6,3 +6,3 @@ 'use strict'; | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {callExpressionSelector, methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall, isCallExpression} = require('./ast/index.js'); | ||
@@ -14,15 +14,33 @@ const MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE = 'template-indent'; | ||
const jestInlineSnapshotSelector = [ | ||
callExpressionSelector({name: 'expect', path: 'callee.object', argumentsLength: 1}), | ||
methodCallSelector({method: 'toMatchInlineSnapshot', argumentsLength: 1}), | ||
' > TemplateLiteral.arguments:first-child', | ||
].join(''); | ||
const isJestInlineSnapshot = node => | ||
isMethodCall(node.parent, { | ||
method: 'toMatchInlineSnapshot', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& node.parent.arguments[0] === node | ||
&& isCallExpression(node.parent.callee.object, { | ||
name: 'expect', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}); | ||
const parsedEsquerySelectors = new Map(); | ||
const parseEsquerySelector = selector => { | ||
if (!parsedEsquerySelectors.has(selector)) { | ||
parsedEsquerySelectors.set(selector, esquery.parse(selector)); | ||
} | ||
return parsedEsquerySelectors.get(selector); | ||
}; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const options = { | ||
tags: ['outdent', 'dedent', 'gql', 'sql', 'html', 'styled'], | ||
functions: ['dedent', 'stripIndent'], | ||
selectors: [jestInlineSnapshotSelector], | ||
selectors: [], | ||
comments: ['HTML', 'indent'], | ||
@@ -34,10 +52,4 @@ ...context.options[0], | ||
const selectors = [ | ||
...options.tags.map(tagName => `TaggedTemplateExpression[tag.name="${tagName}"] > .quasi`), | ||
...options.functions.map(functionName => `CallExpression[callee.name="${functionName}"] > .arguments`), | ||
...options.selectors, | ||
]; | ||
/** @param {import('@babel/core').types.TemplateLiteral} node */ | ||
const indentTemplateLiteralNode = node => { | ||
const getProblem = node => { | ||
const delimiter = '__PLACEHOLDER__' + Math.random(); | ||
@@ -73,5 +85,7 @@ const joined = node.quasis | ||
const dedented = stripIndent(joined); | ||
const trimmed = dedented.replaceAll(new RegExp(`^${eol}|${eol}[ \t]*$`, 'g'), ''); | ||
const fixed | ||
= eol | ||
+ indentString(dedented.trim(), 1, {indent: parentMargin + indent}) | ||
+ indentString(trimmed, 1, {indent: parentMargin + indent}) | ||
+ eol | ||
@@ -84,3 +98,3 @@ + parentMargin; | ||
context.report({ | ||
return { | ||
node, | ||
@@ -91,22 +105,55 @@ messageId: MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE, | ||
.map((replacement, index) => replaceTemplateElement(fixer, node.quasis[index], replacement)), | ||
}); | ||
}; | ||
}; | ||
const shouldIndent = node => { | ||
if (options.comments.length > 0) { | ||
const previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); | ||
if (previousToken?.type === 'Block' && options.comments.includes(previousToken.value.trim().toLowerCase())) { | ||
return true; | ||
} | ||
} | ||
if (isJestInlineSnapshot(node)) { | ||
return true; | ||
} | ||
if ( | ||
options.tags.length > 0 | ||
&& node.parent.type === 'TaggedTemplateExpression' | ||
&& node.parent.quasi === node | ||
&& node.parent.tag.type === 'Identifier' | ||
&& options.tags.includes(node.parent.tag.name) | ||
) { | ||
return true; | ||
} | ||
if ( | ||
options.functions.length > 0 | ||
&& node.parent.type === 'CallExpression' | ||
&& node.parent.arguments.includes(node) | ||
&& node.parent.callee.type === 'Identifier' | ||
&& options.functions.includes(node.parent.callee.name) | ||
) { | ||
return true; | ||
} | ||
if (options.selectors.length > 0) { | ||
const ancestors = sourceCode.getAncestors(node).reverse(); | ||
if (options.selectors.some(selector => esquery.matches(node, parseEsquerySelector(selector), ancestors))) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
return { | ||
/** @param {import('@babel/core').types.TemplateLiteral} node */ | ||
TemplateLiteral(node) { | ||
if (options.comments.length > 0) { | ||
const previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); | ||
if (previousToken && previousToken.type === 'Block' && options.comments.includes(previousToken.value.trim().toLowerCase())) { | ||
indentTemplateLiteralNode(node); | ||
return; | ||
} | ||
if (!shouldIndent(node)) { | ||
return; | ||
} | ||
const ancestry = context.getAncestors().reverse(); | ||
const shouldIndent = selectors.some(selector => esquery.matches(node, esquery.parse(selector), ancestry)); | ||
if (shouldIndent) { | ||
indentTemplateLiteralNode(node); | ||
} | ||
return getProblem(node); | ||
}, | ||
@@ -113,0 +160,0 @@ }; |
@@ -13,7 +13,11 @@ 'use strict'; | ||
switch (encoding.toLowerCase()) { | ||
case 'utf8': | ||
// eslint-disable-next-line unicorn/text-encoding-identifier-case | ||
case 'utf-8': | ||
case 'utf8': { | ||
return 'utf8'; | ||
case 'ascii': | ||
} | ||
case 'ascii': { | ||
return 'ascii'; | ||
} | ||
// No default | ||
@@ -28,3 +32,2 @@ } | ||
&& node.parent.arguments[1] === node | ||
&& node.parent.arguments[0] | ||
&& node.parent.arguments[0].type !== 'SpreadElement' | ||
@@ -44,2 +47,17 @@ && node.parent.callee.type === 'MemberExpression' | ||
if ( | ||
// eslint-disable-next-line unicorn/text-encoding-identifier-case | ||
node.value === 'utf-8' | ||
&& node.parent.type === 'JSXAttribute' | ||
&& node.parent.value === node | ||
&& node.parent.name.type === 'JSXIdentifier' | ||
&& node.parent.name.name.toLowerCase() === 'charset' | ||
&& node.parent.parent.type === 'JSXOpeningElement' | ||
&& node.parent.parent.attributes.includes(node.parent) | ||
&& node.parent.parent.name.type === 'JSXIdentifier' | ||
&& node.parent.parent.name.name.toLowerCase() === 'meta' | ||
) { | ||
return; | ||
} | ||
const {raw} = node; | ||
@@ -53,7 +71,2 @@ const value = raw.slice(1, -1); | ||
const messageData = { | ||
value, | ||
replacement, | ||
}; | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
@@ -65,3 +78,6 @@ const fix = fixer => replaceStringLiteral(fixer, node, replacement); | ||
messageId: MESSAGE_ID_ERROR, | ||
data: messageData, | ||
data: { | ||
value, | ||
replacement, | ||
}, | ||
}; | ||
@@ -77,3 +93,2 @@ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: messageData, | ||
fix: fixer => replaceStringLiteral(fixer, node, replacement), | ||
@@ -80,0 +95,0 @@ }, |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const {switchCallExpressionToNewExpression} = require('./fix/index.js'); | ||
@@ -12,29 +11,31 @@ | ||
const selector = [ | ||
'ThrowStatement', | ||
' > ', | ||
'CallExpression.argument', | ||
matches([ | ||
// `throw FooError()` | ||
[ | ||
'[callee.type="Identifier"]', | ||
`[callee.name=/${customError.source}/]`, | ||
].join(''), | ||
// `throw lib.FooError()` | ||
[ | ||
'[callee.type="MemberExpression"]', | ||
'[callee.computed!=true]', | ||
'[callee.property.type="Identifier"]', | ||
`[callee.property.name=/${customError.source}/]`, | ||
].join(''), | ||
]), | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector]: node => ({ | ||
node, | ||
messageId, | ||
fix: fixer => switchCallExpressionToNewExpression(node, context.getSourceCode(), fixer), | ||
}), | ||
CallExpression(node) { | ||
if (!( | ||
node.parent.type === 'ThrowStatement' | ||
&& node.parent.argument === node | ||
)) { | ||
return; | ||
} | ||
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 => switchCallExpressionToNewExpression(node, context.sourceCode, fixer), | ||
}; | ||
}, | ||
}); | ||
@@ -41,0 +42,0 @@ |
@@ -5,3 +5,3 @@ 'use strict'; | ||
function assertToken(token, {test, expected, ruleId}) { | ||
if (test && test(token)) { | ||
if (test?.(token)) { | ||
return; | ||
@@ -8,0 +8,0 @@ } |
@@ -102,3 +102,3 @@ 'use strict'; | ||
const isUnresolvedName = (name, scope) => | ||
getReferences(scope).some(({identifier, resolved}) => identifier && identifier.name === name && !resolved); | ||
getReferences(scope).some(({identifier, resolved}) => identifier?.name === name && !resolved); | ||
@@ -105,0 +105,0 @@ const isSafeName = (name, scopes) => |
@@ -5,16 +5,7 @@ '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?.type === 'CallExpression' | ||
&& node.callee.type === 'Identifier' | ||
@@ -24,4 +15,3 @@ && node.callee.name === 'Boolean' | ||
const isVueBooleanAttributeValue = node => | ||
node | ||
&& node.type === 'VExpressionContainer' | ||
node?.type === 'VExpressionContainer' | ||
&& node.parent.type === 'VAttribute' | ||
@@ -28,0 +18,0 @@ && node.parent.directive |
'use strict'; | ||
module.exports = string => string.replace( | ||
module.exports = string => string.replaceAll( | ||
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g, | ||
'\\$<symbol>', | ||
); |
'use strict'; | ||
const {isOpeningParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken} = require('@eslint-community/eslint-utils'); | ||
@@ -4,0 +4,0 @@ /** |
'use strict'; | ||
const path = require('path'); | ||
const path = require('node:path'); | ||
const packageJson = require('../../package.json'); | ||
@@ -4,0 +4,0 @@ |
'use strict'; | ||
const {isColonToken} = require('eslint-utils'); | ||
const {isColonToken} = require('@eslint-community/eslint-utils'); | ||
@@ -5,0 +5,0 @@ /** |
'use strict'; | ||
const {findVariable} = require('eslint-utils'); | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
@@ -4,0 +4,0 @@ const getReferences = (scope, nodeOrName) => { |
'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 === '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' | ||
@@ -9,0 +18,0 @@ && node.parent.operator === 'delete' |
@@ -13,6 +13,5 @@ 'use strict'; | ||
const isLogicalExpression = node => | ||
node | ||
&& node.type === 'LogicalExpression' | ||
node?.type === 'LogicalExpression' | ||
&& (node.operator === '&&' || node.operator === '||'); | ||
module.exports = isLogicalExpression; |
'use strict'; | ||
const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils'); | ||
const {isOpeningParenToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
@@ -5,0 +5,0 @@ /** |
'use strict'; | ||
const {getStaticValue} = require('eslint-utils'); | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {isNumberLiteral} = require('../ast/index.js'); | ||
@@ -103,8 +104,5 @@ const isStaticProperties = (node, object, properties) => | ||
const isStaticNumber = (node, scope) => { | ||
const staticResult = getStaticValue(node, scope); | ||
return staticResult !== null && typeof staticResult.value === 'number'; | ||
}; | ||
const isStaticNumber = (node, scope) => | ||
typeof getStaticValue(node, scope)?.value === 'number'; | ||
const isNumberLiteral = node => node.type === 'Literal' && typeof node.value === 'number'; | ||
const isLengthProperty = node => | ||
@@ -184,3 +182,3 @@ node.type === 'MemberExpression' | ||
case 'UpdateExpression': | ||
case 'UpdateExpression': { | ||
if (isNumber(node.argument, scope)) { | ||
@@ -191,2 +189,4 @@ return true; | ||
break; | ||
} | ||
case 'ConditionalExpression': { | ||
@@ -215,3 +215,3 @@ const isConsequentNumber = isNumber(node.consequent, scope); | ||
case 'SequenceExpression': { | ||
if (isNumber(node.expressions[node.expressions.length - 1], scope)) { | ||
if (isNumber(node.expressions.at(-1), scope)) { | ||
return true; | ||
@@ -218,0 +218,0 @@ } |
'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,10 +39,12 @@ /** | ||
switch (node && node.type) { | ||
case 'MemberExpression': | ||
switch (node?.type) { | ||
case 'MemberExpression': { | ||
property = node.property; | ||
break; | ||
} | ||
/* c8 ignore next 2 */ | ||
case 'ChainExpression': | ||
case 'ChainExpression': { | ||
return getStaticPropertyName(node.expression); | ||
} | ||
@@ -52,6 +54,7 @@ // Only reachable when use this to get class/object member key | ||
case 'Property': | ||
case 'MethodDefinition': | ||
case 'MethodDefinition': { | ||
/* c8 ignore next 2 */ | ||
property = node.key; | ||
break; | ||
} | ||
@@ -126,13 +129,18 @@ // No default | ||
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' | ||
left.computed === right.computed | ||
&& isSameReference(left.object, right.object) | ||
&& nameA === getStaticPropertyName(right) | ||
&& isSameReference(left.property, right.property) | ||
); | ||
} | ||
default: | ||
default: { | ||
return false; | ||
} | ||
} | ||
@@ -154,0 +175,0 @@ } |
@@ -28,4 +28,3 @@ 'use strict'; | ||
return ( | ||
reference | ||
&& reference.resolved | ||
Boolean(reference?.resolved) | ||
&& reference.resolved.defs.length > 0 | ||
@@ -32,0 +31,0 @@ ); |
'use strict'; | ||
module.exports = ({parent}) => !parent || parent.type === 'ExpressionStatement'; | ||
const {isExpressionStatement} = require('../ast/index.js'); | ||
module.exports = node => isExpressionStatement(node.parent); |
'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); | ||
@@ -49,4 +49,2 @@ function getPrefix(text) { | ||
isDecimalInteger, | ||
isNumber, | ||
isBigInt, | ||
isNumeric, | ||
@@ -53,0 +51,0 @@ isLegacyOctal, |
'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; | ||
@@ -57,3 +52,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]; | ||
@@ -60,0 +55,0 @@ } |
'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'; | ||
@@ -19,3 +19,3 @@ class FixAbortError extends Error {} | ||
if (result && isIterable(result)) { | ||
if (isIterable(result)) { | ||
try { | ||
@@ -37,31 +37,31 @@ return [...result]; | ||
function reportListenerProblems(listener, context) { | ||
// Listener arguments can be `codePath, node` or `node` | ||
return function (...listenerArguments) { | ||
let problems = listener(...listenerArguments); | ||
function reportListenerProblems(problems, context) { | ||
if (!problems) { | ||
return; | ||
} | ||
if (!problems) { | ||
return; | ||
} | ||
if (!isIterable(problems)) { | ||
problems = [problems]; | ||
} | ||
if (!isIterable(problems)) { | ||
problems = [problems]; | ||
for (const problem of problems) { | ||
if (problem.fix) { | ||
problem.fix = wrapFixFunction(problem.fix); | ||
} | ||
for (const problem of problems) { | ||
if (problem.fix) { | ||
problem.fix = wrapFixFunction(problem.fix); | ||
} | ||
if (Array.isArray(problem.suggest)) { | ||
for (const suggest of problem.suggest) { | ||
if (suggest.fix) { | ||
suggest.fix = wrapFixFunction(suggest.fix); | ||
} | ||
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); | ||
} | ||
}; | ||
context.report(problem); | ||
} | ||
} | ||
@@ -76,7 +76,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); | ||
@@ -99,11 +143,9 @@ | ||
const listeners = create(context); | ||
const {parserServices} = context.sourceCode; | ||
// `vue-eslint-parser` | ||
if ( | ||
context.parserServices | ||
&& context.parserServices.defineTemplateBodyVisitor | ||
) { | ||
if (parserServices?.defineTemplateBodyVisitor) { | ||
return visitScriptBlock | ||
? context.parserServices.defineTemplateBodyVisitor(listeners, listeners) | ||
: context.parserServices.defineTemplateBodyVisitor(listeners); | ||
? parserServices.defineTemplateBodyVisitor(listeners, listeners) | ||
: parserServices.defineTemplateBodyVisitor(listeners); | ||
} | ||
@@ -110,0 +152,0 @@ |
@@ -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,8 +10,15 @@ 'use strict'; | ||
function shouldAddParenthesesToLogicalExpressionChild(node, {operator, property}) { | ||
// When operator or property is different, need check `LogicalExpression` operator precedence, not implemented | ||
// We are not using this, but we can improve this function with it | ||
/* c8 ignore next 3 */ | ||
if (operator !== '??' || property !== 'left') { | ||
throw new Error('Not supported.'); | ||
if (!property) { | ||
throw new Error('`property` is required.'); | ||
} | ||
if ( | ||
node.type === 'LogicalExpression' | ||
&& node.operator === operator | ||
) { | ||
return false; | ||
} | ||
// Not really needed, but more readable | ||
@@ -28,5 +35,6 @@ if ( | ||
if ( | ||
node.type === 'ConditionalExpression' | ||
node.type === 'LogicalExpression' | ||
|| node.type === 'ConditionalExpression' | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'AssignmentExpression' | ||
|| node.type === 'ArrowFunctionExpression' | ||
|| node.type === 'YieldExpression' | ||
@@ -33,0 +41,0 @@ || node.type === 'SequenceExpression' |
@@ -24,6 +24,10 @@ 'use strict'; | ||
case 'ArrayExpression': | ||
case 'FunctionExpression': | ||
case 'FunctionExpression': { | ||
return false; | ||
case 'NewExpression': | ||
} | ||
case 'NewExpression': { | ||
return !isNewExpressionWithParentheses(node, sourceCode); | ||
} | ||
case 'Literal': { | ||
@@ -38,4 +42,5 @@ /* c8 ignore next */ | ||
default: | ||
default: { | ||
return true; | ||
} | ||
} | ||
@@ -42,0 +47,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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
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
657057
213
20763
357
2
17
28
1
+ Added@eslint/eslintrc@^2.1.4
+ Addedcore-js-compat@^3.34.0
+ Addedjsesc@^3.0.2
+ Addedregjsparser@^0.10.0
+ Added@eslint/eslintrc@2.1.4(transitive)
+ Addedbrowserslist@4.23.3(transitive)
+ Addedcaniuse-lite@1.0.30001657(transitive)
+ Addedci-info@4.0.0(transitive)
+ Addedcore-js-compat@3.38.1(transitive)
+ Addedelectron-to-chromium@1.5.15(transitive)
+ Addedescalade@3.2.0(transitive)
+ Addedespree@9.6.1(transitive)
+ Addedglobals@13.24.0(transitive)
+ Addedjsesc@0.5.03.0.2(transitive)
+ Addednode-releases@2.0.18(transitive)
+ Addedregjsparser@0.10.0(transitive)
+ Addedtype-fest@0.20.2(transitive)
+ Addedupdate-browserslist-db@1.1.0(transitive)
- Removedeslint-utils@^3.0.0
- Removedlodash@^4.17.21
- Removedsafe-regex@^2.1.1
- Removedci-info@3.9.0(transitive)
- Removedeslint-utils@3.0.0(transitive)
- Removedeslint-visitor-keys@2.1.0(transitive)
- Removedlodash@4.17.21(transitive)
- Removedsafe-regex@2.1.1(transitive)
Updatedci-info@^4.0.0
Updatedesquery@^1.5.0
Updatedis-builtin-module@^3.2.1
Updatedregexp-tree@^0.1.27
Updatedsemver@^7.5.4