From 5f23c9834815700e5ddbb6ad07c5db3ac59dc114 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 16 Nov 2022 19:06:29 +0800 Subject: [PATCH] Add `prefer-set-size` rule (#1952) Co-authored-by: Sindre Sorhus --- configs/recommended.js | 1 + docs/rules/prefer-set-size.md | 26 ++++ readme.md | 1 + rules/prefer-set-size.js | 101 +++++++++++++++ test/prefer-set-size.mjs | 46 +++++++ test/snapshots/prefer-set-size.mjs.md | 155 ++++++++++++++++++++++++ test/snapshots/prefer-set-size.mjs.snap | Bin 0 -> 672 bytes 7 files changed, 330 insertions(+) create mode 100644 docs/rules/prefer-set-size.md create mode 100644 rules/prefer-set-size.js create mode 100644 test/prefer-set-size.mjs create mode 100644 test/snapshots/prefer-set-size.mjs.md create mode 100644 test/snapshots/prefer-set-size.mjs.snap diff --git a/configs/recommended.js b/configs/recommended.js index 2e7ffd8713..a1d26d031a 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -101,6 +101,7 @@ module.exports = { 'unicorn/prefer-reflect-apply': 'error', 'unicorn/prefer-regexp-test': 'error', 'unicorn/prefer-set-has': 'error', + 'unicorn/prefer-set-size': 'error', 'unicorn/prefer-spread': 'error', // TODO: Enable this by default when targeting Node.js 16. 'unicorn/prefer-string-replace-all': 'off', diff --git a/docs/rules/prefer-set-size.md b/docs/rules/prefer-set-size.md new file mode 100644 index 0000000000..7ddc30c1d1 --- /dev/null +++ b/docs/rules/prefer-set-size.md @@ -0,0 +1,26 @@ +# Prefer using `Set#size` instead of `Array#length` + +✅ This rule is enabled in the `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs). + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + + +Prefer using `Set#size` directly instead of first converting it to an array and then using its `.length` property. + +## Fail + +```js +function isUnique(array) { + return [...new Set(array)].length === array.length; +} +``` + +## Pass + +```js +function isUnique(array) { + return new Set(array).size === array.length; +} +``` diff --git a/readme.md b/readme.md index 247137dc0a..9653435077 100644 --- a/readme.md +++ b/readme.md @@ -139,6 +139,7 @@ Use a [preset config](#preset-configs) or configure each rules in `package.json` | [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()`. | ✅ | 🔧 | | diff --git a/rules/prefer-set-size.js b/rules/prefer-set-size.js new file mode 100644 index 0000000000..c7b7eaf6ec --- /dev/null +++ b/rules/prefer-set-size.js @@ -0,0 +1,101 @@ +'use strict'; +const {findVariable} = require('eslint-utils'); +const {memberExpressionSelector} = require('./selectors/index.js'); +const {fixSpaceAroundKeyword} = require('./fix/index.js'); + +const MESSAGE_ID = 'prefer-set-size'; +const messages = { + [MESSAGE_ID]: 'Prefer using `Set#size` instead of `Array#length`.', +}; + +const lengthAccessSelector = [ + memberExpressionSelector('length'), + '[object.type="ArrayExpression"]', + '[object.elements.length=1]', + '[object.elements.0.type="SpreadElement"]', +].join(''); + +const isNewSet = node => + node?.type === 'NewExpression' + && node.callee.type === 'Identifier' + && node.callee.name === 'Set'; + +function isSet(node, scope) { + if (isNewSet(node)) { + return true; + } + + if (node.type !== 'Identifier') { + return false; + } + + const variable = findVariable(scope, node); + + if (!variable || variable.defs.length !== 1) { + return false; + } + + const [definition] = variable.defs; + + if (definition.type !== 'Variable' || definition.kind !== 'const') { + return false; + } + + const declarator = definition.node; + return declarator.type === 'VariableDeclarator' + && declarator.id.type === 'Identifier' + && isNewSet(definition.node.init); +} + +// `[...set].length` -> `set.size` +function fix(sourceCode, lengthAccessNodes) { + const { + object: arrayExpression, + property, + } = lengthAccessNodes; + const set = arrayExpression.elements[0].argument; + + if (sourceCode.getCommentsInside(arrayExpression).length > sourceCode.getCommentsInside(set).length) { + return; + } + + /** @param {import('eslint').Rule.RuleFixer} fixer */ + return function * (fixer) { + yield fixer.replaceText(property, 'size'); + yield fixer.replaceText(arrayExpression, sourceCode.getText(set)); + yield * fixSpaceAroundKeyword(fixer, lengthAccessNodes, sourceCode); + }; +} + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + const sourceCode = context.getSourceCode(); + + return { + [lengthAccessSelector](node) { + const maybeSet = node.object.elements[0].argument; + if (!isSet(maybeSet, context.getScope())) { + return; + } + + return { + node: node.property, + messageId: MESSAGE_ID, + fix: fix(sourceCode, node), + }; + }, + }; +}; + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer using `Set#size` instead of `Array#length`.', + }, + fixable: 'code', + messages, + }, +}; diff --git a/test/prefer-set-size.mjs b/test/prefer-set-size.mjs new file mode 100644 index 0000000000..c533cad911 --- /dev/null +++ b/test/prefer-set-size.mjs @@ -0,0 +1,46 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + 'new Set(foo).size', + 'for (const foo of bar) console.log([...foo].length)', + '[...new Set(array), foo].length', + '[foo, ...new Set(array), ].length', + '[...new Set(array)].notLength', + '[...new Set(array)]?.length', + '[...new Set(array)][length]', + '[...new Set(array)]["length"]', + '[...new NotSet(array)].length', + '[...Set(array)].length', + 'const foo = new NotSet([]);[...foo].length;', + 'let foo = new Set([]);[...foo].length;', + 'const {foo} = new Set([]);[...foo].length;', + 'const [foo] = new Set([]);[...foo].length;', + '[...foo].length', + 'var foo = new Set(); var foo = new Set(); [...foo].length', + ], + invalid: [ + '[...new Set(array)].length', + outdent` + const foo = new Set([]); + console.log([...foo].length); + `, + outdent` + function isUnique(array) { + return[...new Set(array)].length === array.length + } + `, + '[...new Set(array),].length', + '[...(( new Set(array) ))].length', + '(( [...new Set(array)] )).length', + outdent` + foo + ;[...new Set(array)].length + `, + '[/* comment */...new Set(array)].length', + '[...new /* comment */ Set(array)].length', + ], +}); diff --git a/test/snapshots/prefer-set-size.mjs.md b/test/snapshots/prefer-set-size.mjs.md new file mode 100644 index 0000000000..99ee5ca8b5 --- /dev/null +++ b/test/snapshots/prefer-set-size.mjs.md @@ -0,0 +1,155 @@ +# Snapshot report for `test/prefer-set-size.mjs` + +The actual snapshot is saved in `prefer-set-size.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | [...new Set(array)].length + +> Output + + `␊ + 1 | new Set(array).size␊ + ` + +> Error 1/1 + + `␊ + > 1 | [...new Set(array)].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #2 + 1 | const foo = new Set([]); + 2 | console.log([...foo].length); + +> Output + + `␊ + 1 | const foo = new Set([]);␊ + 2 | console.log(foo.size);␊ + ` + +> Error 1/1 + + `␊ + 1 | const foo = new Set([]);␊ + > 2 | console.log([...foo].length);␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #3 + 1 | function isUnique(array) { + 2 | return[...new Set(array)].length === array.length + 3 | } + +> Output + + `␊ + 1 | function isUnique(array) {␊ + 2 | return new Set(array).size === array.length␊ + 3 | }␊ + ` + +> Error 1/1 + + `␊ + 1 | function isUnique(array) {␊ + > 2 | return[...new Set(array)].length === array.length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + 3 | }␊ + ` + +## Invalid #4 + 1 | [...new Set(array),].length + +> Output + + `␊ + 1 | new Set(array).size␊ + ` + +> Error 1/1 + + `␊ + > 1 | [...new Set(array),].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #5 + 1 | [...(( new Set(array) ))].length + +> Output + + `␊ + 1 | new Set(array).size␊ + ` + +> Error 1/1 + + `␊ + > 1 | [...(( new Set(array) ))].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #6 + 1 | (( [...new Set(array)] )).length + +> Output + + `␊ + 1 | (( new Set(array) )).size␊ + ` + +> Error 1/1 + + `␊ + > 1 | (( [...new Set(array)] )).length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #7 + 1 | foo + 2 | ;[...new Set(array)].length + +> Output + + `␊ + 1 | foo␊ + 2 | ;new Set(array).size␊ + ` + +> Error 1/1 + + `␊ + 1 | foo␊ + > 2 | ;[...new Set(array)].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #8 + 1 | [/* comment */...new Set(array)].length + +> Error 1/1 + + `␊ + > 1 | [/* comment */...new Set(array)].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` + +## Invalid #9 + 1 | [...new /* comment */ Set(array)].length + +> Output + + `␊ + 1 | new /* comment */ Set(array).size␊ + ` + +> Error 1/1 + + `␊ + > 1 | [...new /* comment */ Set(array)].length␊ + | ^^^^^^ Prefer using \`Set#size\` instead of \`Array#length\`.␊ + ` diff --git a/test/snapshots/prefer-set-size.mjs.snap b/test/snapshots/prefer-set-size.mjs.snap new file mode 100644 index 0000000000000000000000000000000000000000..66a1fea33ec5c76bc56c2eb6f1a2d572c911245a GIT binary patch literal 672 zcmV;R0$=?>RzVrUPfi}_%A%#FeAeaxt>Yq9{Fz!Ejk<;~1{akk0e@tM}YfxMc zRA`iy+2L&`GDB@1-`Xa|sOL;z(PdC<&cwiAsoV3f&du9x+wr!~SvkVbn82copg5nI zfng`x=d%k;Qa;G-F?ZUgdD(;sEV>wqO;{KhcFBazH!W9bi7s^C|73^RMJBLlEEK;3 z;zv4?SLbn>M+)3q`lWM{OFa`V z7=9K!K74(uaptZC>wlM8I=e7|MR!86Ejt4PGXpy~Y#3P?1eqovsD%2=M>uV__ z=jZ08=9MUD>Fep~<)xM@1gDm0Bo-AVR%*uT<)r4Nmt=4$06~ocK@bN93IRo_X{kjD zrNxL2)3G z)AT?_qG)591osuxBe?XE>Md&dOA+cX4Gjg%)?;&220vvEW;T?L_`qzjs*br2oeEp G2mk=TPcG2_ literal 0 HcmV?d00001