diff --git a/packages/eslint-plugin/docs/rules/prefer-destructuring.md b/packages/eslint-plugin/docs/rules/prefer-destructuring.md new file mode 100644 index 00000000000..5980b81dbae --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-destructuring.md @@ -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. + + + +### `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. +``` + + + +And it infers binding patterns more accurately thanks to the type checker. + + + +### ❌ 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 }`: + + + +### ❌ Incorrect + +```ts +const x: string = obj.x; +``` + +### ✅ Correct + +```ts +const { x }: { x: string } = obj; +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index d0bd265b099..bb3873bc344 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -141,6 +141,8 @@ export = { '@typescript-eslint/parameter-properties': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-enum-initializers': 'error', + 'prefer-destructuring': 'off', + '@typescript-eslint/prefer-destructuring': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-includes': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 44aedd6198e..0cc9c880759 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -97,6 +97,7 @@ import objectCurlySpacing from './object-curly-spacing'; import paddingLineBetweenStatements from './padding-line-between-statements'; import parameterProperties from './parameter-properties'; import preferAsConst from './prefer-as-const'; +import preferDestructuring from './prefer-destructuring'; import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; @@ -232,6 +233,7 @@ export default { 'padding-line-between-statements': paddingLineBetweenStatements, 'parameter-properties': parameterProperties, 'prefer-as-const': preferAsConst, + 'prefer-destructuring': preferDestructuring, 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, 'prefer-function-type': preferFunctionType, diff --git a/packages/eslint-plugin/src/rules/prefer-destructuring.ts b/packages/eslint-plugin/src/rules/prefer-destructuring.ts new file mode 100644 index 00000000000..41a4db15254 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-destructuring.ts @@ -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; +type EnforcementOptions = BaseOptions[1] & { + enforceForDeclarationWithTypeAnnotation?: boolean; +}; +type Options = [BaseOptions[0], EnforcementOptions]; + +type MessageIds = InferMessageIdsTypeFromRule; + +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({ + 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 { + baseRulesWithoutFixCache ??= baseRule.create(noFixContext(context)); + return baseRulesWithoutFixCache; + } + }, +}); + +type Context = TSESLint.RuleContext; + +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(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()) { + 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); +} diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index 30de8347c0d..6cfa0db393e 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -34,6 +34,7 @@ interface RuleMap { 'no-restricted-globals': typeof import('eslint/lib/rules/no-restricted-globals'); 'object-curly-spacing': typeof import('eslint/lib/rules/object-curly-spacing'); 'prefer-const': typeof import('eslint/lib/rules/prefer-const'); + 'prefer-destructuring': typeof import('eslint/lib/rules/prefer-destructuring'); quotes: typeof import('eslint/lib/rules/quotes'); semi: typeof import('eslint/lib/rules/semi'); 'space-before-blocks': typeof import('eslint/lib/rules/space-before-blocks'); diff --git a/packages/eslint-plugin/tests/rules/prefer-destructuring.test.ts b/packages/eslint-plugin/tests/rules/prefer-destructuring.test.ts new file mode 100644 index 00000000000..812d317b18c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-destructuring.test.ts @@ -0,0 +1,1064 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import rule from '../../src/rules/prefer-destructuring'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('prefer-destructuring', rule, { + valid: [ + // type annotated + ` + declare const object: { foo: string }; + var foo: string = object.foo; + `, + ` + declare const array: number[]; + const bar: number = array[0]; + `, + // enforceForDeclarationWithTypeAnnotation: true + { + code: ` + declare const object: { foo: string }; + var { foo } = object; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const object: { foo: string }; + var { foo }: { foo: number } = object; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const array: number[]; + var [foo] = array; + `, + options: [ + { array: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const array: number[]; + var [foo]: [foo: number] = array; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const object: { bar: string }; + var foo: unknown = object.bar; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const object: { foo: string }; + var { foo: bar } = object; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare const object: { foo: boolean }; + var { foo: bar }: { foo: boolean } = object; + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + declare class Foo { + foo: string; + } + + class Bar extends Foo { + static foo() { + var foo: any = super.foo; + } + } + `, + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + + // numeric property for iterable / non-iterable + ` + let x: { 0: unknown }; + let y = x[0]; + `, + ` + let x: { 0: unknown }; + y = x[0]; + `, + ` + let x: unknown; + let y = x[0]; + `, + ` + let x: unknown; + y = x[0]; + `, + ` + let x: { 0: unknown } | unknown[]; + let y = x[0]; + `, + ` + let x: { 0: unknown } | unknown[]; + y = x[0]; + `, + ` + let x: { 0: unknown } & (() => void); + let y = x[0]; + `, + ` + let x: { 0: unknown } & (() => void); + y = x[0]; + `, + ` + let x: Record; + let y = x[0]; + `, + ` + let x: Record; + y = x[0]; + `, + { + code: ` + let x: { 0: unknown }; + let { 0: y } = x; + `, + options: [ + { array: true, object: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: { 0: unknown }; + ({ 0: y } = x); + `, + options: [ + { array: true, object: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: { 0: unknown }; + let y = x[0]; + `, + options: [{ array: true }, { enforceForRenamedProperties: true }], + }, + { + code: ` + let x: { 0: unknown }; + y = x[0]; + `, + options: [{ array: true }, { enforceForRenamedProperties: true }], + }, + { + code: ` + let x: { 0: unknown }; + let y = x[0]; + `, + options: [ + { + VariableDeclarator: { array: true, object: false }, + AssignmentExpression: { array: true, object: true }, + }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: { 0: unknown }; + y = x[0]; + `, + options: [ + { + VariableDeclarator: { array: true, object: true }, + AssignmentExpression: { array: true, object: false }, + }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: Record; + let i: number = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: Record; + let i: 0 = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: Record; + let i: 0 | 1 | 2 = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: unknown[]; + let i: number = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: unknown[]; + let i: 0 = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: unknown[]; + let i: 0 | 1 | 2 = 0; + y = x[i]; + `, + options: [ + { object: false, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + let x: unknown[]; + let i: number = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: false }, + ], + }, + { + code: ` + let x: { 0: unknown }; + y += x[0]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + class Bar { + public [0]: unknown; + } + class Foo extends Bar { + static foo() { + let y = super[0]; + } + } + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + { + code: ` + class Bar { + public [0]: unknown; + } + class Foo extends Bar { + static foo() { + y = super[0]; + } + } + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + }, + + // already destructured + ` + let xs: unknown[] = [1]; + let [x] = xs; + `, + ` + const obj: { x: unknown } = { x: 1 }; + const { x } = obj; + `, + ` + var obj: { x: unknown } = { x: 1 }; + var { x: y } = obj; + `, + ` + let obj: { x: unknown } = { x: 1 }; + let key: 'x' = 'x'; + let { [key]: foo } = obj; + `, + ` + const obj: { x: unknown } = { x: 1 }; + let x: unknown; + ({ x } = obj); + `, + + // valid unless enforceForRenamedProperties is true + ` + let obj: { x: unknown } = { x: 1 }; + let y = obj.x; + `, + ` + var obj: { x: unknown } = { x: 1 }; + var y: unknown; + y = obj.x; + `, + ` + const obj: { x: unknown } = { x: 1 }; + const y = obj['x']; + `, + ` + let obj: Record = {}; + let key = 'abc'; + var y = obj[key]; + `, + + // shorthand operators shouldn't be reported; + ` + let obj: { x: number } = { x: 1 }; + let x = 10; + x += obj.x; + `, + ` + let obj: { x: boolean } = { x: false }; + let x = true; + x ||= obj.x; + `, + ` + const xs: number[] = [1]; + let x = 3; + x *= xs[0]; + `, + + // optional chaining shouldn't be reported + ` + let xs: unknown[] | undefined; + let x = xs?.[0]; + `, + ` + let obj: Record | undefined; + let x = obj?.x; + `, + + // private identifiers + ` + class C { + #foo: string; + + method() { + const foo: unknown = this.#foo; + } + } + `, + ` + class C { + #foo: string; + + method() { + let foo: unknown; + foo = this.#foo; + } + } + `, + { + code: ` + class C { + #foo: string; + + method() { + const bar: unknown = this.#foo; + } + } + `, + options: [ + { object: true, array: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + class C { + #foo: string; + + method(another: C) { + let bar: unknown; + bar: unknown = another.#foo; + } + } + `, + options: [ + { object: true, array: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + { + code: ` + class C { + #foo: string; + + method() { + const foo: unknown = this.#foo; + } + } + `, + options: [ + { object: true, array: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + }, + ], + invalid: [ + // enforceForDeclarationWithTypeAnnotation: true + { + code: 'var foo: string = object.foo;', + options: [ + { object: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + output: null, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'var foo: string = array[0];', + options: [ + { array: true }, + { enforceForDeclarationWithTypeAnnotation: true }, + ], + output: null, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'var foo: unknown = object.bar;', + options: [ + { object: true }, + { + enforceForDeclarationWithTypeAnnotation: true, + enforceForRenamedProperties: true, + }, + ], + output: null, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + + // numeric property for iterable / non-iterable + { + code: ` + let x: { [Symbol.iterator]: unknown }; + let y = x[0]; + `, + output: null, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: { [Symbol.iterator]: unknown }; + y = x[0]; + `, + output: null, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: [1, 2, 3]; + let y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: [1, 2, 3]; + y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + function* it() { + yield 1; + } + let y = it()[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + function* it() { + yield 1; + } + y = it()[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: any; + let y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: any; + y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: string[] | { [Symbol.iterator]: unknown }; + let y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: string[] | { [Symbol.iterator]: unknown }; + y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: object & unknown[]; + let y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: object & unknown[]; + y = x[0]; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'array' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: { 0: string }; + let y = x[0]; + `, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: { 0: string }; + y = x[0]; + `, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: { 0: string }; + let y = x[0]; + `, + options: [ + { + VariableDeclarator: { object: true, array: false }, + AssignmentExpression: { object: false, array: false }, + }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: { 0: string }; + y = x[0]; + `, + options: [ + { + VariableDeclarator: { object: false, array: false }, + AssignmentExpression: { object: true, array: false }, + }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: Record; + let i: number = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: Record; + let i: 0 = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: Record; + let i: 0 | 1 | 2 = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: unknown[]; + let i: number = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: unknown[]; + let i: 0 = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: unknown[]; + let i: 0 | 1 | 2 = 0; + y = x[i]; + `, + options: [ + { object: true, array: true }, + { enforceForRenamedProperties: true }, + ], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let x: { 0: unknown } | unknown[]; + let y = x[0]; + `, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let x: { 0: unknown } | unknown[]; + y = x[0]; + `, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + + // auto fixes + { + code: ` + let obj = { foo: 'bar' }; + const foo = obj.foo; + `, + output: ` + let obj = { foo: 'bar' }; + const {foo} = obj; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let obj = { foo: 'bar' }; + var x: null = null; + const foo = (x, obj).foo; + `, + output: ` + let obj = { foo: 'bar' }; + var x: null = null; + const {foo} = (x, obj); + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'const call = (() => null).call;', + output: 'const {call} = () => null;', + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + const obj = { foo: 'bar' }; + let a: any; + var foo = (a = obj).foo; + `, + output: ` + const obj = { foo: 'bar' }; + let a: any; + var {foo} = a = obj; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + const obj = { asdf: { qwer: null } }; + const qwer = obj.asdf.qwer; + `, + output: ` + const obj = { asdf: { qwer: null } }; + const {qwer} = obj.asdf; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + const obj = { foo: 100 }; + const /* comment */ foo = obj.foo; + `, + output: ` + const obj = { foo: 100 }; + const /* comment */ {foo} = obj; + `, + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + + // enforceForRenamedProperties: true + { + code: ` + let obj = { foo: 'bar' }; + const x = obj.foo; + `, + output: null, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let obj = { foo: 'bar' }; + let x: unknown; + x = obj.foo; + `, + output: null, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + { + code: ` + let obj: Record; + let key = 'abc'; + const x = obj[key]; + `, + output: null, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` + let obj: Record; + let key = 'abc'; + let x: unknown; + x = obj[key]; + `, + output: null, + options: [{ object: true }, { enforceForRenamedProperties: true }], + errors: [ + { + messageId: 'preferDestructuring', + data: { type: 'object' }, + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/prefer-destructuring.shot b/packages/eslint-plugin/tests/schema-snapshots/prefer-destructuring.shot new file mode 100644 index 00000000000..ca0e2597bf5 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/prefer-destructuring.shot @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes prefer-destructuring 1`] = ` +" +# SCHEMA: + +[ + { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "AssignmentExpression": { + "additionalProperties": false, + "properties": { + "array": { + "type": "boolean" + }, + "object": { + "type": "boolean" + } + }, + "type": "object" + }, + "VariableDeclarator": { + "additionalProperties": false, + "properties": { + "array": { + "type": "boolean" + }, + "object": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "array": { + "type": "boolean" + }, + "object": { + "type": "boolean" + } + }, + "type": "object" + } + ] + }, + { + "properties": { + "enforceForDeclarationWithTypeAnnotation": { + "type": "boolean" + }, + "enforceForRenamedProperties": { + "type": "boolean" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + ( + | { + AssignmentExpression?: { + array?: boolean; + object?: boolean; + }; + VariableDeclarator?: { + array?: boolean; + object?: boolean; + }; + } + | { + array?: boolean; + object?: boolean; + } + ), + { + enforceForDeclarationWithTypeAnnotation?: boolean; + enforceForRenamedProperties?: boolean; + [k: string]: unknown; + }, +]; +" +`; diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 295dd4d757c..dea1fcd6997 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -966,6 +966,33 @@ declare module 'eslint/lib/rules/prefer-const' { export = rule; } +declare module 'eslint/lib/rules/prefer-destructuring' { + import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + + interface DestructuringTypeConfig { + object?: boolean; + array?: boolean; + } + type Option0 = + | DestructuringTypeConfig + | { + VariableDeclarator?: DestructuringTypeConfig; + AssignmentExpression?: DestructuringTypeConfig; + }; + interface Option1 { + enforceForRenamedProperties?: boolean; + } + const rule: TSESLint.RuleModule< + 'preferDestructuring', + [Option0, Option1?], + { + VariableDeclarator(node: TSESTree.VariableDeclarator): void; + AssignmentExpression(node: TSESTree.AssignmentExpression): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/object-curly-spacing' { import type { TSESLint, TSESTree } from '@typescript-eslint/utils';