Skip to content

Commit

Permalink
Add prefer-set-size rule (#1952)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fisker and sindresorhus committed Nov 16, 2022
1 parent 76deaa3 commit 5f23c98
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 0 deletions.
1 change: 1 addition & 0 deletions configs/recommended.js
Expand Up @@ -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',
Expand Down
26 changes: 26 additions & 0 deletions 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).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

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;
}
```
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -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()`. || 🔧 | |
Expand Down
101 changes: 101 additions & 0 deletions 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,
},
};
46 changes: 46 additions & 0 deletions 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',
],
});
155 changes: 155 additions & 0 deletions 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\`.␊
`
Binary file added test/snapshots/prefer-set-size.mjs.snap
Binary file not shown.

0 comments on commit 5f23c98

Please sign in to comment.