eslint-plugin-unicorn
Advanced tools
Comparing version 45.0.2 to 48.0.1
'use strict'; | ||
module.exports = { | ||
env: { | ||
es2022: true, | ||
es2024: true, | ||
}, | ||
@@ -59,3 +59,2 @@ parserOptions: { | ||
'unicorn/no-unreadable-iife': 'error', | ||
'unicorn/no-unsafe-regex': 'off', | ||
'unicorn/no-unused-properties': 'off', | ||
@@ -77,4 +76,4 @@ 'unicorn/no-useless-fallback-in-spread': '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-at': 'error', | ||
'unicorn/prefer-blob-reading-methods': 'error', | ||
'unicorn/prefer-code-point': 'error', | ||
@@ -87,4 +86,3 @@ 'unicorn/prefer-date-now': 'error', | ||
'unicorn/prefer-dom-node-text-content': 'error', | ||
// TODO: Enable this by default when targeting Node.js 16. | ||
'unicorn/prefer-event-target': 'off', | ||
'unicorn/prefer-event-target': 'error', | ||
'unicorn/prefer-export-from': 'error', | ||
@@ -112,4 +110,3 @@ 'unicorn/prefer-includes': 'error', | ||
'unicorn/prefer-spread': 'error', | ||
// TODO: Enable this by default when targeting Node.js 16. | ||
'unicorn/prefer-string-replace-all': 'off', | ||
'unicorn/prefer-string-replace-all': 'error', | ||
'unicorn/prefer-string-slice': 'error', | ||
@@ -116,0 +113,0 @@ 'unicorn/prefer-string-starts-ends-with': 'error', |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const allRulesEnabledConfig = require('./configs/all.js'); | ||
const {name, version} = require('./package.json'); | ||
@@ -14,2 +15,3 @@ const deprecatedRules = createDeprecatedRules({ | ||
'no-reduce': 'unicorn/no-array-reduce', | ||
'no-unsafe-regex': [], | ||
'prefer-dataset': 'unicorn/prefer-dom-node-dataset', | ||
@@ -30,2 +32,6 @@ 'prefer-event-key': 'unicorn/prefer-keyboard-event-key', | ||
module.exports = { | ||
meta: { | ||
name, | ||
version, | ||
}, | ||
rules: { | ||
@@ -32,0 +38,0 @@ ...loadRules(), |
{ | ||
"name": "eslint-plugin-unicorn", | ||
"version": "45.0.2", | ||
"version": "48.0.1", | ||
"description": "More than 100 powerful ESLint rules", | ||
@@ -14,3 +14,3 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">=14.18" | ||
"node": ">=16" | ||
}, | ||
@@ -20,3 +20,3 @@ "scripts": { | ||
"fix": "run-p --continue-on-error fix:*", | ||
"fix:eslint-docs": "eslint-doc-generator --ignore-deprecated-rules --ignore-config all --rule-doc-title-format desc --url-configs \"https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs\"", | ||
"fix:eslint-docs": "eslint-doc-generator", | ||
"fix:js": "npm run lint:js -- --fix", | ||
@@ -51,9 +51,9 @@ "fix:md": "npm run lint:md -- --fix", | ||
"dependencies": { | ||
"@babel/helper-validator-identifier": "^7.19.1", | ||
"@eslint-community/eslint-utils": "^4.1.2", | ||
"ci-info": "^3.6.1", | ||
"@babel/helper-validator-identifier": "^7.22.5", | ||
"@eslint-community/eslint-utils": "^4.4.0", | ||
"ci-info": "^3.8.0", | ||
"clean-regexp": "^1.0.0", | ||
"esquery": "^1.4.0", | ||
"esquery": "^1.5.0", | ||
"indent-string": "^4.0.0", | ||
"is-builtin-module": "^3.2.0", | ||
"is-builtin-module": "^3.2.1", | ||
"jsesc": "^3.0.2", | ||
@@ -63,40 +63,39 @@ "lodash": "^4.17.21", | ||
"read-pkg-up": "^7.0.1", | ||
"regexp-tree": "^0.1.24", | ||
"regjsparser": "^0.9.1", | ||
"safe-regex": "^2.1.1", | ||
"semver": "^7.3.8", | ||
"regexp-tree": "^0.1.27", | ||
"regjsparser": "^0.10.0", | ||
"semver": "^7.5.4", | ||
"strip-indent": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/code-frame": "^7.18.6", | ||
"@babel/core": "^7.20.2", | ||
"@babel/eslint-parser": "^7.19.1", | ||
"@babel/code-frame": "^7.22.5", | ||
"@babel/core": "^7.22.8", | ||
"@babel/eslint-parser": "^7.22.7", | ||
"@lubien/fixture-beta-package": "^1.0.0-beta.1", | ||
"@typescript-eslint/parser": "^5.43.0", | ||
"@typescript-eslint/parser": "^6.2.0", | ||
"ava": "^3.15.0", | ||
"c8": "^7.12.0", | ||
"chalk": "^5.1.2", | ||
"c8": "^8.0.0", | ||
"chalk": "^5.3.0", | ||
"enquirer": "^2.3.6", | ||
"eslint": "^8.28.0", | ||
"eslint": "^8.44.0", | ||
"eslint-ava-rule-tester": "^4.0.0", | ||
"eslint-doc-generator": "^1.0.0", | ||
"eslint-plugin-eslint-plugin": "^5.0.6", | ||
"eslint-doc-generator": "^1.4.3", | ||
"eslint-plugin-eslint-plugin": "^5.1.0", | ||
"eslint-plugin-internal-rules": "file:./scripts/internal-rules/", | ||
"eslint-remote-tester": "^3.0.0", | ||
"eslint-remote-tester-repositories": "^0.0.7", | ||
"execa": "^6.1.0", | ||
"eslint-remote-tester-repositories": "^1.0.1", | ||
"execa": "^7.1.1", | ||
"listr": "^0.14.3", | ||
"lodash-es": "^4.17.21", | ||
"markdownlint-cli": "^0.32.2", | ||
"markdownlint-cli": "^0.35.0", | ||
"mem": "^9.0.2", | ||
"npm-package-json-lint": "^6.3.0", | ||
"npm-package-json-lint": "^7.0.0", | ||
"npm-run-all": "^4.1.5", | ||
"outdent": "^0.8.0", | ||
"typescript": "^4.9.3", | ||
"vue-eslint-parser": "^9.1.0", | ||
"xo": "^0.53.1", | ||
"yaml": "^2.1.3" | ||
"typescript": "^5.1.6", | ||
"vue-eslint-parser": "^9.3.1", | ||
"xo": "^0.54.2", | ||
"yaml": "^2.3.1" | ||
}, | ||
"peerDependencies": { | ||
"eslint": ">=8.28.0" | ||
"eslint": ">=8.44.0" | ||
}, | ||
@@ -116,2 +115,5 @@ "ava": { | ||
"xo": { | ||
"extends": [ | ||
"plugin:internal-rules/all" | ||
], | ||
"ignores": [ | ||
@@ -123,2 +125,3 @@ ".cache-eslint-remote-tester", | ||
"rules": { | ||
"unicorn/expiring-todo-comments": "off", | ||
"unicorn/no-null": "error", | ||
@@ -133,3 +136,4 @@ "unicorn/prefer-array-flat": [ | ||
} | ||
] | ||
], | ||
"import/order": "off" | ||
}, | ||
@@ -170,10 +174,2 @@ "overrides": [ | ||
} | ||
}, | ||
{ | ||
"files": [ | ||
"rules/**/*.js" | ||
], | ||
"extends": [ | ||
"plugin:internal-rules/all" | ||
] | ||
} | ||
@@ -180,0 +176,0 @@ ] |
225
readme.md
@@ -29,3 +29,3 @@ # 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) | ||
"env": { | ||
"es2022": true | ||
"es2024": true | ||
}, | ||
@@ -53,3 +53,2 @@ "parserOptions": { | ||
💼 [Configurations](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs) enabled in.\ | ||
🚫 [Configurations](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs) disabled in.\ | ||
✅ Set in the `recommended` [configuration](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs).\ | ||
@@ -59,113 +58,113 @@ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ | ||
| Name | Description | 💼 | 🚫 | 🔧 | 💡 | | ||
| :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | :- | | ||
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | ✅ | | 🔧 | | | ||
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. | ✅ | | 🔧 | | | ||
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | ✅ | | 🔧 | 💡 | | ||
| [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. | ✅ | | | | | ||
| [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | ✅ | 🔧 | | | ||
| [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. | ✅ | | 🔧 | | | ||
| [error-message](docs/rules/error-message.md) | Enforce passing a `message` value when creating a built-in error. | ✅ | | | | | ||
| [escape-case](docs/rules/escape-case.md) | Require escape sequences to use uppercase values. | ✅ | | 🔧 | | | ||
| [expiring-todo-comments](docs/rules/expiring-todo-comments.md) | Add expiration conditions to TODO comments. | ✅ | | | | | ||
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. | ✅ | | 🔧 | 💡 | | ||
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. | ✅ | | | | | ||
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ | | | | | ||
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ | | 🔧 | | | ||
| [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | ✅ | | | | | ||
| [no-array-callback-reference](docs/rules/no-array-callback-reference.md) | Prevent passing a function reference directly to iterator methods. | ✅ | | | 💡 | | ||
| [no-array-for-each](docs/rules/no-array-for-each.md) | Prefer `for…of` over the `forEach` method. | ✅ | | 🔧 | 💡 | | ||
| [no-array-method-this-argument](docs/rules/no-array-method-this-argument.md) | Disallow using the `this` argument in array methods. | ✅ | | 🔧 | 💡 | | ||
| [no-array-push-push](docs/rules/no-array-push-push.md) | Enforce combining multiple `Array#push()` into one call. | ✅ | | 🔧 | 💡 | | ||
| [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | ✅ | | | | | ||
| [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. | ✅ | | 🔧 | | | ||
| [no-console-spaces](docs/rules/no-console-spaces.md) | Do not use leading/trailing space between `console.log` parameters. | ✅ | | 🔧 | | | ||
| [no-document-cookie](docs/rules/no-document-cookie.md) | Do not use `document.cookie` directly. | ✅ | | | | | ||
| [no-empty-file](docs/rules/no-empty-file.md) | Disallow empty files. | ✅ | | | | | ||
| [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. | ✅ | | 🔧 | | | ||
| [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. | ✅ | | 🔧 | | | ||
| [no-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. | ✅ | | 🔧 | | | ||
| [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. | ✅ | | | | | ||
| [no-keyword-prefix](docs/rules/no-keyword-prefix.md) | Disallow identifiers starting with `new` or `class`. | | ✅ | | | | ||
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | ✅ | | 🔧 | | | ||
| [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | ✅ | | 🔧 | | | ||
| [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. | ✅ | | 🔧 | | | ||
| [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. | ✅ | | 🔧 | 💡 | | ||
| [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. | ✅ | | 🔧 | 💡 | | ||
| [no-null](docs/rules/no-null.md) | Disallow the use of the `null` literal. | ✅ | | 🔧 | 💡 | | ||
| [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) | Disallow the use of objects as default parameters. | ✅ | | | | | ||
| [no-process-exit](docs/rules/no-process-exit.md) | Disallow `process.exit()`. | ✅ | | | | | ||
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. | ✅ | | 🔧 | | | ||
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. | ✅ | | | | | ||
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. | ✅ | | | | | ||
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. | ✅ | | 🔧 | 💡 | | ||
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. | ✅ | | 🔧 | | | ||
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. | ✅ | | 🔧 | | | ||
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. | ✅ | | | | | ||
| [no-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(…)` 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-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` for `{String,Array,TypedArray}#{slice,at}()` and `Array#splice()`. | ✅ | | 🔧 | | | ||
| [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()` and `String#split('')`. | ✅ | | 🔧 | 💡 | | ||
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. | | ✅ | 🔧 | | | ||
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. | ✅ | | 🔧 | | | ||
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. | ✅ | | 🔧 | 💡 | | ||
| [prefer-string-trim-start-end](docs/rules/prefer-string-trim-start-end.md) | Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. | ✅ | | 🔧 | | | ||
| [prefer-switch](docs/rules/prefer-switch.md) | Prefer `switch` over multiple `else-if`. | ✅ | | 🔧 | | | ||
| [prefer-ternary](docs/rules/prefer-ternary.md) | Prefer ternary expressions over simple `if-else` statements. | ✅ | | 🔧 | | | ||
| [prefer-top-level-await](docs/rules/prefer-top-level-await.md) | Prefer top-level await over top-level promises and async function calls. | ✅ | | | 💡 | | ||
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. | ✅ | | 🔧 | | | ||
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. | ✅ | | 🔧 | | | ||
| [relative-url-style](docs/rules/relative-url-style.md) | Enforce consistent relative URL style. | ✅ | | 🔧 | 💡 | | ||
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. | ✅ | | 🔧 | | | ||
| [require-number-to-fixed-digits-argument](docs/rules/require-number-to-fixed-digits-argument.md) | Enforce using the digits argument with `Number#toFixed()`. | ✅ | | 🔧 | | | ||
| [require-post-message-target-origin](docs/rules/require-post-message-target-origin.md) | Enforce using the `targetOrigin` argument with `window.postMessage()`. | | ✅ | | 💡 | | ||
| [string-content](docs/rules/string-content.md) | Enforce better string content. | | ✅ | 🔧 | 💡 | | ||
| [switch-case-braces](docs/rules/switch-case-braces.md) | Enforce consistent brace style for `case` clauses. | ✅ | | 🔧 | | | ||
| [template-indent](docs/rules/template-indent.md) | Fix whitespace-insensitive template indentation. | ✅ | | 🔧 | | | ||
| [text-encoding-identifier-case](docs/rules/text-encoding-identifier-case.md) | Enforce consistent case for text encoding identifiers. | ✅ | | 🔧 | 💡 | | ||
| [throw-new-error](docs/rules/throw-new-error.md) | Require `new` when throwing an error. | ✅ | | 🔧 | | | ||
| Name | Description | 💼 | 🔧 | 💡 | | ||
| :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | | ||
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | ✅ | 🔧 | | | ||
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. | ✅ | 🔧 | | | ||
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | ✅ | 🔧 | 💡 | | ||
| [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. | ✅ | | | | ||
| [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | 🔧 | | | ||
| [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. | ✅ | 🔧 | | | ||
| [error-message](docs/rules/error-message.md) | Enforce passing a `message` value when creating a built-in error. | ✅ | | | | ||
| [escape-case](docs/rules/escape-case.md) | Require escape sequences to use uppercase values. | ✅ | 🔧 | | | ||
| [expiring-todo-comments](docs/rules/expiring-todo-comments.md) | Add expiration conditions to TODO comments. | ✅ | | | | ||
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. | ✅ | 🔧 | 💡 | | ||
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. | ✅ | | | | ||
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ | | | | ||
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ | 🔧 | | | ||
| [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | ✅ | | | | ||
| [no-array-callback-reference](docs/rules/no-array-callback-reference.md) | Prevent passing a function reference directly to iterator methods. | ✅ | | 💡 | | ||
| [no-array-for-each](docs/rules/no-array-for-each.md) | Prefer `for…of` over the `forEach` method. | ✅ | 🔧 | 💡 | | ||
| [no-array-method-this-argument](docs/rules/no-array-method-this-argument.md) | Disallow using the `this` argument in array methods. | ✅ | 🔧 | 💡 | | ||
| [no-array-push-push](docs/rules/no-array-push-push.md) | Enforce combining multiple `Array#push()` into one call. | ✅ | 🔧 | 💡 | | ||
| [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | ✅ | | | | ||
| [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. | ✅ | 🔧 | | | ||
| [no-console-spaces](docs/rules/no-console-spaces.md) | Do not use leading/trailing space between `console.log` parameters. | ✅ | 🔧 | | | ||
| [no-document-cookie](docs/rules/no-document-cookie.md) | Do not use `document.cookie` directly. | ✅ | | | | ||
| [no-empty-file](docs/rules/no-empty-file.md) | Disallow empty files. | ✅ | | | | ||
| [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. | ✅ | 🔧 | | | ||
| [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. | ✅ | 🔧 | | | ||
| [no-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. | ✅ | 🔧 | | | ||
| [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. | ✅ | | | | ||
| [no-keyword-prefix](docs/rules/no-keyword-prefix.md) | Disallow identifiers starting with `new` or `class`. | | | | | ||
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | ✅ | 🔧 | | | ||
| [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | ✅ | 🔧 | | | ||
| [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. | ✅ | 🔧 | | | ||
| [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. | ✅ | 🔧 | 💡 | | ||
| [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. | ✅ | 🔧 | 💡 | | ||
| [no-null](docs/rules/no-null.md) | Disallow the use of the `null` literal. | ✅ | 🔧 | 💡 | | ||
| [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) | Disallow the use of objects as default parameters. | ✅ | | | | ||
| [no-process-exit](docs/rules/no-process-exit.md) | Disallow `process.exit()`. | ✅ | | | | ||
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. | ✅ | 🔧 | | | ||
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. | ✅ | | | | ||
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. | ✅ | | | | ||
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. | ✅ | 🔧 | 💡 | | ||
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. | ✅ | 🔧 | | | ||
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. | ✅ | 🔧 | | | ||
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. | ✅ | | | | ||
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | | | ||
| [no-useless-fallback-in-spread](docs/rules/no-useless-fallback-in-spread.md) | Disallow useless fallback when spreading in object literals. | ✅ | 🔧 | | | ||
| [no-useless-length-check](docs/rules/no-useless-length-check.md) | Disallow useless array length check. | ✅ | 🔧 | | | ||
| [no-useless-promise-resolve-reject](docs/rules/no-useless-promise-resolve-reject.md) | Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks | ✅ | 🔧 | | | ||
| [no-useless-spread](docs/rules/no-useless-spread.md) | Disallow unnecessary spread. | ✅ | 🔧 | | | ||
| [no-useless-switch-case](docs/rules/no-useless-switch-case.md) | Disallow useless case in switch statements. | ✅ | | 💡 | | ||
| [no-useless-undefined](docs/rules/no-useless-undefined.md) | Disallow useless `undefined`. | ✅ | 🔧 | | | ||
| [no-zero-fractions](docs/rules/no-zero-fractions.md) | Disallow number literals with zero fractions or dangling dots. | ✅ | 🔧 | | | ||
| [number-literal-case](docs/rules/number-literal-case.md) | Enforce proper case for numeric literals. | ✅ | 🔧 | | | ||
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the style of numeric separators by correctly grouping digits. | ✅ | 🔧 | | | ||
| [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) | Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. | ✅ | 🔧 | | | ||
| [prefer-array-find](docs/rules/prefer-array-find.md) | Prefer `.find(…)` and `.findLast(…)` over the first or last element from `.filter(…)`. | ✅ | 🔧 | 💡 | | ||
| [prefer-array-flat](docs/rules/prefer-array-flat.md) | Prefer `Array#flat()` over legacy techniques to flatten arrays. | ✅ | 🔧 | | | ||
| [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) | Prefer `.flatMap(…)` over `.map(…).flat()`. | ✅ | 🔧 | | | ||
| [prefer-array-index-of](docs/rules/prefer-array-index-of.md) | Prefer `Array#{indexOf,lastIndexOf}()` over `Array#{findIndex,findLastIndex}()` when looking for the index of an item. | ✅ | 🔧 | 💡 | | ||
| [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`. | ✅ | 🔧 | 💡 | | ||
| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. | ✅ | 🔧 | 💡 | | ||
| [prefer-blob-reading-methods](docs/rules/prefer-blob-reading-methods.md) | Prefer `Blob#arrayBuffer()` over `FileReader#readAsArrayBuffer(…)` and `Blob#text()` over `FileReader#readAsText(…)`. | ✅ | | | | ||
| [prefer-code-point](docs/rules/prefer-code-point.md) | Prefer `String#codePointAt(…)` over `String#charCodeAt(…)` and `String.fromCodePoint(…)` over `String.fromCharCode(…)`. | ✅ | | 💡 | | ||
| [prefer-date-now](docs/rules/prefer-date-now.md) | Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. | ✅ | 🔧 | | | ||
| [prefer-default-parameters](docs/rules/prefer-default-parameters.md) | Prefer default parameters over reassignment. | ✅ | 🔧 | 💡 | | ||
| [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) | Prefer `Node#append()` over `Node#appendChild()`. | ✅ | 🔧 | | | ||
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over calling attribute methods. | ✅ | 🔧 | | | ||
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. | ✅ | 🔧 | 💡 | | ||
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | ✅ | | 💡 | | ||
| [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | ✅ | | | | ||
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | ✅ | 🔧 | 💡 | | ||
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 | | ||
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | | | ||
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ | 🔧 | | | ||
| [prefer-logical-operator-over-ternary](docs/rules/prefer-logical-operator-over-ternary.md) | Prefer using a logical operator over a ternary. | ✅ | | 💡 | | ||
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | ✅ | 🔧 | 💡 | | ||
| [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. | ✅ | 🔧 | | | ||
| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. | ✅ | 🔧 | | | ||
| [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | 💡 | | ||
| [prefer-native-coercion-functions](docs/rules/prefer-native-coercion-functions.md) | Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly. | ✅ | 🔧 | | | ||
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` when possible. | ✅ | 🔧 | | | ||
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ | 🔧 | | | ||
| [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 | 💡 | | ||
| [prefer-object-from-entries](docs/rules/prefer-object-from-entries.md) | Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object. | ✅ | 🔧 | | | ||
| [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | | | ||
| [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. | ✅ | 🔧 | | | ||
| [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. | ✅ | 🔧 | | | ||
| [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ | 🔧 | | | ||
| [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ | 🔧 | 💡 | | ||
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 | | ||
| [prefer-set-size](docs/rules/prefer-set-size.md) | Prefer using `Set#size` instead of `Array#length`. | ✅ | 🔧 | | | ||
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split('')`. | ✅ | 🔧 | 💡 | | ||
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. | ✅ | 🔧 | | | ||
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. | ✅ | 🔧 | | | ||
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. | ✅ | 🔧 | 💡 | | ||
| [prefer-string-trim-start-end](docs/rules/prefer-string-trim-start-end.md) | Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. | ✅ | 🔧 | | | ||
| [prefer-switch](docs/rules/prefer-switch.md) | Prefer `switch` over multiple `else-if`. | ✅ | 🔧 | | | ||
| [prefer-ternary](docs/rules/prefer-ternary.md) | Prefer ternary expressions over simple `if-else` statements. | ✅ | 🔧 | | | ||
| [prefer-top-level-await](docs/rules/prefer-top-level-await.md) | Prefer top-level await over top-level promises and async function calls. | ✅ | | 💡 | | ||
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. | ✅ | 🔧 | | | ||
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. | ✅ | 🔧 | | | ||
| [relative-url-style](docs/rules/relative-url-style.md) | Enforce consistent relative URL style. | ✅ | 🔧 | 💡 | | ||
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. | ✅ | 🔧 | | | ||
| [require-number-to-fixed-digits-argument](docs/rules/require-number-to-fixed-digits-argument.md) | Enforce using the digits argument with `Number#toFixed()`. | ✅ | 🔧 | | | ||
| [require-post-message-target-origin](docs/rules/require-post-message-target-origin.md) | Enforce using the `targetOrigin` argument with `window.postMessage()`. | | | 💡 | | ||
| [string-content](docs/rules/string-content.md) | Enforce better string content. | | 🔧 | 💡 | | ||
| [switch-case-braces](docs/rules/switch-case-braces.md) | Enforce consistent brace style for `case` clauses. | ✅ | 🔧 | | | ||
| [template-indent](docs/rules/template-indent.md) | Fix whitespace-insensitive template indentation. | ✅ | 🔧 | | | ||
| [text-encoding-identifier-case](docs/rules/text-encoding-identifier-case.md) | Enforce consistent case for text encoding identifiers. | ✅ | 🔧 | 💡 | | ||
| [throw-new-error](docs/rules/throw-new-error.md) | Require `new` when throwing an error. | ✅ | 🔧 | | | ||
@@ -172,0 +171,0 @@ <!-- end auto-generated rules list --> |
@@ -11,2 +11,7 @@ 'use strict'; | ||
} = require('./literal.js'); | ||
const { | ||
isNewExpression, | ||
isCallExpression, | ||
isCallOrNewExpression, | ||
} = require('./call-or-new-expression.js'); | ||
@@ -22,6 +27,15 @@ module.exports = { | ||
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'), | ||
isNewExpression: require('./is-new-expression.js'), | ||
functionTypes: require('./function-types.js'), | ||
}; |
'use strict'; | ||
const {isStringLiteral} = require('./literal.js'); | ||
const {isCallExpression} = require('./call-or-new-expression.js'); | ||
const isStaticRequire = node => Boolean( | ||
node?.type === 'CallExpression' | ||
&& node.callee.type === 'Identifier' | ||
&& node.callee.name === 'require' | ||
&& !node.optional | ||
&& node.arguments.length === 1 | ||
&& isStringLiteral(node.arguments[0]), | ||
); | ||
const isStaticRequire = node => | ||
isCallExpression(node, { | ||
name: 'require', | ||
argumentsLength: 1, | ||
optional: false, | ||
}) | ||
&& isStringLiteral(node.arguments[0]); | ||
module.exports = isStaticRequire; |
@@ -5,4 +5,3 @@ 'use strict'; | ||
const escapeString = require('./utils/escape-string.js'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {isStringLiteral} = require('./ast/index.js'); | ||
const {isStringLiteral, isNewExpression, isRegexLiteral} = require('./ast/index.js'); | ||
@@ -16,4 +15,2 @@ const MESSAGE_ID = 'better-regex'; | ||
const newRegExp = newExpressionSelector({name: 'RegExp', minimumArguments: 1}); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
@@ -30,3 +27,7 @@ const create = context => { | ||
return { | ||
'Literal[regex]'(node) { | ||
Literal(node) { | ||
if (!isRegexLiteral(node)) { | ||
return; | ||
} | ||
const {raw: original, regex} = node; | ||
@@ -86,3 +87,7 @@ | ||
}, | ||
[newRegExp](node) { | ||
NewExpression(node) { | ||
if (!isNewExpression(node, {name: 'RegExp', minimumArguments: 1})) { | ||
return; | ||
} | ||
const [patternNode, flagsNode] = node.arguments; | ||
@@ -89,0 +94,0 @@ |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const {renameVariable} = require('./fix/index.js'); | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -13,24 +13,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; | ||
@@ -55,3 +55,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; | ||
@@ -66,3 +73,3 @@ | ||
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); | ||
@@ -99,4 +99,4 @@ // Property is destructured outside the current scope | ||
const expression = source.getText(node); | ||
const member = source.getText(node.property); | ||
const expression = sourceCode.getText(node); | ||
const member = sourceCode.getText(node.property); | ||
@@ -103,0 +103,0 @@ // Member might already be destructured |
'use strict'; | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils'); | ||
const getReferences = require('./utils/get-references.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const { | ||
getReferences, | ||
isNodeMatches, | ||
} = require('./utils/index.js'); | ||
const { | ||
functionTypes, | ||
} = require('./ast/index.js'); | ||
@@ -152,3 +157,3 @@ const MESSAGE_ID = 'consistent-function-scoping'; | ||
const {checkArrowFunctions} = {checkArrowFunctions: true, ...context.options[0]}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager} = sourceCode; | ||
@@ -158,37 +163,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), | ||
}, | ||
}; | ||
}); | ||
}; | ||
@@ -195,0 +200,0 @@ |
@@ -186,7 +186,19 @@ 'use strict'; | ||
/** @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); | ||
} | ||
}); | ||
}; | ||
@@ -193,0 +205,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {isOpeningBraceToken} = require('@eslint-community/eslint-utils'); | ||
const {matches} = require('./selectors/index.js'); | ||
@@ -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-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 = { |
@@ -50,3 +50,6 @@ 'use strict'; | ||
const packageResult = readPkgUp.sync(); | ||
// 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 is invalid and would make this plugin throw an error. See also #1871 | ||
const packageResult = readPkgUp.sync({normalize: false}); | ||
const hasPackage = Boolean(packageResult); | ||
@@ -264,3 +267,3 @@ const packageJson = hasPackage ? packageResult.packageJson : {}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const comments = sourceCode.getAllComments(); | ||
@@ -288,9 +291,5 @@ const unusedComments = comments | ||
...context, | ||
getSourceCode() { | ||
return { | ||
...sourceCode, | ||
getAllComments() { | ||
return options.allowWarningComments ? [] : unusedComments; | ||
}, | ||
}; | ||
sourceCode: { | ||
...sourceCode, | ||
getAllComments: () => options.allowWarningComments ? [] : unusedComments, | ||
}, | ||
@@ -297,0 +296,0 @@ }; |
@@ -6,5 +6,4 @@ 'use strict'; | ||
const {isBooleanNode, getBooleanAncestor} = require('./utils/boolean.js'); | ||
const {memberExpressionSelector} = require('./selectors/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const {isLiteral, isMemberExpression, isNumberLiteral} = require('./ast/index.js'); | ||
@@ -49,4 +48,2 @@ const TYPE_NON_ZERO = 'non-zero'; | ||
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; | ||
@@ -149,8 +155,15 @@ function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) { | ||
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)) { | ||
@@ -175,3 +188,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; | ||
@@ -178,0 +197,0 @@ node = lengthNode; |
@@ -147,3 +147,3 @@ 'use strict'; | ||
const chosenCasesFunctions = chosenCases.map(case_ => ignoreNumbers(cases[case_].fn)); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
@@ -150,0 +150,0 @@ if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') { |
'use strict'; | ||
const {defaultsDeep} = require('lodash'); | ||
const {getStringIfConstant} = require('@eslint-community/eslint-utils'); | ||
const {callExpressionSelector} = require('./selectors/index.js'); | ||
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 @@ |
@@ -52,3 +52,3 @@ 'use strict'; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const newExpressionTracker = new GlobalReferenceTracker({ | ||
@@ -66,4 +66,4 @@ objects: builtins.disallowNew, | ||
return { | ||
* 'Program:exit'() { | ||
const scope = context.getScope(); | ||
* 'Program:exit'(program) { | ||
const scope = sourceCode.getScope(program); | ||
@@ -70,0 +70,0 @@ yield * newExpressionTracker.track(scope); |
'use strict'; | ||
const {isParenthesized} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isNodeMatches, isNodeValueNotFunction} = require('./utils/index.js'); | ||
@@ -17,117 +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', | ||
], | ||
}, | ||
], | ||
[ | ||
'findLast', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'findIndex', | ||
{ | ||
ignore: [ | ||
'Boolean', | ||
], | ||
}, | ||
], | ||
[ | ||
'findLastIndex', | ||
{ | ||
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 = [ | ||
@@ -153,6 +160,2 @@ // http://bluebirdjs.com/docs/api/promise.map.html | ||
if (type === 'Identifier' && options.ignore.includes(name)) { | ||
return; | ||
} | ||
const problem = { | ||
@@ -179,3 +182,3 @@ node, | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
let nodeText = sourceCode.getText(node); | ||
@@ -201,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} */ | ||
@@ -244,0 +250,0 @@ module.exports = { |
@@ -10,3 +10,2 @@ 'use strict'; | ||
} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector, referenceIdentifierSelector} = require('./selectors/index.js'); | ||
const {extendFixRange} = require('./fix/index.js'); | ||
@@ -21,3 +20,3 @@ const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {fixSpaceAroundKeyword, removeParentheses} = require('./fix/index.js'); | ||
const {isArrowFunctionBody} = require('./ast/index.js'); | ||
const {isArrowFunctionBody, isMethodCall, isReferenceIdentifier, functionTypes} = require('./ast/index.js'); | ||
@@ -31,8 +30,2 @@ const MESSAGE_ID_ERROR = 'no-array-for-each/error'; | ||
const forEachMethodCallSelector = methodCallSelector({ | ||
method: 'forEach', | ||
includeOptionalCall: true, | ||
includeOptionalMember: true, | ||
}); | ||
const continueAbleNodeTypes = new Set([ | ||
@@ -86,3 +79,3 @@ 'WhileStatement', | ||
function getFixFunction(callExpression, functionInfo, context) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const [callback] = callExpression.arguments; | ||
@@ -101,3 +94,3 @@ const parameters = callback.params; | ||
let text = 'for ('; | ||
text += isFunctionParameterVariableReassigned(callback, context) ? 'let' : 'const'; | ||
text += isFunctionParameterVariableReassigned(callback, sourceCode) ? 'let' : 'const'; | ||
text += ' '; | ||
@@ -284,4 +277,4 @@ text += shouldUseEntries ? `[${indexText}, ${elementText}]` : elementText; | ||
function isFunctionParametersSafeToFix(callbackFunction, {context, scope, callExpression, allIdentifiers}) { | ||
const variables = context.getDeclaredVariables(callbackFunction); | ||
function isFunctionParametersSafeToFix(callbackFunction, {sourceCode, scope, callExpression, allIdentifiers}) { | ||
const variables = sourceCode.getDeclaredVariables(callbackFunction); | ||
@@ -320,4 +313,4 @@ for (const variable of variables) { | ||
function isFunctionParameterVariableReassigned(callbackFunction, context) { | ||
return context.getDeclaredVariables(callbackFunction) | ||
function isFunctionParameterVariableReassigned(callbackFunction, sourceCode) { | ||
return sourceCode.getDeclaredVariables(callbackFunction) | ||
.filter(variable => variable.defs[0].type === 'Parameter') | ||
@@ -329,3 +322,3 @@ .some(variable => | ||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, context}) { | ||
function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceCode}) { | ||
// Check `CallExpression` | ||
@@ -365,3 +358,3 @@ if (callExpression.optional || callExpression.arguments.length !== 1) { | ||
|| parameters.some(({type, typeAnnotation}) => type === 'RestElement' || typeAnnotation) | ||
|| !isFunctionParametersSafeToFix(callback, {scope, callExpression, allIdentifiers, context}) | ||
|| !isFunctionParametersSafeToFix(callback, {scope, callExpression, allIdentifiers, sourceCode}) | ||
) { | ||
@@ -398,65 +391,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); | ||
}, | ||
[forEachMethodCallSelector](node) { | ||
if (isNodeMatches(node.callee.object, ignoredObjects)) { | ||
return; | ||
} | ||
} | ||
}); | ||
callExpressions.push({ | ||
node, | ||
scope: context.getScope(), | ||
}); | ||
}, | ||
* 'Program:exit'() { | ||
for (const {node, scope} of callExpressions) { | ||
const iterable = node.callee; | ||
context.on('ReturnStatement', node => { | ||
const currentFunction = functionStack[functionStack.length - 1]; | ||
if (!currentFunction) { | ||
return; | ||
} | ||
const problem = { | ||
node: iterable.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 = iterable.optional && hasSideEffect(iterable, 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; | ||
} | ||
}); | ||
}; | ||
@@ -463,0 +470,0 @@ |
'use strict'; | ||
const {hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js'); | ||
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'); | ||
@@ -73,21 +74,2 @@ const ERROR = 'error'; | ||
const selector = [ | ||
methodCallSelector({ | ||
methods: [ | ||
'every', | ||
'filter', | ||
'find', | ||
'findLast', | ||
'findIndex', | ||
'findLastIndex', | ||
'flatMap', | ||
'forEach', | ||
'map', | ||
'some', | ||
], | ||
argumentsLength: 2, | ||
}), | ||
notFunctionSelector('arguments.0'), | ||
].join(''); | ||
function removeThisArgument(callExpression, sourceCode) { | ||
@@ -124,11 +106,31 @@ return fixer => removeArgument(fixer, callExpression.arguments[1], sourceCode); | ||
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; | ||
@@ -135,0 +137,0 @@ const [callback, thisArgument] = callExpression.arguments; |
'use strict'; | ||
const {hasSideEffect, isCommaToken, isSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
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 {methodCallSelector} = require('./selectors/index.js'); | ||
const {arrayPrototypeMethodSelector, notFunctionSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
const {isNodeValueNotFunction, isArrayPrototypeProperty} = require('./utils/index.js'); | ||
@@ -10,16 +10,14 @@ const MESSAGE_ID = 'no-reduce'; | ||
const prototypeSelector = method => [ | ||
methodCallSelector(method), | ||
arrayPrototypeMethodSelector({ | ||
path: 'callee.object', | ||
methods: ['reduce', 'reduceRight'], | ||
}), | ||
].join(''); | ||
const cases = [ | ||
// `array.{reduce,reduceRight}()` | ||
{ | ||
selector: [ | ||
methodCallSelector({methods: ['reduce', 'reduceRight'], minimumArguments: 1, maximumArguments: 2}), | ||
notFunctionSelector('arguments.0'), | ||
].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, | ||
@@ -49,6 +47,15 @@ isSimpleOperation(callExpression) { | ||
{ | ||
selector: [ | ||
prototypeSelector('call'), | ||
notFunctionSelector('arguments.1'), | ||
].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, | ||
@@ -58,3 +65,11 @@ }, | ||
{ | ||
selector: prototypeSelector('apply'), | ||
test: callExpression => | ||
isMethodCall(callExpression, { | ||
method: 'apply', | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayPrototypeProperty(callExpression.callee.object, { | ||
properties: ['reduce', 'reduceRight'], | ||
}), | ||
getMethodNode: callExpression => callExpression.callee.object.property, | ||
@@ -80,20 +95,23 @@ }, | ||
const {allowSimpleOperations} = {allowSimpleOperations: true, ...context.options[0]}; | ||
const listeners = {}; | ||
for (const {selector, getMethodNode, isSimpleOperation} of cases) { | ||
listeners[selector] = callExpression => { | ||
if (allowSimpleOperations && isSimpleOperation?.(callExpression)) { | ||
return; | ||
} | ||
return { | ||
* CallExpression(callExpression) { | ||
for (const {test, getMethodNode, isSimpleOperation} of cases) { | ||
if (!test(callExpression)) { | ||
continue; | ||
} | ||
const methodNode = getMethodNode(callExpression); | ||
return { | ||
node: methodNode, | ||
messageId: MESSAGE_ID, | ||
data: {method: methodNode.name}, | ||
}; | ||
}; | ||
} | ||
if (allowSimpleOperations && isSimpleOperation?.(callExpression)) { | ||
continue; | ||
} | ||
return listeners; | ||
const methodNode = getMethodNode(callExpression); | ||
yield { | ||
node: methodNode, | ||
messageId: MESSAGE_ID, | ||
data: {method: methodNode.name}, | ||
}; | ||
} | ||
}, | ||
}; | ||
}; | ||
@@ -100,0 +118,0 @@ |
@@ -15,6 +15,10 @@ 'use strict'; | ||
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; | ||
@@ -21,0 +25,0 @@ const problem = { |
'use strict'; | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const toLocation = require('./utils/to-location.js'); | ||
const {isStringLiteral} = require('./ast/index.js'); | ||
const {isStringLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -11,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 | ||
@@ -34,3 +19,3 @@ const hasLeadingSpace = value => value.length > 1 && value.charAt(0) === ' ' && value.charAt(1) !== ' '; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const getProblem = (node, method, position) => { | ||
@@ -51,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; | ||
@@ -54,0 +57,0 @@ const {arguments: messages} = node; |
@@ -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,5 +21,5 @@ | ||
const create = context => { | ||
const filename = context.getPhysicalFilename().toLowerCase(); | ||
const filename = context.physicalFilename; | ||
if (!/\.(?:js|mjs|cjs|ts|mts|cts)$/.test(filename)) { | ||
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 @@ |
@@ -265,3 +265,3 @@ 'use strict'; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {scopeManager, text: sourceCodeText} = sourceCode; | ||
@@ -284,3 +284,3 @@ | ||
const scope = context.getScope(); | ||
const scope = sourceCode.getScope(node); | ||
const staticResult = getStaticValue(arrayIdentifier, scope); | ||
@@ -287,0 +287,0 @@ if (staticResult && !Array.isArray(staticResult.value)) { |
'use strict'; | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
const {isStringLiteral, isRegexLiteral} = require('./ast/index.js'); | ||
@@ -27,3 +28,3 @@ const MESSAGE_ID = 'no-hex-escape'; | ||
Literal(node) { | ||
if (node.regex || typeof node.value === 'string') { | ||
if (isStringLiteral(node) || isRegexLiteral(node)) { | ||
return checkEscape(context, node, node.raw); | ||
@@ -30,0 +31,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-community/eslint-utils'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url.js'); | ||
const {methodCallSelector, matches} = require('./selectors/index.js'); | ||
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, |
'use strict'; | ||
const {isParenthesized, isNotSemicolonToken} = require('@eslint-community/eslint-utils'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
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} */ |
@@ -6,3 +6,2 @@ /* | ||
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
const { | ||
@@ -25,14 +24,2 @@ removeParentheses, | ||
const selector = [ | ||
matches([ | ||
'IfStatement[alternate][alternate.type!="IfStatement"]', | ||
'ConditionalExpression', | ||
]), | ||
matches([ | ||
'[test.type="UnaryExpression"][test.operator="!"]', | ||
'[test.type="BinaryExpression"][test.operator="!="]', | ||
'[test.type="BinaryExpression"][test.operator="!=="]', | ||
]), | ||
].join(''); | ||
function * convertNegatedCondition(fixer, node, sourceCode) { | ||
@@ -87,10 +74,29 @@ const {test} = node; | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
const create = context => { | ||
context.on(['IfStatement', 'ConditionalExpression'], node => { | ||
if ( | ||
node.type === 'IfStatement' | ||
&& ( | ||
!node.alternate | ||
|| node.alternate.type === 'IfStatement' | ||
) | ||
) { | ||
return; | ||
} | ||
const {test} = node; | ||
if (!( | ||
(test.type === 'UnaryExpression' && test.operator === '!') | ||
|| (test.type === 'BinaryExpression' && (test.operator === '!=' || test.operator === '!==')) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.test, | ||
node: test, | ||
messageId: MESSAGE_ID, | ||
/** @param {import('eslint').Rule.RuleFixer} fixer */ | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
yield * convertNegatedCondition(fixer, node, sourceCode); | ||
@@ -101,3 +107,3 @@ yield * swapConsequentAndAlternate(fixer, node, sourceCode); | ||
node.type !== 'ConditionalExpression' | ||
|| node.test.type !== 'UnaryExpression' | ||
|| test.type !== 'UnaryExpression' | ||
) { | ||
@@ -109,3 +115,3 @@ return; | ||
const {test, parent} = node; | ||
const {parent} = node; | ||
const [firstToken, secondToken] = sourceCode.getFirstTokens(test, 2); | ||
@@ -129,4 +135,4 @@ if ( | ||
}; | ||
}, | ||
}); | ||
}); | ||
}; | ||
@@ -133,0 +139,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
@@ -11,27 +11,38 @@ 'use strict'; | ||
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-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-community/eslint-utils'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {switchNewExpressionToCallExpression} = require('./fix/index.js'); | ||
const isNumber = require('./utils/is-number.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
@@ -55,7 +55,11 @@ const ERROR = 'error'; | ||
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) { | ||
@@ -62,0 +66,0 @@ return { |
'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 @@ |
@@ -12,9 +12,2 @@ 'use strict'; | ||
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 === '='; | ||
@@ -55,4 +48,2 @@ const isDeclarationOfExportDefaultDeclaration = node => | ||
|| (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-community/eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -15,3 +15,3 @@ const MESSAGE_ID_OBJECT = 'no-thenable-object'; | ||
const isStringThen = (node, context) => | ||
getStaticValue(node, context.getScope())?.value === 'then'; | ||
getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then'; | ||
@@ -24,4 +24,13 @@ const cases = [ | ||
{ | ||
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, | ||
@@ -34,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, | ||
@@ -42,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, | ||
@@ -50,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, | ||
@@ -79,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, | ||
@@ -86,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, | ||
@@ -92,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 : []), | ||
}, | ||
@@ -100,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} */ | ||
@@ -123,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 isShadowed = require('./utils/is-shadowed.js'); | ||
const {matches} = require('./selectors/index.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const { | ||
addParenthesizesToReturnOrThrowExpression, | ||
removeSpacesAfter, | ||
} = require('./fix/index.js'); | ||
const {removeSpacesAfter} = require('./fix/index.js'); | ||
const isOnSameLine = require('./utils/is-on-same-line.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const { | ||
needsSemicolon, | ||
isParenthesized, | ||
} = require('./utils/parentheses.js'); | ||
isOnSameLine, | ||
isShadowed, | ||
} = require('./utils/index.js'); | ||
@@ -21,11 +21,2 @@ const MESSAGE_ID_ERROR = 'no-typeof-undefined/error'; | ||
const selector = [ | ||
'BinaryExpression', | ||
matches(['===', '!==', '==', '!='].map(operator => `[operator="${operator}"]`)), | ||
'[left.type="UnaryExpression"]', | ||
'[left.operator="typeof"]', | ||
'[left.prefix]', | ||
'[right.type="Literal"]', | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
@@ -39,13 +30,26 @@ const create = context => { | ||
}; | ||
const {sourceCode} = context; | ||
return { | ||
[selector](binaryExpression) { | ||
const {left: typeofNode, right: undefinedString, operator} = binaryExpression; | ||
if (undefinedString.value !== 'undefined') { | ||
BinaryExpression(binaryExpression) { | ||
if (!( | ||
( | ||
binaryExpression.operator === '===' | ||
|| binaryExpression.operator === '!==' | ||
|| binaryExpression.operator === '==' | ||
|| binaryExpression.operator === '!=' | ||
) | ||
&& binaryExpression.left.type === 'UnaryExpression' | ||
&& binaryExpression.left.operator === 'typeof' | ||
&& binaryExpression.left.prefix | ||
&& isLiteral(binaryExpression.right, 'undefined') | ||
)) { | ||
return; | ||
} | ||
const {left: typeofNode, right: undefinedString, operator} = binaryExpression; | ||
const valueNode = typeofNode.argument; | ||
const isGlobalVariable = valueNode.type === 'Identifier' | ||
&& !isShadowed(context.getScope(), valueNode); | ||
&& !isShadowed(sourceCode.getScope(valueNode), valueNode); | ||
@@ -56,3 +60,2 @@ if (!checkGlobalVariables && isGlobalVariable) { | ||
const sourceCode = context.getSourceCode(); | ||
const [typeofToken, secondToken] = sourceCode.getFirstTokens(typeofNode, 2); | ||
@@ -59,0 +62,0 @@ |
@@ -49,3 +49,3 @@ 'use strict'; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const awaitToken = sourceCode.getFirstToken(node); | ||
@@ -52,0 +52,0 @@ const problem = { |
@@ -16,9 +16,11 @@ 'use strict'; | ||
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 @@ }; |
@@ -83,2 +83,3 @@ 'use strict'; | ||
const create = context => { | ||
const {sourceCode} = context; | ||
const getPropertyDisplayName = property => { | ||
@@ -93,3 +94,3 @@ if (property.key.type === 'Identifier') { | ||
return context.getSourceCode().getText(property.key); | ||
return sourceCode.getText(property.key); | ||
}; | ||
@@ -216,4 +217,4 @@ | ||
return { | ||
'Program:exit'() { | ||
const scopes = getScopes(context.getScope()); | ||
'Program:exit'(program) { | ||
const scopes = getScopes(sourceCode.getScope(program)); | ||
for (const scope of scopes) { | ||
@@ -220,0 +221,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; | ||
@@ -178,6 +161,31 @@ break; | ||
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); | ||
@@ -184,0 +192,0 @@ if (!functionNode || !(functionNode.async || isPromiseCallback(functionNode))) { |
'use strict'; | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const { | ||
matches, | ||
newExpressionSelector, | ||
methodCallSelector, | ||
} = require('./selectors/index.js'); | ||
const typedArray = require('./shared/typed-array.js'); | ||
@@ -14,7 +9,7 @@ const { | ||
} = require('./fix/index.js'); | ||
const isOnSameLine = require('./utils/is-on-same-line.js'); | ||
const { | ||
isParenthesized, | ||
} = require('./utils/parentheses.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
isOnSameLine, | ||
} = require('./utils/index.js'); | ||
const {isNewExpression, isMethodCall, isCallOrNewExpression} = require('./ast/index.js'); | ||
@@ -34,73 +29,7 @@ const SPREAD_IN_LIST = 'spread-in-list'; | ||
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 singleArraySpreadSelector = [ | ||
'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}), | ||
]), | ||
' > ', | ||
`${singleArraySpreadSelector}.arguments:first-child`, | ||
].join(''), | ||
`ForOfStatement > ${singleArraySpreadSelector}.right`, | ||
`YieldExpression[delegate=true] > ${singleArraySpreadSelector}.argument`, | ||
]); | ||
const uselessArrayCloneSelector = [ | ||
`${singleArraySpreadSelector} > .elements:first-child > .argument`, | ||
matches([ | ||
// Array methods returns a new array | ||
methodCallSelector([ | ||
'concat', | ||
'copyWithin', | ||
'filter', | ||
'flat', | ||
'flatMap', | ||
'map', | ||
'slice', | ||
'splice', | ||
]), | ||
// `String#split()` | ||
methodCallSelector('split'), | ||
// `Object.keys()` and `Object.values()` | ||
methodCallSelector({object: 'Object', methods: ['keys', 'values'], argumentsLength: 1}), | ||
// `await Promise.all()` and `await Promise.allSettled` | ||
[ | ||
'AwaitExpression', | ||
methodCallSelector({ | ||
object: 'Promise', | ||
methods: ['all', 'allSettled'], | ||
argumentsLength: 1, | ||
path: 'argument', | ||
}), | ||
].join(''), | ||
// `Array.from()`, `Array.of()` | ||
methodCallSelector({object: 'Array', methods: ['from', 'of']}), | ||
// `new Array()` | ||
newExpressionSelector('Array'), | ||
]), | ||
].join(''); | ||
const parentDescriptions = { | ||
@@ -153,7 +82,7 @@ ArrayExpression: 'array literal', | ||
// `[...value]` | ||
// ^ | ||
// ^ | ||
yield fixer.remove(closingBracketToken); | ||
// `[...value,]` | ||
// ^ | ||
// ^ | ||
if (isCommaToken(commaToken)) { | ||
@@ -188,124 +117,256 @@ yield fixer.remove(commaToken); | ||
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); | ||
} | ||
const commaTokens = getCommaTokens(spreadObject, sourceCode); | ||
for (const [index, commaToken] of commaTokens.entries()) { | ||
if (spreadObject.elements[index]) { | ||
continue; | ||
} | ||
if (parentType !== 'CallExpression' && parentType !== 'NewExpression') { | ||
return; | ||
} | ||
// `call(...[foo, , bar])` | ||
// ^ Replace holes with `undefined` | ||
yield fixer.insertTextBefore(commaToken, 'undefined'); | ||
const commaTokens = getCommaTokens(spreadObject, sourceCode); | ||
for (const [index, commaToken] of commaTokens.entries()) { | ||
if (spreadObject.elements[index]) { | ||
continue; | ||
} | ||
}, | ||
}; | ||
}, | ||
[uselessIterableToArraySelector](arrayExpression) { | ||
const {parent} = arrayExpression; | ||
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; | ||
// `call(...[foo, , bar])` | ||
// ^ Replace holes with `undefined` | ||
yield fixer.insertTextBefore(commaToken, 'undefined'); | ||
} | ||
}, | ||
}; | ||
}); | ||
case 'NewExpression': { | ||
parentDescription = `new ${parent.callee.name}(…)`; | ||
break; | ||
} | ||
// Useless iterable to array | ||
context.on('ArrayExpression', arrayExpression => { | ||
if (!isSingleArraySpread(arrayExpression)) { | ||
return; | ||
} | ||
case 'CallExpression': { | ||
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`; | ||
break; | ||
} | ||
// No default | ||
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: arrayExpression, | ||
messageId, | ||
data: {parentDescription}, | ||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode), | ||
}; | ||
}, | ||
[uselessArrayCloneSelector](node) { | ||
const arrayExpression = node.parent.parent; | ||
const problem = { | ||
node: arrayExpression, | ||
messageId: CLONE_ARRAY, | ||
}; | ||
case 'YieldExpression': { | ||
messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR; | ||
break; | ||
} | ||
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; | ||
case 'NewExpression': { | ||
parentDescription = `new ${parent.callee.name}(…)`; | ||
break; | ||
} | ||
return Object.assign(problem, { | ||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode), | ||
}); | ||
}, | ||
}; | ||
case 'CallExpression': { | ||
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`; | ||
break; | ||
} | ||
// No default | ||
} | ||
return { | ||
node: arrayExpression, | ||
messageId, | ||
data: {parentDescription}, | ||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode), | ||
}; | ||
}); | ||
// Useless array clone | ||
context.on('ArrayExpression', arrayExpression => { | ||
if (!isSingleArraySpread(arrayExpression)) { | ||
return; | ||
} | ||
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; | ||
} | ||
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), | ||
}); | ||
}); | ||
}; | ||
@@ -312,0 +373,0 @@ |
@@ -16,3 +16,3 @@ 'use strict'; | ||
const create = context => ({ | ||
* 'SwitchStatement[cases.length>1]'(switchStatement) { | ||
* SwitchStatement(switchStatement) { | ||
const {cases} = switchStatement; | ||
@@ -47,3 +47,3 @@ | ||
node, | ||
loc: getSwitchCaseHeadLocation(node, context.getSourceCode()), | ||
loc: getSwitchCaseHeadLocation(node, context.sourceCode), | ||
messageId: MESSAGE_ID_ERROR, | ||
@@ -50,0 +50,0 @@ suggest: [ |
'use strict'; | ||
const {isCommaToken} = require('@eslint-community/eslint-utils'); | ||
const {replaceNodeOrTokenAndSpacesBefore} = require('./fix/index.js'); | ||
const {isUndefined} = require('./ast/index.js'); | ||
const {isUndefined, isFunction} = require('./ast/index.js'); | ||
@@ -11,28 +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 compareFunctionNames = new Set([ | ||
@@ -110,7 +84,12 @@ 'is', | ||
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()); | ||
const functionNode = getFunction(sourceCode.getScope(node)); | ||
if (functionNode?.returnType) { | ||
@@ -124,7 +103,6 @@ return; | ||
messageId, | ||
fix: fixer => fix(node, fixer), | ||
fix, | ||
}; | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const options = { | ||
@@ -138,79 +116,156 @@ checkArguments: true, | ||
const listeners = { | ||
[returnSelector]: listener( | ||
removeNodeAndLeadingSpace, | ||
/* CheckFunctionReturnType */ true, | ||
), | ||
[yieldSelector]: listener(removeNodeAndLeadingSpace), | ||
[arrowFunctionSelector]: listener( | ||
(node, fixer) => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode), | ||
/* CheckFunctionReturnType */ true, | ||
), | ||
[variableInitSelector]: listener( | ||
(node, fixer) => fixer.removeRange([node.parent.id.range[1], node.range[1]]), | ||
), | ||
[assignmentPatternSelector]: listener( | ||
(node, fixer) => fixer.removeRange([node.parent.left.range[1], node.range[1]]), | ||
), | ||
}; | ||
// `return undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'ReturnStatement' | ||
&& node.parent.argument === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => removeNodeAndLeadingSpace(node, fixer), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
if (options.checkArguments) { | ||
listeners.CallExpression = node => { | ||
if (shouldIgnore(node.callee)) { | ||
return; | ||
} | ||
// `yield undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'YieldExpression' | ||
&& !node.parent.delegate | ||
&& node.parent.argument === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => removeNodeAndLeadingSpace(node, fixer), | ||
); | ||
} | ||
}); | ||
const argumentNodes = node.arguments; | ||
// `() => undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'ArrowFunctionExpression' | ||
&& node.parent.body === node | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
// Ignore arguments in `Function#bind()`, but not `this` argument | ||
if (isFunctionBindCall(node) && argumentNodes.length !== 1) { | ||
return; | ||
} | ||
// `let foo = undefined` / `var foo = undefined` | ||
context.on('Identifier', node => { | ||
if ( | ||
isUndefined(node) | ||
&& node.parent.type === 'VariableDeclarator' | ||
&& node.parent.init === node | ||
&& node.parent.parent.type === 'VariableDeclaration' | ||
&& node.parent.parent.kind !== 'const' | ||
&& node.parent.parent.declarations.includes(node.parent) | ||
) { | ||
return getProblem( | ||
node, | ||
fixer => fixer.removeRange([node.parent.id.range[1], node.range[1]]), | ||
/* CheckFunctionReturnType */ true, | ||
); | ||
} | ||
}); | ||
const undefinedArguments = []; | ||
for (let index = argumentNodes.length - 1; index >= 0; index--) { | ||
const node = argumentNodes[index]; | ||
if (isUndefined(node)) { | ||
undefinedArguments.unshift(node); | ||
} else { | ||
break; | ||
} | ||
} | ||
// `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; | ||
if (undefinedArguments.length === 0) { | ||
return; | ||
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 (!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[undefinedArguments.length - 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; | ||
}); | ||
}; | ||
@@ -217,0 +272,0 @@ |
@@ -42,3 +42,3 @@ 'use strict'; | ||
const start = end - (raw.length - formatted.length); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
@@ -45,0 +45,0 @@ loc: toLocation([start, end], sourceCode), |
'use strict'; | ||
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} = require('./ast/index.js'); | ||
const {isUndefined, isNullLiteral, isStaticRequire} = require('./ast/index.js'); | ||
@@ -76,4 +75,8 @@ const MESSAGE_ID = 'prefer-add-event-listener'; | ||
[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; | ||
@@ -83,4 +86,4 @@ } | ||
'ImportDeclaration > Literal'(node) { | ||
if (!isDisabled && excludedPackages.has(node.value)) { | ||
Literal(node) { | ||
if (node.parent.type === 'ImportDeclaration' && !isDisabled && excludedPackages.has(node.value)) { | ||
isDisabled = true; | ||
@@ -142,3 +145,3 @@ } | ||
) { | ||
fix = fixer => fixCode(fixer, context.getSourceCode(), node, memberExpression); | ||
fix = fixer => fixCode(fixer, context.sourceCode, node, memberExpression); | ||
} | ||
@@ -145,0 +148,0 @@ |
'use strict'; | ||
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,2 +9,10 @@ removeMemberExpressionProperty, | ||
} = require('./fix/index.js'); | ||
const { | ||
isLeftHandSide, | ||
singular, | ||
getScopes, | ||
avoidCapture, | ||
getVariableIdentifiers, | ||
} = require('./utils/index.js'); | ||
const {isMethodCall} = require('./ast/index.js'); | ||
@@ -42,93 +41,10 @@ const ERROR_ZERO_INDEX = 'error-zero-index'; | ||
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 popSelector = [ | ||
methodCallSelector({ | ||
method: 'pop', | ||
argumentsLength: 0, | ||
}), | ||
methodCallSelector({ | ||
...filterMethodSelectorOptions, | ||
path: 'callee.object', | ||
}), | ||
].join(''); | ||
const atMinusOneSelector = [ | ||
methodCallSelector({ | ||
method: 'at', | ||
argumentsLength: 1, | ||
}), | ||
'[arguments.0.type="UnaryExpression"]', | ||
'[arguments.0.operator="-"]', | ||
'[arguments.0.prefix]', | ||
'[arguments.0.argument.type="Literal"]', | ||
'[arguments.0.argument.raw=1]', | ||
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` | ||
@@ -257,2 +173,3 @@ // - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)` | ||
&& left.elements.length === 1 | ||
&& left.elements[0] | ||
&& left.elements[0].type !== 'RestElement'; | ||
@@ -263,3 +180,3 @@ }; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const { | ||
@@ -272,118 +189,211 @@ checkFromLast, | ||
const listeners = { | ||
[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 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 [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); | ||
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); | ||
} | ||
// Prevent possible variable conflicts | ||
yield * extendFixRange(fixer, sourceCode.ast.range); | ||
} | ||
for (const node of zeroIndexNodes) { | ||
yield removeMemberExpressionProperty(fixer, node, sourceCode); | ||
} | ||
for (const node of zeroIndexNodes) { | ||
yield removeMemberExpressionProperty(fixer, node, sourceCode); | ||
} | ||
for (const node of destructuringNodes) { | ||
yield * fixDestructuring(node, sourceCode, fixer); | ||
} | ||
}; | ||
} | ||
for (const node of destructuringNodes) { | ||
yield * fixDestructuring(node, sourceCode, fixer); | ||
} | ||
}; | ||
} | ||
return problem; | ||
}); | ||
return problem; | ||
}, | ||
}; | ||
if (!checkFromLast) { | ||
return listeners; | ||
return; | ||
} | ||
return Object.assign(listeners, { | ||
[popSelector](node) { | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_POP, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'findLast'), | ||
...removeMethodCall(fixer, node, sourceCode), | ||
], | ||
}; | ||
}, | ||
[atMinusOneSelector](node) { | ||
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), | ||
], | ||
}; | ||
}, | ||
// `array.filter().pop()` | ||
context.on('CallExpression', node => { | ||
if (!( | ||
isMethodCall(node, { | ||
method: 'pop', | ||
argumentsLength: 0, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}) | ||
&& isArrayFilterCall(node.callee.object) | ||
)) { | ||
return; | ||
} | ||
return { | ||
node: node.callee.object.callee.property, | ||
messageId: ERROR_POP, | ||
fix: fixer => [ | ||
fixer.replaceText(node.callee.object.callee.property, 'findLast'), | ||
...removeMethodCall(fixer, node, sourceCode), | ||
], | ||
}; | ||
}); | ||
// `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 { | ||
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), | ||
], | ||
}; | ||
}); | ||
}; | ||
@@ -390,0 +400,0 @@ |
'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 @@ |
@@ -16,6 +16,6 @@ 'use strict'; | ||
module.exports = { | ||
create: context => ({ | ||
...indexOfOverFindIndexRule.createListeners(context), | ||
...lastIndexOfOverFindLastIndexRule.createListeners(context), | ||
}), | ||
create(context) { | ||
indexOfOverFindIndexRule.listen(context); | ||
lastIndexOfOverFindLastIndexRule.listen(context); | ||
}, | ||
meta: { | ||
@@ -22,0 +22,0 @@ type: 'suggestion', |
'use strict'; | ||
const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {isBooleanNode} = require('./utils/boolean.js'); | ||
const {getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const { | ||
isBooleanNode, | ||
getParenthesizedRange, | ||
isNodeValueNotFunction, | ||
} = require('./utils/index.js'); | ||
const {removeMemberExpressionProperty} = require('./fix/index.js'); | ||
const {isLiteral, isUndefined} = require('./ast/index.js'); | ||
const {isLiteral, isUndefined, isMethodCall, isMemberExpression} = require('./ast/index.js'); | ||
@@ -18,8 +20,2 @@ const ERROR_ID_ARRAY_SOME = 'some'; | ||
const arrayFindOrFindLastCallSelector = methodCallSelector({ | ||
methods: ['find', 'findLast'], | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
}); | ||
const isCheckingUndefined = node => | ||
@@ -49,17 +45,15 @@ node.parent.type === 'BinaryExpression' | ||
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 => ({ | ||
[arrayFindOrFindLastCallSelector](callExpression) { | ||
CallExpression(callExpression) { | ||
if (!isMethodCall(callExpression, { | ||
methods: ['find', 'findLast'], | ||
minimumArguments: 1, | ||
maximumArguments: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const isCompare = isCheckingUndefined(callExpression); | ||
@@ -85,3 +79,3 @@ if (!isCompare && !isBooleanNode(callExpression)) { | ||
const parenthesizedRange = getParenthesizedRange(callExpression, context.getSourceCode()); | ||
const parenthesizedRange = getParenthesizedRange(callExpression, context.sourceCode); | ||
yield fixer.replaceTextRange([parenthesizedRange[1], callExpression.parent.range[1]], ''); | ||
@@ -99,3 +93,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; | ||
@@ -109,4 +124,4 @@ return { | ||
const sourceCode = context.getSourceCode(); | ||
const lengthNode = filterCall.parent; | ||
const {sourceCode} = context; | ||
const lengthNode = binaryExpression.left; | ||
/* | ||
@@ -119,3 +134,2 @@ Remove `.length` | ||
const compareNode = lengthNode.parent; | ||
/* | ||
@@ -128,3 +142,3 @@ Remove `> 0` | ||
getParenthesizedRange(lengthNode, sourceCode)[1], | ||
compareNode.range[1], | ||
binaryExpression.range[1], | ||
]); | ||
@@ -131,0 +145,0 @@ |
@@ -7,7 +7,7 @@ 'use strict'; | ||
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 { | ||
@@ -17,5 +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} = require('./ast/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'; | ||
@@ -152,169 +143,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]` | ||
// 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 ( | ||
indexNode.type === 'BinaryExpression' | ||
&& indexNode.operator === '-' | ||
&& indexNode.left === lengthNode | ||
&& indexNode.right.type === 'Literal' | ||
&& /^\d+$/.test(indexNode.right.raw) | ||
tokenBefore.type === 'Punctuator' | ||
&& tokenBefore.value === '-' | ||
&& /^\s+$/.test(sourceCode.text.slice(tokenBefore.range[1], numberNode.range[0])) | ||
) { | ||
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]]); | ||
} | ||
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; | ||
}); | ||
} | ||
@@ -321,0 +347,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-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) => | ||
@@ -131,3 +122,3 @@ left | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const functionStack = []; | ||
@@ -153,3 +144,3 @@ | ||
const variable = findVariable(context.getScope(), secondId); | ||
const variable = findVariable(sourceCode.getScope(node), secondId); | ||
@@ -195,20 +186,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); | ||
} | ||
}); | ||
}; | ||
@@ -215,0 +207,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 {isIdentifierName} = require('@babel/helper-validator-identifier'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
const {methodCallSelector, matches} = require('./selectors/index.js'); | ||
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.replace(/-[a-z]/g, s => s[1].toUpperCase()); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
[selector](node) { | ||
const [nameNode] = node.arguments; | ||
let attributeName = nameNode.value; | ||
function getFix(callExpression, context) { | ||
const method = callExpression.callee.property.name; | ||
if (typeof attributeName !== 'string') { | ||
return; | ||
} | ||
// `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; | ||
} | ||
attributeName = attributeName.toLowerCase(); | ||
// `element.setAttribute(…)` returns `undefined`, but `AssignmentExpression` returns value of RHS | ||
if (method === 'setAttribute' && !isValueNotUsable(callExpression)) { | ||
return; | ||
} | ||
if (!attributeName.startsWith('data-')) { | ||
return; | ||
} | ||
if (method === 'removeAttribute' && !isExpressionStatement(callExpression.parent)) { | ||
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) { | ||
@@ -51,3 +49,3 @@ case 'setAttribute': | ||
if (method === 'setAttribute') { | ||
text += ` = ${sourceCode.getText(node.arguments[1])}`; | ||
text += ` = ${sourceCode.getText(callExpression.arguments[1])}`; | ||
} else if (method === 'removeAttribute') { | ||
@@ -71,7 +69,40 @@ text = `delete ${text}`; | ||
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), | ||
}; | ||
@@ -78,0 +109,0 @@ }, |
'use strict'; | ||
const {isParenthesized, hasSideEffect} = require('@eslint-community/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 {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, |
'use strict'; | ||
const {matches} = require('./selectors/index.js'); | ||
@@ -9,15 +8,18 @@ const MESSAGE_ID = 'prefer-event-target'; | ||
const selector = [ | ||
'Identifier', | ||
'[name="EventEmitter"]', | ||
matches([ | ||
'ClassDeclaration > .superClass', | ||
'ClassExpression > .superClass', | ||
'NewExpression > .callee', | ||
]), | ||
].join(''); | ||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = () => ({ | ||
[selector](node) { | ||
Identifier(node) { | ||
if (!( | ||
node.name === 'EventEmitter' | ||
&& ( | ||
( | ||
(node.parent.type === 'ClassDeclaration' || node.parent.type === 'ClassExpression') | ||
&& node.parent.superClass === node | ||
) | ||
|| (node.parent.type === 'NewExpression' && node.parent.callee === node) | ||
) | ||
)) { | ||
return; | ||
} | ||
return { | ||
@@ -24,0 +26,0 @@ node, |
@@ -7,2 +7,5 @@ 'use strict'; | ||
} = require('@eslint-community/eslint-utils'); | ||
const { | ||
isStringLiteral, | ||
} = require('./ast/index.js'); | ||
@@ -174,3 +177,3 @@ const MESSAGE_ID_ERROR = 'error'; | ||
function getExported(identifier, context, sourceCode) { | ||
function getExported(identifier, sourceCode) { | ||
const {parent} = identifier; | ||
@@ -206,3 +209,3 @@ switch (parent.type) { | ||
&& parent.parent.parent.type === 'ExportNamedDeclaration' | ||
&& isVariableUnused(parent, context) | ||
&& isVariableUnused(parent, sourceCode) | ||
) { | ||
@@ -223,4 +226,4 @@ return { | ||
function isVariableUnused(node, context) { | ||
const variables = context.getDeclaredVariables(node); | ||
function isVariableUnused(node, sourceCode) { | ||
const variables = sourceCode.getDeclaredVariables(node); | ||
@@ -277,6 +280,6 @@ /* c8 ignore next 3 */ | ||
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 +323,3 @@ if (!exported) { | ||
function create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {ignoreUsedVariables} = {ignoreUsedVariables: false, ...context.options[0]}; | ||
@@ -327,12 +330,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 +352,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 +355,0 @@ return { |
@@ -18,3 +18,3 @@ 'use strict'; | ||
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-community/eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
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 => { | ||
@@ -111,4 +102,16 @@ while (node.type === 'AwaitExpression') { | ||
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); | ||
@@ -142,3 +145,3 @@ if ( | ||
messageId: MESSAGE_ID, | ||
fix: fixer => removeArgument(fixer, charsetNode, context.getSourceCode()), | ||
fix: fixer => removeArgument(fixer, charsetNode, sourceCode), | ||
}; | ||
@@ -145,0 +148,0 @@ }, |
@@ -28,3 +28,3 @@ 'use strict'; | ||
case 'FunctionExpression': { | ||
const eventVariable = context.getDeclaredVariables(callback)[0]; | ||
const eventVariable = context.sourceCode.getDeclaredVariables(callback)[0]; | ||
const references = eventVariable?.references; | ||
@@ -114,3 +114,11 @@ return { | ||
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` | ||
@@ -117,0 +125,0 @@ const {event, references} = getEventNodeAndReferences(context, node); |
@@ -113,3 +113,3 @@ 'use strict'; | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -116,0 +116,0 @@ return { |
'use strict'; | ||
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,58 +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, | ||
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); | ||
}, | ||
}; | ||
} | ||
}); | ||
}; | ||
@@ -101,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); |
@@ -5,4 +5,3 @@ 'use strict'; | ||
const assertToken = require('./utils/assert-token.js'); | ||
const {referenceIdentifierSelector} = require('./selectors/index.js'); | ||
const {isStaticRequire} = require('./ast/index.js'); | ||
const {isStaticRequire, isReferenceIdentifier, isFunction} = require('./ast/index.js'); | ||
const { | ||
@@ -33,10 +32,2 @@ removeParentheses, | ||
const identifierSelector = referenceIdentifierSelector([ | ||
'exports', | ||
'require', | ||
'module', | ||
'__filename', | ||
'__dirname', | ||
]); | ||
function fixRequireCall(node, sourceCode) { | ||
@@ -174,3 +165,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) { | ||
@@ -221,3 +221,3 @@ return function * (fixer) { | ||
function create(context) { | ||
const filename = context.getFilename().toLowerCase(); | ||
const filename = context.filename.toLowerCase(); | ||
@@ -228,21 +228,26 @@ if (filename.endsWith('.cjs')) { | ||
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 { | ||
@@ -252,26 +257,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, | ||
}]; | ||
@@ -281,47 +307,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; | ||
}); | ||
} | ||
@@ -328,0 +343,0 @@ |
'use strict'; | ||
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils'); | ||
const {not} = require('./selectors/index.js'); | ||
const {functionTypes} = require('./ast/index.js'); | ||
@@ -72,15 +72,2 @@ const MESSAGE_ID = 'prefer-native-coercion-functions'; | ||
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) { | ||
@@ -133,4 +120,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); | ||
@@ -142,3 +148,3 @@ | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const {replacementFunction, fix} = problem; | ||
@@ -168,4 +174,4 @@ | ||
return problem; | ||
}, | ||
}); | ||
}); | ||
}; | ||
@@ -172,0 +178,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
@@ -40,2 +40,11 @@ 'use strict'; | ||
[ | ||
'toSpliced', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
]), | ||
}, | ||
], | ||
[ | ||
'at', | ||
@@ -51,2 +60,12 @@ { | ||
], | ||
[ | ||
'with', | ||
{ | ||
argumentsIndexes: [0], | ||
supportObjects: new Set([ | ||
'Array', | ||
...typedArray, | ||
]), | ||
}, | ||
], | ||
]); | ||
@@ -97,3 +116,3 @@ | ||
if ( | ||
// [].{slice,splice} | ||
// `[].{slice,splice,toSpliced,at,with}` | ||
( | ||
@@ -103,3 +122,3 @@ parentCallee.type === 'ArrayExpression' | ||
) | ||
// ''.slice | ||
// `''.slice` | ||
|| ( | ||
@@ -140,3 +159,7 @@ method === 'slice' | ||
const create = context => ({ | ||
'CallExpression[callee.type="MemberExpression"]'(node) { | ||
CallExpression(node) { | ||
if (node.callee.type !== 'MemberExpression') { | ||
return; | ||
} | ||
const parsed = parse(node); | ||
@@ -168,3 +191,3 @@ | ||
* fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
for (const node of removableNodes) { | ||
@@ -184,3 +207,3 @@ yield removeLengthNode(node, fixer, sourceCode); | ||
docs: { | ||
description: 'Prefer negative index over `.length - index` for `{String,Array,TypedArray}#{slice,at}()` and `Array#splice()`.', | ||
description: 'Prefer negative index over `.length - index` when possible.', | ||
}, | ||
@@ -187,0 +210,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,16 +11,23 @@ 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; | ||
} | ||
const selector = matches([ | ||
importExportSourceSelector, | ||
STATIC_REQUIRE_SOURCE_SELECTOR, | ||
]); | ||
const {value} = node; | ||
const create = () => ({ | ||
[selector](node) { | ||
const {value} = node; | ||
if ( | ||
@@ -27,0 +34,0 @@ typeof value !== 'string' |
@@ -5,2 +5,3 @@ 'use strict'; | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const isLeftHandSide = require('./utils/is-left-hand-side.js'); | ||
@@ -84,3 +85,3 @@ const MESSAGE_ID_ERROR = 'error'; | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -95,2 +96,3 @@ let objects = Object.keys(globalObjects); | ||
handle: reference => checkProperty(reference, sourceCode), | ||
filter: ({node}) => !isLeftHandSide(node), | ||
}); | ||
@@ -97,0 +99,0 @@ |
'use strict'; | ||
const {isCommaToken, isArrowToken, isClosingParenToken} = require('@eslint-community/eslint-utils'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url.js'); | ||
const {matches, methodCallSelector} = require('./selectors/index.js'); | ||
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,13 +97,6 @@ }, | ||
]; | ||
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); | ||
@@ -116,3 +112,3 @@ const firstToken = parentheses[0] || initObject; | ||
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', |
@@ -12,13 +12,13 @@ 'use strict'; | ||
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-community/eslint-utils'); | ||
const { | ||
methodCallSelector, | ||
emptyObjectSelector, | ||
emptyArraySelector, | ||
matches, | ||
} = require('./selectors/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isMemberExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -16,44 +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.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); | ||
} | ||
@@ -60,0 +72,0 @@ }, |
'use strict'; | ||
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js'); | ||
const {isStringLiteral, isNullLiteral} = require('./ast/index.js'); | ||
const {isNodeValueNotDomNode} = require('./utils/index.js'); | ||
const {isMethodCall, isStringLiteral, isNullLiteral} = require('./ast/index.js'); | ||
@@ -10,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([ | ||
@@ -100,3 +92,15 @@ ['getElementById', 'querySelector'], | ||
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; | ||
@@ -103,0 +107,0 @@ const preferredSelector = disallowedIdentifierNames.get(method); |
'use strict'; | ||
const {getPropertyName} = require('@eslint-community/eslint-utils'); | ||
const {not, methodCallSelector} = require('./selectors/index.js'); | ||
const {isNullLiteral} = require('./ast/index.js'); | ||
const {isNullLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -11,7 +10,2 @@ 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) => ( | ||
@@ -68,4 +62,16 @@ ( | ||
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); | ||
@@ -72,0 +78,0 @@ if (fix) { |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {checkVueTemplate} = require('./utils/rule.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isRegexLiteral, isNewExpression} = require('./ast/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'); | ||
@@ -21,5 +22,7 @@ const REGEXP_EXEC = 'regexp-exec'; | ||
type: REGEXP_EXEC, | ||
selector: methodCallSelector({ | ||
test: node => isMethodCall(node, { | ||
method: 'exec', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -35,5 +38,7 @@ getNodes: node => ({ | ||
type: STRING_MATCH, | ||
selector: methodCallSelector({ | ||
test: node => isMethodCall(node, { | ||
method: 'match', | ||
argumentsLength: 1, | ||
optionalCall: false, | ||
optionalMember: false, | ||
}), | ||
@@ -92,11 +97,13 @@ getNodes: node => ({ | ||
/** @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); | ||
@@ -106,3 +113,3 @@ const {methodNode, regexpNode} = nodes; | ||
if (regexpNode.type === 'Literal' && !regexpNode.regex) { | ||
return; | ||
continue; | ||
} | ||
@@ -115,7 +122,8 @@ | ||
const fixFunction = fixer => fix(fixer, nodes, context.getSourceCode()); | ||
const {sourceCode} = context; | ||
const fixFunction = fixer => fix(fixer, nodes, sourceCode); | ||
if ( | ||
isRegExpNode(regexpNode) | ||
|| isRegExpWithoutGlobalFlag(regexpNode, context.getScope()) | ||
|| isRegExpWithoutGlobalFlag(regexpNode, sourceCode.getScope(regexpNode)) | ||
) { | ||
@@ -132,6 +140,6 @@ problem.fix = fixFunction; | ||
return problem; | ||
}, | ||
]), | ||
); | ||
yield problem; | ||
} | ||
}, | ||
}); | ||
@@ -138,0 +146,0 @@ /** @type {import('eslint').Rule.RuleModule} */ |
'use strict'; | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const getVariableIdentifiers = require('./utils/get-variable-identifiers.js'); | ||
const { | ||
matches, | ||
not, | ||
methodCallSelector, | ||
callOrNewExpressionSelector, | ||
} = require('./selectors/index.js'); | ||
const {getVariableIdentifiers} = require('./utils/index.js'); | ||
const {isCallOrNewExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -18,61 +13,20 @@ 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 => { | ||
@@ -124,5 +78,44 @@ const {type, optional, callee, arguments: includesArguments} = node.parent.parent ?? {}; | ||
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 | ||
@@ -129,0 +122,0 @@ // But can't reproduce, just ignore this case |
'use strict'; | ||
const {findVariable} = require('@eslint-community/eslint-utils'); | ||
const {memberExpressionSelector} = require('./selectors/index.js'); | ||
const {fixSpaceAroundKeyword} = require('./fix/index.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
const {isNewExpression, isMemberExpression} = require('./ast/index.js'); | ||
@@ -12,9 +11,2 @@ const MESSAGE_ID = 'prefer-set-size'; | ||
const lengthAccessSelector = [ | ||
memberExpressionSelector('length'), | ||
'[object.type="ArrayExpression"]', | ||
'[object.elements.length=1]', | ||
'[object.elements.0.type="SpreadElement"]', | ||
].join(''); | ||
const isNewSet = node => isNewExpression(node, {name: 'Set'}); | ||
@@ -71,8 +63,20 @@ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
return { | ||
[lengthAccessSelector](node) { | ||
MemberExpression(node) { | ||
if ( | ||
!isMemberExpression(node, { | ||
property: 'length', | ||
optional: false, | ||
}) | ||
|| node.object.type !== 'ArrayExpression' | ||
|| node.object.elements.length !== 1 | ||
|| node.object.elements[0]?.type !== 'SpreadElement' | ||
) { | ||
return; | ||
} | ||
const maybeSet = node.object.elements[0].argument; | ||
if (!isSet(maybeSet, context.getScope())) { | ||
if (!isSet(maybeSet, sourceCode.getScope(maybeSet))) { | ||
return; | ||
@@ -79,0 +83,0 @@ } |
'use strict'; | ||
const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
const {getParenthesizedRange, getParenthesizedText} = require('./utils/parentheses.js'); | ||
const shouldAddParenthesesToSpreadElementArgument = require('./utils/should-add-parentheses-to-spread-element-argument.js'); | ||
const {isNodeMatches} = require('./utils/is-node-matches.js'); | ||
const { | ||
replaceNodeOrTokenAndSpacesBefore, | ||
removeSpacesAfter, | ||
removeMethodCall, | ||
} = require('./fix/index.js'); | ||
const {isLiteral} = require('./ast/index.js'); | ||
const isMethodNamed = require('./utils/is-method-named.js'); | ||
getParenthesizedRange, | ||
getParenthesizedText, | ||
needsSemicolon, | ||
shouldAddParenthesesToSpreadElementArgument, | ||
isNodeMatches, | ||
isMethodNamed, | ||
} = require('./utils/index.js'); | ||
const {removeMethodCall} = require('./fix/index.js'); | ||
const {isLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -19,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'; | ||
@@ -30,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(\'\')`.', | ||
@@ -39,24 +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'); | ||
const arraySliceCallSelector = [ | ||
methodCallSelector({ | ||
method: 'slice', | ||
minimumArguments: 0, | ||
maximumArguments: 1, | ||
}), | ||
'[callee.object.type!="ArrayExpression"]', | ||
].join(''); | ||
const ignoredSliceCallee = [ | ||
@@ -70,7 +48,2 @@ 'arrayBuffer', | ||
const stringSplitCallSelector = methodCallSelector({ | ||
method: 'split', | ||
argumentsLength: 1, | ||
}); | ||
const isArrayLiteral = node => node.type === 'ArrayExpression'; | ||
@@ -263,9 +236,2 @@ const isArrayLiteralHasTrailingComma = (node, sourceCode) => { | ||
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) { | ||
@@ -279,11 +245,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); | ||
}; | ||
@@ -344,6 +302,17 @@ } | ||
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 { | ||
@@ -354,145 +323,201 @@ node, | ||
}; | ||
}, | ||
[arrayConcatCallSelector](node) { | ||
const {object} = node.callee; | ||
} | ||
}); | ||
if (isNotArray(object, context.getScope())) { | ||
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 && !isLiteral(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 (!isLiteral(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; | ||
}); | ||
}; | ||
@@ -506,3 +531,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(\'\')`.', | ||
}, | ||
@@ -509,0 +534,0 @@ fixable: 'code', |
@@ -5,4 +5,3 @@ 'use strict'; | ||
const escapeString = require('./utils/escape-string.js'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const {isRegexLiteral, isNewExpression} = require('./ast/index.js'); | ||
const {isRegexLiteral, isNewExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -16,7 +15,2 @@ const MESSAGE_ID_USE_REPLACE_ALL = 'method'; | ||
const selector = methodCallSelector({ | ||
methods: ['replace', 'replaceAll'], | ||
argumentsLength: 2, | ||
}); | ||
function getPatternReplacement(node) { | ||
@@ -85,3 +79,12 @@ if (!isRegexLiteral(node)) { | ||
const create = context => ({ | ||
[selector](node) { | ||
CallExpression(node) { | ||
if (!isMethodCall(node, { | ||
methods: ['replace', 'replaceAll'], | ||
argumentsLength: 2, | ||
optionalCall: false, | ||
optionalMember: false, | ||
})) { | ||
return; | ||
} | ||
const { | ||
@@ -92,3 +95,3 @@ arguments: [pattern], | ||
if (!isRegExpWithGlobalFlag(pattern, context.getScope())) { | ||
if (!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))) { | ||
return; | ||
@@ -95,0 +98,0 @@ } |
'use strict'; | ||
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} = require('./ast/index.js'); | ||
const {isNumberLiteral, isMethodCall} = require('./ast/index.js'); | ||
@@ -16,8 +15,2 @@ const MESSAGE_ID_SUBSTR = 'substr'; | ||
const selector = methodCallSelector({ | ||
methods: ['substr', 'substring'], | ||
includeOptionalMember: true, | ||
includeOptionalCall: true, | ||
}); | ||
const getNumericValue = node => { | ||
@@ -49,4 +42,4 @@ if (isNumberLiteral(node)) { | ||
const scope = context.getScope(); | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const scope = sourceCode.getScope(node); | ||
const firstArgumentStaticResult = getStaticValue(firstArgument, scope); | ||
@@ -76,3 +69,3 @@ const secondArgumentRange = getParenthesizedRange(secondArgument, sourceCode); | ||
if (argumentNodes.every(node => isNumber(node, context.getScope()))) { | ||
if (argumentNodes.every(node => isNumber(node, scope))) { | ||
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); | ||
@@ -88,3 +81,3 @@ | ||
function * fixSubstringArguments({node, fixer, context, abort}) { | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const [firstArgument, secondArgument] = node.arguments; | ||
@@ -151,3 +144,7 @@ | ||
const create = context => ({ | ||
[selector](node) { | ||
CallExpression(node) { | ||
if (!isMethodCall(node, {methods: ['substr', 'substring']})) { | ||
return; | ||
} | ||
const method = node.callee.property.name; | ||
@@ -154,0 +151,0 @@ |
'use strict'; | ||
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {methodCallSelector} = require('./selectors/index.js'); | ||
const escapeString = require('./utils/escape-string.js'); | ||
@@ -8,2 +7,3 @@ const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js'); | ||
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); | ||
const {isMethodCall, isRegexLiteral} = require('./ast/index.js'); | ||
@@ -30,7 +30,2 @@ const MESSAGE_STARTS_WITH = 'prefer-starts-with'; | ||
const regexTestSelector = [ | ||
methodCallSelector({method: 'test', argumentsLength: 1}), | ||
'[callee.object.regex]', | ||
].join(''); | ||
const checkRegex = ({pattern, flags}) => { | ||
@@ -66,6 +61,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; | ||
@@ -89,3 +96,3 @@ const {regex} = regexNode; | ||
if (!isString) { | ||
const staticValue = getStaticValue(target, context.getScope()); | ||
const staticValue = getStaticValue(target, sourceCode.getScope(target)); | ||
@@ -92,0 +99,0 @@ if (staticValue) { |
'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'; |
@@ -259,3 +259,3 @@ 'use strict'; | ||
}; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
const ifStatements = new Set(); | ||
@@ -266,7 +266,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); | ||
} | ||
}, | ||
@@ -273,0 +275,0 @@ * 'Program:exit'() { |
@@ -14,10 +14,2 @@ 'use strict'; | ||
const selector = [ | ||
'IfStatement', | ||
':not(IfStatement > .alternate)', | ||
'[test.type!="ConditionalExpression"]', | ||
'[consequent]', | ||
'[alternate]', | ||
].join(''); | ||
const isTernary = node => node?.type === 'ConditionalExpression'; | ||
@@ -50,3 +42,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-community/eslint-utils'); | ||
const {matches, not, memberExpressionSelector} = require('./selectors/index.js'); | ||
const {isFunction, isMemberExpression, isMethodCall} = require('./ast/index.js'); | ||
@@ -16,30 +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 *', 'ClassDeclaration *', 'ClassExpression *']), | ||
].join(''); | ||
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 => | ||
@@ -50,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') { | ||
@@ -62,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) { | ||
@@ -103,9 +124,3 @@ return; | ||
!value | ||
|| !( | ||
( | ||
value.type === 'ArrowFunctionExpression' | ||
|| value.type === 'FunctionExpression' | ||
|| value.type === 'FunctionDeclaration' | ||
) && !value.generator && value.async | ||
) | ||
|| !(isFunction(value) && !value.generator && value.async) | ||
) { | ||
@@ -112,0 +127,0 @@ return; |
'use strict'; | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
const {isNewExpression} = require('./ast/index.js'); | ||
@@ -54,7 +54,2 @@ const MESSAGE_ID = 'prefer-type-error'; | ||
const selector = [ | ||
'ThrowStatement', | ||
newExpressionSelector({name: 'Error', path: 'argument'}), | ||
].join(''); | ||
const isTypecheckingIdentifier = (node, callExpression, isMemberExpression) => | ||
@@ -129,5 +124,6 @@ callExpression !== undefined | ||
const create = () => ({ | ||
[selector](node) { | ||
ThrowStatement(node) { | ||
if ( | ||
isLone(node) | ||
isNewExpression(node.argument, {name: 'Error'}) | ||
&& isLone(node) | ||
&& node.parent.parent | ||
@@ -134,0 +130,0 @@ && isTypechecking(node.parent.parent) |
'use strict'; | ||
const path = require('node:path'); | ||
const {defaultsDeep, upperFirst, lowerFirst} = require('lodash'); | ||
const avoidCapture = require('./utils/avoid-capture.js'); | ||
@@ -342,3 +341,3 @@ const cartesianProductSamples = require('./utils/cartesian-product-samples.js'); | ||
const options = prepareOptions(context.options[0]); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
const filenameWithExtension = context.physicalFilename; | ||
@@ -544,3 +543,3 @@ // A `class` declaration produces two variables in two scopes: | ||
'Program:exit'() { | ||
'Program:exit'(program) { | ||
if (!options.checkVariables) { | ||
@@ -550,3 +549,3 @@ return; | ||
checkScope(context.getScope()); | ||
checkScope(context.sourceCode.getScope(program)); | ||
}, | ||
@@ -553,0 +552,0 @@ }; |
'use strict'; | ||
const {getStaticValue} = require('@eslint-community/eslint-utils'); | ||
const {newExpressionSelector} = require('./selectors/index.js'); | ||
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,3 +40,3 @@ 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)); | ||
@@ -62,3 +53,3 @@ if ( | ||
function canRemoveDotSlash(node, context) { | ||
function canRemoveDotSlash(node, sourceCode) { | ||
const rawValue = node.raw.slice(1, -1); | ||
@@ -70,3 +61,3 @@ if (!rawValue.startsWith(DOT_SLASH)) { | ||
const baseNode = node.parent.arguments[1]; | ||
const staticValueResult = getStaticValue(baseNode, context.getScope()); | ||
const staticValueResult = getStaticValue(baseNode, sourceCode.getScope(node)); | ||
@@ -83,4 +74,4 @@ if ( | ||
function addDotSlash(node, context) { | ||
if (!canAddDotSlash(node, context)) { | ||
function addDotSlash(node, sourceCode) { | ||
if (!canAddDotSlash(node, sourceCode)) { | ||
return; | ||
@@ -92,4 +83,4 @@ } | ||
function removeDotSlash(node, context) { | ||
if (!canRemoveDotSlash(node, context)) { | ||
function removeDotSlash(node, sourceCode) { | ||
if (!canRemoveDotSlash(node, sourceCode)) { | ||
return; | ||
@@ -109,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]; | ||
@@ -132,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); | ||
@@ -140,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'); | ||
@@ -14,5 +14,14 @@ | ||
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); | ||
@@ -19,0 +28,0 @@ const replacements = []; |
@@ -87,2 +87,5 @@ /* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */ | ||
}, | ||
elems: { | ||
elements: true, | ||
}, | ||
env: { | ||
@@ -89,0 +92,0 @@ environment: true, |
'use strict'; | ||
const {hasSideEffect, isParenthesized, findVariable} = require('@eslint-community/eslint-utils'); | ||
const {matches, methodCallSelector} = require('../selectors/index.js'); | ||
const isFunctionSelfUsedInside = require('../utils/is-function-self-used-inside.js'); | ||
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; | ||
@@ -54,76 +52,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; |
@@ -76,59 +76,57 @@ 'use strict'; | ||
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 problem = { | ||
const {fix: autoFix, message = defaultMessage, match, suggest, regex} = replacement; | ||
const problem = { | ||
node, | ||
message, | ||
data: { | ||
match, | ||
suggest, | ||
}, | ||
}; | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' | ||
? fixer => fixer.replaceText( | ||
node, | ||
message, | ||
data: { | ||
match, | ||
suggest, | ||
escapeString(fixed, raw[0]), | ||
) | ||
: fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed), | ||
); | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
fix, | ||
}, | ||
}; | ||
]; | ||
} | ||
const fixed = string.replace(regex, suggest); | ||
const fix = type === 'Literal' | ||
? fixer => fixer.replaceText( | ||
node, | ||
escapeString(fixed, raw[0]), | ||
) | ||
: fixer => replaceTemplateElement( | ||
fixer, | ||
node, | ||
escapeTemplateElementRaw(fixed), | ||
); | ||
if (autoFix) { | ||
problem.fix = fix; | ||
} else { | ||
problem.suggest = [ | ||
{ | ||
messageId: SUGGESTION_MESSAGE_ID, | ||
fix, | ||
}, | ||
]; | ||
} | ||
return problem; | ||
}, | ||
}; | ||
return problem; | ||
}); | ||
}; | ||
@@ -135,0 +133,0 @@ |
@@ -40,3 +40,3 @@ 'use strict'; | ||
const isBracesRequired = context.options[0] !== 'avoid'; | ||
const sourceCode = context.getSourceCode(); | ||
const {sourceCode} = context; | ||
@@ -43,0 +43,0 @@ return { |
@@ -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(); | ||
@@ -85,3 +97,3 @@ const joined = node.quasis | ||
context.report({ | ||
return { | ||
node, | ||
@@ -92,22 +104,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?.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); | ||
}, | ||
@@ -114,0 +159,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 @@ |
@@ -59,3 +59,3 @@ 'use strict'; | ||
return { | ||
'Program:exit': () => this.track(context.getScope()), | ||
'Program:exit': program => this.track(context.sourceCode.getScope(program)), | ||
}; | ||
@@ -62,0 +62,0 @@ } |
'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' |
'use strict'; | ||
module.exports = ({parent}) => !parent || parent.type === 'ExpressionStatement'; | ||
const {isExpressionStatement} = require('../ast/index.js'); | ||
module.exports = node => isExpressionStatement(node.parent); |
@@ -36,36 +36,31 @@ 'use strict'; | ||
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, | ||
}; | ||
} | ||
suggest.data = { | ||
...problem.data, | ||
...suggest.data, | ||
}; | ||
} | ||
} | ||
context.report(problem); | ||
} | ||
}; | ||
context.report(problem); | ||
} | ||
} | ||
@@ -81,6 +76,34 @@ | ||
const wrapped = context => { | ||
const listeners = create(context); | ||
const listeners = {}; | ||
const addListener = (selector, listener) => { | ||
listeners[selector] ??= []; | ||
listeners[selector].push(listener); | ||
}; | ||
if (!listeners) { | ||
return {}; | ||
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); | ||
} | ||
@@ -90,3 +113,11 @@ | ||
Object.entries(listeners) | ||
.map(([selector, listener]) => [selector, reportListenerProblems(listener, context)]), | ||
.map(([selector, listeners]) => [ | ||
selector, | ||
// Listener arguments can be `codePath, node` or `node` | ||
(...listenerArguments) => { | ||
for (const listener of listeners) { | ||
reportListenerProblems(listener(...listenerArguments), context); | ||
} | ||
}, | ||
]), | ||
); | ||
@@ -112,8 +143,9 @@ }; | ||
const listeners = create(context); | ||
const {parserServices} = context.sourceCode; | ||
// `vue-eslint-parser` | ||
if (context.parserServices?.defineTemplateBodyVisitor) { | ||
if (parserServices?.defineTemplateBodyVisitor) { | ||
return visitScriptBlock | ||
? context.parserServices.defineTemplateBodyVisitor(listeners, listeners) | ||
: context.parserServices.defineTemplateBodyVisitor(listeners); | ||
? parserServices.defineTemplateBodyVisitor(listeners, listeners) | ||
: parserServices.defineTemplateBodyVisitor(listeners); | ||
} | ||
@@ -120,0 +152,0 @@ |
593722
16
18994
208
218
+ Addedregjsparser@0.10.0(transitive)
- Removedsafe-regex@^2.1.1
- Removedregjsparser@0.9.1(transitive)
- Removedsafe-regex@2.1.1(transitive)
Updatedci-info@^3.8.0
Updatedesquery@^1.5.0
Updatedis-builtin-module@^3.2.1
Updatedregexp-tree@^0.1.27
Updatedregjsparser@^0.10.0
Updatedsemver@^7.5.4