-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): add new extended rule prefer-destructuring
#7117
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
Merged
JoshuaKGoldberg
merged 41 commits into
typescript-eslint:main
from
seiyab:prefer-destructuring
Oct 11, 2023
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
8e328f6
copy prefer-destructuring from ESLint
seiyab 392f132
rewrite schema by hand
seiyab 4435c44
add enforceForTypeAnnotatedProperties option
seiyab e5ab24f
add tests
seiyab 90e094f
prevent fixing type-annotated declaration
seiyab 1619def
add tests
seiyab 8bcb921
move baseTests into end of file
seiyab 47b9e6c
refactor
seiyab e997503
consider numeric properties of non-itreable objects for VariableDecla…
seiyab 375f99c
consider numeric properties of non-itreable objects for AssignmentEx…
seiyab 05680aa
fix a bug
seiyab a687993
fix a bug
seiyab 0fbc758
add "openjsf" into the dictionary
seiyab 7951154
add prefer-destructuring into all
seiyab d3626c6
add doc and minor tweak
seiyab 93c179c
fix typo
seiyab 5ebadbf
fix incorrect "correct" case
seiyab 5d1e5be
improve test coverage
seiyab 2d7e2af
improve test coverage
seiyab 7199034
Merge branch 'main' into prefer-destructuring
seiyab 854f1c1
Merge branch 'main' into prefer-destructuring
seiyab 2b92380
Merge branch 'main' into prefer-destructuring
seiyab 2b4fdc1
Merge branch 'main' into prefer-destructuring
JoshuaKGoldberg 521506c
fix: bring in adjustments from main branch
JoshuaKGoldberg 044d503
Updated snapshot
JoshuaKGoldberg 0102f80
Update packages/eslint-plugin/src/rules/prefer-destructuring.ts
seiyab b6a1840
rename a function
seiyab 4d69749
fix typo
seiyab b2de45b
reduce baseRule.create() calls
seiyab c0dbe8f
lazily run baseRule.create(noFixContext(context))
seiyab 7e0d976
lint
seiyab a1e24e4
add test cases
seiyab 67a18bc
add test cases
seiyab 6b4bebe
Merge branch 'main' into prefer-destructuring
seiyab 96cbada
remove tests copied from base rule
seiyab 4ca0e2a
add tests
seiyab 25f780d
add tests
seiyab 7451956
declare variables
seiyab 6ca18e1
minor improvements
seiyab 3c23be8
improve type and coverage
seiyab 77b6704
Merge branch 'main' into prefer-destructuring
seiyab File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,91 @@ | ||
--- | ||
description: 'Require destructuring from arrays and/or objects.' | ||
--- | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/prefer-destructuring** for documentation. | ||
|
||
## Examples | ||
|
||
This rule extends the base [`eslint/prefer-destructuring`](https://eslint.org/docs/latest/rules/prefer-destructuring) rule. | ||
It adds support for TypeScript's type annotations in variable declarations. | ||
|
||
<!-- tabs --> | ||
|
||
### `eslint/prefer-destructuring` | ||
|
||
```ts | ||
const x: string = obj.x; // This is incorrect and the auto fixer provides following untyped fix. | ||
// const { x } = obj; | ||
``` | ||
|
||
### `@typescript-eslint/prefer-destructuring` | ||
|
||
```ts | ||
const x: string = obj.x; // This is correct by default. You can also forbid this by an option. | ||
``` | ||
|
||
<!-- /tabs --> | ||
|
||
And it infers binding patterns more accurately thanks to the type checker. | ||
|
||
<!-- tabs --> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
const x = ['a']; | ||
const y = x[0]; | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
const x = { 0: 'a' }; | ||
const y = x[0]; | ||
``` | ||
|
||
It is correct when `enforceForRenamedProperties` is not true. | ||
Valid destructuring syntax is renamed style like `{ 0: y } = x` rather than `[y] = x` because `x` is not iterable. | ||
|
||
## Options | ||
|
||
This rule adds the following options: | ||
|
||
```ts | ||
type Options = [ | ||
BasePreferDestructuringOptions[0], | ||
BasePreferDestructuringOptions[1] & { | ||
enforceForDeclarationWithTypeAnnotation?: boolean; | ||
}, | ||
]; | ||
|
||
const defaultOptions: Options = [ | ||
basePreferDestructuringDefaultOptions[0], | ||
{ | ||
...basePreferDestructuringDefaultOptions[1], | ||
enforceForDeclarationWithTypeAnnotation: false, | ||
}, | ||
]; | ||
``` | ||
|
||
### `enforceForDeclarationWithTypeAnnotation` | ||
|
||
When set to `true`, type annotated variable declarations are enforced to use destructuring assignment. | ||
|
||
Examples with `{ enforceForDeclarationWithTypeAnnotation: true }`: | ||
|
||
<!--tabs--> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
const x: string = obj.x; | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
const { x }: { x: string } = obj; | ||
``` |
This file contains hidden or 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 hidden or 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
237 changes: 237 additions & 0 deletions
237
packages/eslint-plugin/src/rules/prefer-destructuring.ts
This file contains hidden or 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,237 @@ | ||
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; | ||
import * as tsutils from 'ts-api-utils'; | ||
import type * as ts from 'typescript'; | ||
|
||
import type { | ||
InferMessageIdsTypeFromRule, | ||
InferOptionsTypeFromRule, | ||
} from '../util'; | ||
import { createRule, getParserServices, isTypeAnyType } from '../util'; | ||
import { getESLintCoreRule } from '../util/getESLintCoreRule'; | ||
|
||
const baseRule = getESLintCoreRule('prefer-destructuring'); | ||
|
||
type BaseOptions = InferOptionsTypeFromRule<typeof baseRule>; | ||
type EnforcementOptions = BaseOptions[1] & { | ||
enforceForDeclarationWithTypeAnnotation?: boolean; | ||
}; | ||
type Options = [BaseOptions[0], EnforcementOptions]; | ||
|
||
type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>; | ||
|
||
const destructuringTypeConfig: JSONSchema4 = { | ||
type: 'object', | ||
properties: { | ||
array: { | ||
type: 'boolean', | ||
}, | ||
object: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}; | ||
|
||
const schema: readonly JSONSchema4[] = [ | ||
{ | ||
oneOf: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
VariableDeclarator: destructuringTypeConfig, | ||
AssignmentExpression: destructuringTypeConfig, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
destructuringTypeConfig, | ||
], | ||
}, | ||
{ | ||
type: 'object', | ||
properties: { | ||
enforceForRenamedProperties: { | ||
type: 'boolean', | ||
}, | ||
enforceForDeclarationWithTypeAnnotation: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
}, | ||
]; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: 'prefer-destructuring', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Require destructuring from arrays and/or objects', | ||
extendsBaseRule: true, | ||
requiresTypeChecking: true, | ||
}, | ||
schema, | ||
fixable: baseRule.meta.fixable, | ||
hasSuggestions: baseRule.meta.hasSuggestions, | ||
messages: baseRule.meta.messages, | ||
}, | ||
defaultOptions: [ | ||
{ | ||
VariableDeclarator: { | ||
array: true, | ||
object: true, | ||
}, | ||
AssignmentExpression: { | ||
array: true, | ||
object: true, | ||
}, | ||
}, | ||
{}, | ||
], | ||
create(context, [enabledTypes, options]) { | ||
const { | ||
enforceForRenamedProperties = false, | ||
enforceForDeclarationWithTypeAnnotation = false, | ||
} = options; | ||
const { program, esTreeNodeToTSNodeMap } = getParserServices(context); | ||
const typeChecker = program.getTypeChecker(); | ||
const baseRules = baseRule.create(context); | ||
let baseRulesWithoutFixCache: typeof baseRules | null = null; | ||
|
||
return { | ||
VariableDeclarator(node): void { | ||
performCheck(node.id, node.init, node); | ||
}, | ||
AssignmentExpression(node): void { | ||
if (node.operator !== '=') { | ||
return; | ||
} | ||
performCheck(node.left, node.right, node); | ||
}, | ||
}; | ||
|
||
function performCheck( | ||
leftNode: TSESTree.BindingName | TSESTree.Expression, | ||
rightNode: TSESTree.Expression | null, | ||
reportNode: TSESTree.VariableDeclarator | TSESTree.AssignmentExpression, | ||
): void { | ||
const rules = | ||
leftNode.type === AST_NODE_TYPES.Identifier && | ||
leftNode.typeAnnotation === undefined | ||
? baseRules | ||
: baseRulesWithoutFix(); | ||
if ( | ||
'typeAnnotation' in leftNode && | ||
leftNode.typeAnnotation !== undefined && | ||
!enforceForDeclarationWithTypeAnnotation | ||
) { | ||
return; | ||
} | ||
|
||
if ( | ||
rightNode != null && | ||
isArrayLiteralIntegerIndexAccess(rightNode) && | ||
rightNode.object.type !== AST_NODE_TYPES.Super | ||
) { | ||
const tsObj = esTreeNodeToTSNodeMap.get(rightNode.object); | ||
const objType = typeChecker.getTypeAtLocation(tsObj); | ||
if (!isTypeAnyOrIterableType(objType, typeChecker)) { | ||
if ( | ||
!enforceForRenamedProperties || | ||
!getNormalizedEnabledType(reportNode.type, 'object') | ||
) { | ||
return; | ||
} | ||
context.report({ | ||
node: reportNode, | ||
messageId: 'preferDestructuring', | ||
data: { type: 'object' }, | ||
}); | ||
return; | ||
} | ||
} | ||
|
||
if (reportNode.type === AST_NODE_TYPES.AssignmentExpression) { | ||
rules.AssignmentExpression(reportNode); | ||
} else { | ||
rules.VariableDeclarator(reportNode); | ||
} | ||
} | ||
|
||
function getNormalizedEnabledType( | ||
nodeType: | ||
| AST_NODE_TYPES.VariableDeclarator | ||
| AST_NODE_TYPES.AssignmentExpression, | ||
destructuringType: 'array' | 'object', | ||
): boolean | undefined { | ||
if ('object' in enabledTypes || 'array' in enabledTypes) { | ||
return enabledTypes[destructuringType]; | ||
} | ||
return enabledTypes[nodeType as keyof typeof enabledTypes][ | ||
destructuringType as keyof (typeof enabledTypes)[keyof typeof enabledTypes] | ||
]; | ||
} | ||
|
||
function baseRulesWithoutFix(): ReturnType<typeof baseRule.create> { | ||
baseRulesWithoutFixCache ??= baseRule.create(noFixContext(context)); | ||
return baseRulesWithoutFixCache; | ||
} | ||
}, | ||
}); | ||
|
||
type Context = TSESLint.RuleContext<MessageIds, Options>; | ||
|
||
function noFixContext(context: Context): Context { | ||
const customContext: { | ||
report: Context['report']; | ||
} = { | ||
report: (descriptor): void => { | ||
context.report({ | ||
...descriptor, | ||
fix: undefined, | ||
}); | ||
}, | ||
}; | ||
|
||
// we can't directly proxy `context` because its `report` property is non-configurable | ||
// and non-writable. So we proxy `customContext` and redirect all | ||
// property access to the original context except for `report` | ||
return new Proxy<Context>(customContext as typeof context, { | ||
get(target, path, receiver): unknown { | ||
if (path !== 'report') { | ||
return Reflect.get(context, path, receiver); | ||
} | ||
return Reflect.get(target, path, receiver); | ||
}, | ||
}); | ||
} | ||
|
||
function isTypeAnyOrIterableType( | ||
type: ts.Type, | ||
typeChecker: ts.TypeChecker, | ||
): boolean { | ||
if (isTypeAnyType(type)) { | ||
return true; | ||
} | ||
if (!type.isUnion()) { | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const iterator = tsutils.getWellKnownSymbolPropertyOfType( | ||
type, | ||
'iterator', | ||
typeChecker, | ||
); | ||
return iterator !== undefined; | ||
} | ||
return type.types.every(t => isTypeAnyOrIterableType(t, typeChecker)); | ||
} | ||
|
||
function isArrayLiteralIntegerIndexAccess( | ||
node: TSESTree.Expression, | ||
): node is TSESTree.MemberExpression { | ||
if (node.type !== AST_NODE_TYPES.MemberExpression) { | ||
return false; | ||
} | ||
if (node.property.type !== AST_NODE_TYPES.Literal) { | ||
return false; | ||
} | ||
return Number.isInteger(node.property.value); | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
This file contains hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.