Skip to content

Commit

Permalink
Add prefer-destructuring-in-parameters rule
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Jan 21, 2021
1 parent a091842 commit b9f5126
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 0 deletions.
17 changes: 17 additions & 0 deletions docs/rules/prefer-destructuring-in-parameters.md
@@ -0,0 +1,17 @@
# Prefer destructuring in parameters over accessing properties.

<!-- More detailed description. Remove this comment. -->

This rule is fixable.

## Fail

```js
const foo = 'unicorn';
```

## Pass

```js
const foo = '🦄';
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -88,6 +88,7 @@ module.exports = {
'unicorn/prefer-array-some': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-default-parameters': 'error',
'unicorn/prefer-destructuring-in-parameters': 'error',
'unicorn/prefer-dom-node-append': 'error',
'unicorn/prefer-dom-node-dataset': 'error',
'unicorn/prefer-dom-node-remove': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -80,6 +80,7 @@ Configure it in `package.json`.
"unicorn/prefer-array-some": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-default-parameters": "error",
"unicorn/prefer-destructuring-in-parameters": "error",
"unicorn/prefer-dom-node-append": "error",
"unicorn/prefer-dom-node-dataset": "error",
"unicorn/prefer-dom-node-remove": "error",
Expand Down Expand Up @@ -158,6 +159,7 @@ Configure it in `package.json`.
- [prefer-array-some](docs/rules/prefer-array-some.md) - Prefer `.some(…)` over `.find(…)`.
- [prefer-date-now](docs/rules/prefer-date-now.md) - Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. *(fixable)*
- [prefer-default-parameters](docs/rules/prefer-default-parameters.md) - Prefer default parameters over reassignment. *(fixable)*
- [prefer-destructuring-in-parameters](docs/rules/prefer-destructuring-in-parameters.md) - Prefer destructuring in parameters over accessing properties. *(fixable)*
- [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) - Prefer `Node#append()` over `Node#appendChild()`. *(fixable)*
- [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)*
- [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) - Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. *(fixable)*
Expand Down
183 changes: 183 additions & 0 deletions rules/prefer-destructuring-in-parameters.js
@@ -0,0 +1,183 @@
'use strict';
const {upperFirst} = require('lodash');
const {findVariable, isNotOpeningParenToken} = require('eslint-utils');
const getDocumentationUrl = require('./utils/get-documentation-url');
const avoidCapture = require('./utils/avoid-capture');

const MESSAGE_ID = 'prefer-destructuring-in-parameters';
const messages = {
[MESSAGE_ID]: '`{{member}}` should be destructed in parameter `{{parameter}}`.'
};

const indexVariableNamePrefixes = ['first', 'second'];

function getMemberExpressionProperty(node) {
const {parent} = node;
if (parent.type !== 'MemberExpression') {
return;
}

const {computed, optional, object, property} = parent;

if (optional || object !== node) {
return;
}

if (computed) {
if (property.type !== 'Literal') {
return;
}

const index = property.value;
if (
typeof index !== 'number' ||
!Number.isInteger(index) ||
!(index >= 0 && index < indexVariableNamePrefixes.length)
) {
return;
}

return index;
}

if (property.type === 'Identifier') {
return property.name;
}
}

function fix({sourceCode, parameter, memberExpressions, isIndex}) {
function * fixArrowFunctionParentheses(fixer) {
const functionNode = parameter.parent;
if (
functionNode.type === 'ArrowFunctionExpression' &&
functionNode.params.length === 1 &&
isNotOpeningParenToken(sourceCode.getFirstToken(parameter))
) {
yield fixer.insertTextBefore(parameter, '(');
yield fixer.insertTextAfter(parameter, ')');
}
}

function fixParameter(fixer) {
let text;
if (isIndex) {
const variables = [];
for (const [index, {variable}] of memberExpressions.entries()) {
variables[index] = variable;
}

text = `[${variables.join(', ')}]`;
} else {
const variables = [...memberExpressions.keys()];

text = `{${variables.join(', ')}}`
}

return fixer.replaceText(parameter, text);
}

return function * (fixer) {
yield * fixArrowFunctionParentheses(fixer);
yield fixParameter(fixer);

for (const {variable, expressions} of memberExpressions.values()) {
for (const expression of expressions) {
yield fixer.replaceText(expression, variable);
}
}
};
}

const create = context => {
const {ecmaVersion} = context.parserOptions;
const sourceCode = context.getSourceCode();
return {
':function > Identifier.params'(parameter) {
const scope = context.getScope();
const {name} = parameter;
const variable = findVariable(scope, parameter);
const identifiers = variable.references.map(({identifier}) => identifier);

const memberExpressions = new Map();
let lastPropertyType;
let firstExpression;
for (const identifier of identifiers) {
const property = getMemberExpressionProperty(identifier);
const propertyType = typeof property;
if (
propertyType === 'undefined' ||
(lastPropertyType && propertyType !== lastPropertyType)
) {
return;
}

const memberExpression = identifier.parent;

if (memberExpressions.has(property)) {
memberExpressions.get(property).expressions.push(memberExpression);
} else {
memberExpressions.set(property, {expressions: [memberExpression]});
}

lastPropertyType = propertyType;
firstExpression = (
firstExpression && firstExpression.node.range[0] < memberExpression.range[0]
) ?
firstExpression :
{node: memberExpression, property};
}

if (memberExpressions.size === 0) {
return;
}

const isIndex = lastPropertyType === 'number';
const scopes = [
variable.scope,
...variable.references.map(({from}) => from)
];
for (const [property, data] of memberExpressions.entries()) {
let variableName;
if (isIndex) {
const index = indexVariableNamePrefixes[property];
variableName = avoidCapture(`${index}ElementOf${upperFirst(name)}`, scopes, ecmaVersion);
} else {
variableName = avoidCapture(property, scopes, ecmaVersion);
if (variableName !== property) {
return;
}
}

data.variable = variableName;
}

const {node, property} = firstExpression;
context.report({
node,
messageId: MESSAGE_ID,
data: {
member: isIndex ? `${name}[${property}]` : `${name}.${property}`,
parameter: name
},
fix: fix({
sourceCode,
parameter,
memberExpressions,
isIndex
})
});
}
}
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages
}
};
29 changes: 29 additions & 0 deletions test/prefer-destructuring-in-parameters.js
@@ -0,0 +1,29 @@
import {outdent} from 'outdent';
import {test} from './utils/test.js';

test.snapshot({
valid: [
'const foo = bar => bar',
'const foo = function bar(baz) {return bar.name}',
'const foo = ({bar}) => bar',
'const foo = bar => bar[3]',
'const foo = bar => bar[1.5]',
'const foo = bar => bar[-1]',
'const foo = bar => bar[0xFF]',
'const foo = bar => bar[null]',
'const foo = bar => bar[1n]',
'const foo = bar => bar["baz"]',
'const foo = bar => bar.length && bar[0]',
'const foo = bar => bar.default',
'const foo = bar => bar.function',
],
invalid: [
'const foo = bar => bar[0]',
'const foo = bar => bar[0] === firstElementOfBar',
'const foo = (bar) => bar[0]',
'const foo = (bar, {baz}) => bar[0] === baz',
'const foo = bar => bar[0b01]',
'const foo = bar => bar.length',
'const foo = bar => bar.baz'
]
});
85 changes: 85 additions & 0 deletions test/snapshots/prefer-destructuring-in-parameters.js.md
@@ -0,0 +1,85 @@
# Snapshot report for `test/prefer-destructuring-in-parameters.js`

The actual snapshot is saved in `prefer-destructuring-in-parameters.js.snap`.

Generated by [AVA](https://avajs.dev).

## Invalid #1
1 | const foo = bar => bar[0]

> Output
`␊
1 | const foo = ([firstElementOfBar]) => firstElementOfBar␊
`

> Error 1/1
`␊
> 1 | const foo = bar => bar[0]␊
| ^^^^^^ `bar[0]` should be destructed in parameter `bar`.␊
`

## Invalid #2
1 | const foo = bar => bar[0] === firstElementOfBar

> Output
`␊
1 | const foo = ([firstElementOfBar_]) => firstElementOfBar_ === firstElementOfBar␊
`

> Error 1/1
`␊
> 1 | const foo = bar => bar[0] === firstElementOfBar␊
| ^^^^^^ `bar[0]` should be destructed in parameter `bar`.␊
`

## Invalid #3
1 | const foo = bar => bar[0b01]

> Output
`␊
1 | const foo = ([, secondElementOfBar]) => secondElementOfBar␊
`

> Error 1/1
`␊
> 1 | const foo = bar => bar[0b01]␊
| ^^^^^^^^^ `bar[1]` should be destructed in parameter `bar`.␊
`

## Invalid #4
1 | const foo = bar => bar.length

> Output
`␊
1 | const foo = ({length}) => length␊
`

> Error 1/1
`␊
> 1 | const foo = bar => bar.length␊
| ^^^^^^^^^^ `bar.length` should be destructed in parameter `bar`.␊
`

## Invalid #5
1 | const foo = bar => bar.baz

> Output
`␊
1 | const foo = ({baz}) => baz␊
`

> Error 1/1
`␊
> 1 | const foo = bar => bar.baz␊
| ^^^^^^^ `bar.baz` should be destructed in parameter `bar`.␊
`
Binary file not shown.

0 comments on commit b9f5126

Please sign in to comment.