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 consistent-destructuring rule #325

Merged
merged 28 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
21d4637
Add `consistent-destructuring` rule
medusalix Jun 20, 2019
095b5d6
Fix tests
medusalix Jun 20, 2019
e4f402f
Add suggestions
medusalix Jun 21, 2019
fe83cb7
Fix scoping and already defined variables
medusalix Jul 2, 2019
59cd382
Fix tests
medusalix Jul 2, 2019
ed9fbe8
Implement suggestions
medusalix Mar 27, 2020
9db11e9
Update consistent-destructuring.md
sindresorhus Apr 14, 2020
408c960
Update ECMA version
medusalix Apr 14, 2020
3ba98ae
Use selector instead of manual check
medusalix Apr 16, 2020
74d2981
Ignore computed expressions
medusalix Apr 16, 2020
372c43e
Fix test formatting
medusalix Apr 16, 2020
308b41b
Fix rest destructuring
medusalix Apr 16, 2020
d125813
Only fix rest if member is already destructured
medusalix Apr 16, 2020
d84778e
Add test case with multiple suggestions
medusalix Apr 16, 2020
ccd05ac
Remove `ruleId` from tests
medusalix Jun 22, 2020
3073b0c
Fix function calls
medusalix Jun 30, 2020
8afbf13
Add actual message tests
medusalix Jun 30, 2020
7ff30fc
Add additional tests
medusalix Sep 27, 2020
380cf68
Ignore complex expressions
medusalix Sep 27, 2020
7cef746
Add `init` attribute null-check
medusalix Sep 27, 2020
ae5dfd9
Add some valid cases
fisker Oct 24, 2020
27111d0
Update tests
fisker Oct 24, 2020
0bb180e
Format tests
medusalix Dec 14, 2020
647c6ae
Ignore increment operator
medusalix Dec 14, 2020
7c057d6
Ignore new expressions
medusalix Dec 18, 2020
8fbecf7
Remove failing tests
medusalix Dec 18, 2020
351210c
Fix lint issues
medusalix Dec 23, 2020
7cb946b
Ignore identifier collisions
medusalix Dec 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/rules/consistent-destructuring.md
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);
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
rules: {
'unicorn/better-regex': 'error',
'unicorn/catch-error-name': 'error',
'unicorn/consistent-destructuring': 'error',
'unicorn/consistent-function-scoping': 'error',
'unicorn/custom-error-definition': 'off',
'unicorn/empty-brace-spaces': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Configure it in `package.json`.
"rules": {
"unicorn/better-regex": "error",
"unicorn/catch-error-name": "error",
"unicorn/consistent-destructuring": "error",
"unicorn/consistent-function-scoping": "error",
"unicorn/custom-error-definition": "off",
"unicorn/empty-brace-spaces": "error",
Expand Down Expand Up @@ -107,6 +108,7 @@ Configure it in `package.json`.

- [better-regex](docs/rules/better-regex.md) - Improve regexes by making them shorter, consistent, and safer. *(fixable)*
- [catch-error-name](docs/rules/catch-error-name.md) - Enforce a specific parameter name in catch clauses. *(fixable)*
- [consistent-destructuring](docs/rules/consistent-destructuring.md) - Use destructured variables over properties. *(partly fixable)*
- [consistent-function-scoping](docs/rules/consistent-function-scoping.md) - Move function definitions to the highest possible scope.
- [custom-error-definition](docs/rules/custom-error-definition.md) - Enforce correct `Error` subclassing. *(fixable)*
- [empty-brace-spaces](docs/rules/empty-brace-spaces.md) - Enforce no spaces between braces. *(fixable)*
Expand Down
169 changes: 169 additions & 0 deletions rules/consistent-destructuring.js
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,
medusalix marked this conversation as resolved.
Show resolved Hide resolved
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}}`.'
}
}
};
14 changes: 7 additions & 7 deletions rules/custom-error-definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ const hasValidSuperClass = node => {
return false;
}

let {name} = node.superClass;
let {name, type, property} = node.superClass;

if (node.superClass.type === 'MemberExpression') {
({name} = node.superClass.property);
if (type === 'MemberExpression') {
({name} = property);
}

return nameRegexp.test(name);
Expand Down Expand Up @@ -87,16 +87,16 @@ const customErrorDefinition = (context, node) => {
});
}

const {body} = node.body;
const {body, range} = node.body;
const constructor = body.find(x => x.kind === 'constructor');

if (!constructor) {
context.report({
node,
message: 'Add a constructor to your error.',
fix: fixer => fixer.insertTextAfterRange([
node.body.range[0],
node.body.range[0] + 1
range[0],
range[0] + 1
], getConstructorMethod(name))
});
return;
Expand Down Expand Up @@ -144,7 +144,7 @@ const customErrorDefinition = (context, node) => {

const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
if (!nameExpression) {
const nameProperty = node.body.body.find(node => isClassProperty(node, 'name'));
const nameProperty = body.find(node => isClassProperty(node, 'name'));

if (!nameProperty || !nameProperty.value || nameProperty.value.value !== name) {
context.report({
Expand Down
18 changes: 9 additions & 9 deletions rules/new-for-builtins.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ const disallowNew = new Set(builtins.disallowNew);
const create = context => {
return {
CallExpression: node => {
const {callee} = node;
const {callee, parent} = node;
const {name} = callee;

if (
name === 'Object' &&
node.parent &&
node.parent.type === 'BinaryExpression' &&
(node.parent.operator === '===' || node.parent.operator === '!==') &&
(node.parent.left === node || node.parent.right === node)
parent &&
parent.type === 'BinaryExpression' &&
(parent.operator === '===' || parent.operator === '!==') &&
(parent.left === node || parent.right === node)
) {
return;
}
Expand All @@ -37,8 +37,8 @@ const create = context => {
}
},
NewExpression: node => {
const {callee} = node;
const {name} = callee;
const {callee, range} = node;
const {name, range: calleeRange} = callee;

if (disallowNew.has(name) && !isShadowed(context.getScope(), callee)) {
const problem = {
Expand All @@ -49,8 +49,8 @@ const create = context => {

if (name !== 'String' && name !== 'Boolean' && name !== 'Number') {
problem.fix = fixer => fixer.removeRange([
node.range[0],
node.callee.range[0]
range[0],
calleeRange[0]
]);
}

Expand Down
4 changes: 2 additions & 2 deletions rules/no-for-loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ const getSingularName = originalName => {

const create = context => {
const sourceCode = context.getSourceCode();
const {scopeManager} = sourceCode;
const {scopeManager, text: sourceCodeText} = sourceCode;

return {
ForStatement(node) {
Expand Down Expand Up @@ -378,7 +378,7 @@ const create = context => {
if (removeDeclaration) {
declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;
if (elementNode.id.typeAnnotation && shouldGenerateIndex) {
declarationElement = sourceCode.text.slice(elementNode.id.range[0], elementNode.id.typeAnnotation.range[0]).trim();
declarationElement = sourceCodeText.slice(elementNode.id.range[0], elementNode.id.typeAnnotation.range[0]).trim();
typeAnnotation = sourceCode.getText(
elementNode.id.typeAnnotation,
-1 // Skip leading `:`
Expand Down