Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
prefer-destructuring-in-parameters
rule
- Loading branch information
Showing
7 changed files
with
317 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = '🦄'; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' | ||
] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.