Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer-set-has rule #604

Merged
merged 18 commits into from Mar 24, 2020
35 changes: 35 additions & 0 deletions docs/rules/prefer-set-has.md
@@ -0,0 +1,35 @@
# Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence

[`Set#has()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) is faster than [`Array#includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes).

This rule is fixable.

## Fail

```js
const array = [1, 2, 3];

function isExists(find) {
return array.includes(find);
}
```

## Pass

```js
const set = new Set([1, 2, 3]);

function isExists(find) {
return set.has(find);
}
```

```js
const array = [1, 2];

function isExists(find) {
return array.includes(find);
}

array.push(3);
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -57,6 +57,7 @@ module.exports = {
'unicorn/prefer-reflect-apply': 'error',
// TODO: Enable this by default when it's shipping in a Node.js LTS version.
'unicorn/prefer-replace-all': 'off',
'unicorn/prefer-set-has': 'error',
'unicorn/prefer-spread': 'error',
'unicorn/prefer-starts-ends-with': 'error',
'unicorn/prefer-string-slice': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -72,6 +72,7 @@ Configure it in `package.json`.
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-reflect-apply": "error",
"unicorn/prefer-replace-all": "off",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-starts-ends-with": "error",
"unicorn/prefer-string-slice": "error",
Expand Down Expand Up @@ -126,6 +127,7 @@ Configure it in `package.json`.
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. *(partly fixable)*
- [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) - Prefer `Reflect.apply()` over `Function#apply()`. *(fixable)*
- [prefer-replace-all](docs/rules/prefer-replace-all.md) - Prefer `String#replaceAll()` over regex searches with the global flag. *(fixable)*
- [prefer-set-has](docs/rules/prefer-set-has.md) - Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. *(fixable)*
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
- [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWith()` & `String#endsWith()` over more complex alternatives.
- [prefer-string-slice](docs/rules/prefer-string-slice.md) - Prefer `String#slice()` over `String#substr()` and `String#substring()`. *(partly fixable)*
Expand Down
5 changes: 1 addition & 4 deletions rules/consistent-function-scoping.js
@@ -1,13 +1,10 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const getReferences = require('./utils/get-references');

const MESSAGE_ID_NAMED = 'named';
const MESSAGE_ID_ANONYMOUS = 'anonymous';

const getReferences = scope => scope.references.concat(
...scope.childScopes.map(scope => getReferences(scope))
);

const isSameScope = (scope1, scope2) =>
scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block);

Expand Down
6 changes: 3 additions & 3 deletions rules/prefer-event-key.js
Expand Up @@ -2,11 +2,11 @@
const getDocumentationUrl = require('./utils/get-documentation-url');
const quoteString = require('./utils/quote-string');

const keys = [
const keys = new Set([
'keyCode',
'charCode',
'which'
];
]);

// https://github.com/facebook/react/blob/b87aabd/packages/react-dom/src/events/getEventKey.js#L36
// Only meta characters which can't be deciphered from `String.fromCharCode()`
Expand Down Expand Up @@ -174,7 +174,7 @@ const create = context => {
Property(node) {
// Destructured case
const propertyName = node.value && node.value.name;
if (!keys.includes(propertyName)) {
if (!keys.has(propertyName)) {
return;
}

Expand Down
95 changes: 95 additions & 0 deletions rules/prefer-set-has.js
@@ -0,0 +1,95 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const getReferences = require('./utils/get-references');

const selector = [
':not(ExportNamedDeclaration)',
'>',
'VariableDeclaration',
'>',
'VariableDeclarator',
'[init.type="ArrayExpression"]',
'>',
'Identifier'
].join('');

const MESSAGE_ID = 'preferSetHas';

const isIncludesCall = node => {
/* istanbul ignore next */
if (!node.parent || !node.parent.parent) {
return false;
}

const {type, optional, callee, arguments: parameters} = node.parent.parent;
return (
type === 'CallExpression' &&
!optional,
callee &&
callee.type === 'MemberExpression' &&
!callee.computed &&
callee.object === node &&
callee.property.type === 'Identifier' &&
callee.property.name === 'includes' &&
parameters.length === 1 &&
parameters[0].type !== 'SpreadElement'
);
};

const create = context => {
const scope = context.getScope();
const declarations = new Set();

return {
[selector]: node => {
declarations.add(node);
},
'Program:exit'() {
if (declarations.size === 0) {
return;
}

const references = getReferences(scope);
for (const declaration of declarations) {
const variable = references
.find(({identifier}) => identifier === declaration)
.resolved;
const nodes = variable.references
.map(({identifier}) => identifier)
.filter(node => node !== declaration);

if (
nodes.length > 0 &&
nodes.every(node => isIncludesCall(node))
) {
context.report({
node: declaration,
messageId: MESSAGE_ID,
data: {
name: declaration.name
},
fix: fixer => [
fixer.insertTextBefore(declaration.parent.init, 'new Set('),
fixer.insertTextAfter(declaration.parent.init, ')'),
...nodes.map(node => fixer.replaceText(node.parent.property, 'has'))
]
});
}
}
}
};
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages: {
[MESSAGE_ID]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.'
}
}
};
10 changes: 10 additions & 0 deletions rules/utils/get-references.js
@@ -0,0 +1,10 @@
'use strict';
const {uniq} = require('lodash');

const getReferences = scope => uniq(
scope.references.concat(
...scope.childScopes.map(scope => getReferences(scope))
)
);

module.exports = getReferences;