Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
consistent-destructuring
rule (#325)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: fisker <lionkay@gmail.com>
- Loading branch information
1 parent
7392174
commit 32bd31c
Showing
18 changed files
with
779 additions
and
77 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,52 @@ | ||
# Use destructured variables over properties | ||
|
||
Enforces the use of already destructured objects and their variables over accessing each property individually. Previous destructurings are easily missed which leads to an inconsistent code style. | ||
|
||
This rule is partly fixable. It does not fix nested destructuring. | ||
|
||
## Fail | ||
|
||
```js | ||
const {a} = foo; | ||
console.log(a, foo.b); | ||
``` | ||
|
||
```js | ||
const {a} = foo; | ||
console.log(foo.a); | ||
``` | ||
|
||
```js | ||
const { | ||
a: { | ||
b | ||
} | ||
} = foo; | ||
console.log(foo.a.c); | ||
``` | ||
|
||
```js | ||
const {bar} = foo; | ||
const {a} = foo.bar; | ||
``` | ||
|
||
## Pass | ||
|
||
```js | ||
const {a} = foo; | ||
console.log(a); | ||
``` | ||
|
||
```js | ||
console.log(foo.a, foo.b); | ||
``` | ||
|
||
```js | ||
const {a} = foo; | ||
console.log(a, foo.b()); | ||
``` | ||
|
||
```js | ||
const {a} = foo.bar; | ||
console.log(foo.bar); | ||
``` |
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,169 @@ | ||
'use strict'; | ||
const avoidCapture = require('./utils/avoid-capture'); | ||
const getDocumentationUrl = require('./utils/get-documentation-url'); | ||
|
||
const MESSAGE_ID = 'consistentDestructuring'; | ||
const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest'; | ||
|
||
const declaratorSelector = [ | ||
'VariableDeclarator', | ||
'[id.type="ObjectPattern"]', | ||
'[init]', | ||
'[init.type!="Literal"]' | ||
].join(''); | ||
|
||
const memberSelector = [ | ||
'MemberExpression', | ||
'[computed=false]', | ||
':not(', | ||
'AssignmentExpression > MemberExpression.left,', | ||
'CallExpression > MemberExpression.callee,', | ||
'NewExpression > MemberExpression.callee,', | ||
'UpdateExpression > MemberExpression.argument,', | ||
'UnaryExpression[operator="delete"] > MemberExpression.argument', | ||
')' | ||
].join(''); | ||
|
||
const isSimpleExpression = expression => { | ||
while (expression) { | ||
if (expression.computed) { | ||
return false; | ||
} | ||
|
||
if (expression.type !== 'MemberExpression') { | ||
break; | ||
} | ||
|
||
expression = expression.object; | ||
} | ||
|
||
return expression.type === 'Identifier' || | ||
expression.type === 'ThisExpression'; | ||
}; | ||
|
||
const isChildInParentScope = (child, parent) => { | ||
while (child) { | ||
if (child === parent) { | ||
return true; | ||
} | ||
|
||
child = child.upper; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
const create = context => { | ||
const {ecmaVersion} = context.parserOptions; | ||
const source = context.getSourceCode(); | ||
const declarations = new Map(); | ||
|
||
return { | ||
[declaratorSelector]: node => { | ||
// Ignore any complex expressions (e.g. arrays, functions) | ||
if (!isSimpleExpression(node.init)) { | ||
return; | ||
} | ||
|
||
declarations.set(source.getText(node.init), { | ||
scope: context.getScope(), | ||
variables: context.getDeclaredVariables(node), | ||
objectPattern: node.id | ||
}); | ||
}, | ||
[memberSelector]: node => { | ||
const declaration = declarations.get(source.getText(node.object)); | ||
|
||
if (!declaration) { | ||
return; | ||
} | ||
|
||
const {scope, objectPattern} = declaration; | ||
const memberScope = context.getScope(); | ||
|
||
// Property is destructured outside the current scope | ||
if (!isChildInParentScope(memberScope, scope)) { | ||
return; | ||
} | ||
|
||
const destructurings = objectPattern.properties.filter(property => | ||
property.type === 'Property' && | ||
property.key.type === 'Identifier' && | ||
property.value.type === 'Identifier' | ||
); | ||
const lastProperty = objectPattern.properties[objectPattern.properties.length - 1]; | ||
const hasRest = lastProperty && lastProperty.type === 'RestElement'; | ||
|
||
const expression = source.getText(node); | ||
const member = source.getText(node.property); | ||
|
||
// Member might already be destructured | ||
const destructuredMember = destructurings.find(property => | ||
property.key.name === member | ||
); | ||
|
||
if (!destructuredMember) { | ||
// Don't destructure additional members when rest is used | ||
if (hasRest) { | ||
return; | ||
} | ||
|
||
// Destructured member collides with an existing identifier | ||
if (avoidCapture(member, [memberScope], ecmaVersion) !== member) { | ||
return; | ||
} | ||
} | ||
|
||
// Don't try to fix nested member expressions | ||
if (node.parent.type === 'MemberExpression') { | ||
context.report({ | ||
node, | ||
messageId: MESSAGE_ID | ||
}); | ||
|
||
return; | ||
} | ||
|
||
const newMember = destructuredMember ? destructuredMember.value.name : member; | ||
|
||
context.report({ | ||
node, | ||
messageId: MESSAGE_ID, | ||
suggest: [{ | ||
messageId: MESSAGE_ID_SUGGEST, | ||
data: { | ||
expression, | ||
property: newMember | ||
}, | ||
* fix(fixer) { | ||
const {properties} = objectPattern; | ||
const lastProperty = properties[properties.length - 1]; | ||
|
||
yield fixer.replaceText(node, newMember); | ||
|
||
if (!destructuredMember) { | ||
yield lastProperty ? | ||
fixer.insertTextAfter(lastProperty, `, ${newMember}`) : | ||
fixer.replaceText(objectPattern, `{${newMember}}`); | ||
} | ||
} | ||
}] | ||
}); | ||
} | ||
}; | ||
}; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocumentationUrl(__filename) | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
[MESSAGE_ID]: 'Use destructured variables over properties.', | ||
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.' | ||
} | ||
} | ||
}; |
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
Oops, something went wrong.