Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): add new extended ruleprefer-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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
JoshuaKGoldberg merged 41 commits intotypescript-eslint:mainfromseiyab:prefer-destructuringOct 11, 2023
+1,518 −0
Merged
Changes fromall commits
Commits
Show all changes
41 commits Select commitHold shift + click to select a range
8e328f6 copy prefer-destructuring from ESLint
seiyab392f132 rewrite schema by hand
seiyab4435c44 add enforceForTypeAnnotatedProperties option
seiyabe5ab24f add tests
seiyab90e094f prevent fixing type-annotated declaration
seiyab1619def add tests
seiyab8bcb921 move baseTests into end of file
seiyab47b9e6c refactor
seiyabe997503 consider numeric properties of non-itreable objects for VariableDecla…
seiyab375f99c consider numeric properties of non-itreable objects for AssignmentEx…
seiyab05680aa fix a bug
seiyaba687993 fix a bug
seiyab0fbc758 add "openjsf" into the dictionary
seiyab7951154 add prefer-destructuring into all
seiyabd3626c6 add doc and minor tweak
seiyab93c179c fix typo
seiyab5ebadbf fix incorrect "correct" case
seiyab5d1e5be improve test coverage
seiyab2d7e2af improve test coverage
seiyab7199034 Merge branch 'main' into prefer-destructuring
seiyab854f1c1 Merge branch 'main' into prefer-destructuring
seiyab2b92380 Merge branch 'main' into prefer-destructuring
seiyab2b4fdc1 Merge branch 'main' into prefer-destructuring
JoshuaKGoldberg521506c fix: bring in adjustments from main branch
JoshuaKGoldberg044d503 Updated snapshot
JoshuaKGoldberg0102f80 Update packages/eslint-plugin/src/rules/prefer-destructuring.ts
seiyabb6a1840 rename a function
seiyab4d69749 fix typo
seiyabb2de45b reduce baseRule.create() calls
seiyabc0dbe8f lazily run baseRule.create(noFixContext(context))
seiyab7e0d976 lint
seiyaba1e24e4 add test cases
seiyab67a18bc add test cases
seiyab6b4bebe Merge branch 'main' into prefer-destructuring
seiyab96cbada remove tests copied from base rule
seiyab4ca0e2a add tests
seiyab25f780d add tests
seiyab7451956 declare variables
seiyab6ca18e1 minor improvements
seiyab3c23be8 improve type and coverage
seiyab77b6704 Merge branch 'main' into prefer-destructuring
seiyabFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
91 changes: 91 additions & 0 deletionspackages/eslint-plugin/docs/rules/prefer-destructuring.md
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; | ||
| ``` |
2 changes: 2 additions & 0 deletionspackages/eslint-plugin/src/configs/all.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
2 changes: 2 additions & 0 deletionspackages/eslint-plugin/src/rules/index.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
237 changes: 237 additions & 0 deletionspackages/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; | ||
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| } | ||
| if (!type.isUnion()) { | ||
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| 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 resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| } | ||
1 change: 1 addition & 0 deletionspackages/eslint-plugin/src/util/getESLintCoreRule.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
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
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.