From 95f6bf4818cdec48a0583bf82f928c598af22736 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 5 Sep 2020 18:21:28 -0700 Subject: [PATCH] fix(scope-manager): correctly handle inferred types in nested type scopes (#2497) --- .../tests/rules/no-unused-vars.test.ts | 14 ++ .../src/referencer/TypeVisitor.ts | 39 +++++ .../fixtures/type-declaration/conditional3.ts | 1 + .../type-declaration/conditional3.ts.shot | 131 +++++++++++++++ .../fixtures/type-declaration/conditional4.ts | 1 + .../type-declaration/conditional4.ts.shot | 106 ++++++++++++ .../fixtures/type-declaration/conditional5.ts | 3 + .../type-declaration/conditional5.ts.shot | 156 ++++++++++++++++++ 8 files changed, 451 insertions(+) create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts create mode 100644 packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts.shot diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts index 5dd485d12e3..bac969579d9 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts @@ -789,6 +789,20 @@ declare const [v6]; `, filename: 'foo.d.ts', }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2459 + ` +export type Test = U extends (k: infer I) => void ? I : never; + `, + ` +export type Test = U extends { [k: string]: infer I } ? I : never; + `, + ` +export type Test = U extends (arg: { + [k: string]: (arg2: infer I) => void; +}) => void + ? I + : never; + `, ], invalid: [ diff --git a/packages/scope-manager/src/referencer/TypeVisitor.ts b/packages/scope-manager/src/referencer/TypeVisitor.ts index d921529ac82..5e43f83bb32 100644 --- a/packages/scope-manager/src/referencer/TypeVisitor.ts +++ b/packages/scope-manager/src/referencer/TypeVisitor.ts @@ -2,6 +2,7 @@ import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/types'; import { Referencer } from './Referencer'; import { Visitor } from './Visitor'; import { ParameterDefinition, TypeDefinition } from '../definition'; +import { ScopeType } from '../scope'; class TypeVisitor extends Visitor { readonly #referencer: Referencer; @@ -121,6 +122,44 @@ class TypeVisitor extends Visitor { this.visit(node.typeAnnotation); } + protected TSInferType(node: TSESTree.TSInferType): void { + const typeParameter = node.typeParameter; + let scope = this.#referencer.currentScope(); + + /* + In cases where there is a sub-type scope created within a conditional type, then the generic should be defined in the + conditional type's scope, not the child type scope. + If we define it within the child type's scope then it won't be able to be referenced outside the child type + */ + if ( + scope.type === ScopeType.functionType || + scope.type === ScopeType.mappedType + ) { + // search up the scope tree to figure out if we're in a nested type scope + let currentScope = scope.upper; + while (currentScope) { + if ( + currentScope.type === ScopeType.functionType || + currentScope.type === ScopeType.mappedType + ) { + // ensure valid type parents only + currentScope = currentScope.upper; + continue; + } + if (currentScope.type === ScopeType.conditionalType) { + scope = currentScope; + break; + } + break; + } + } + + scope.defineIdentifier( + typeParameter.name, + new TypeDefinition(typeParameter.name, typeParameter), + ); + } + protected TSInterfaceDeclaration( node: TSESTree.TSInterfaceDeclaration, ): void { diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts b/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts new file mode 100644 index 00000000000..a06e6959b43 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts @@ -0,0 +1 @@ +type Test = U extends (k: infer I) => void ? I : never; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts.shot b/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts.shot new file mode 100644 index 00000000000..12e493db419 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional3.ts.shot @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-declaration conditional3 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + TypeDefinition$1 { + name: Identifier<"Test">, + node: TSTypeAliasDeclaration$1, + }, + ], + name: "Test", + references: Array [], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + TypeDefinition$2 { + name: Identifier<"U">, + node: TSTypeParameter$2, + }, + ], + name: "U", + references: Array [ + Reference$1 { + identifier: Identifier<"U">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$3 { + defs: Array [ + ParameterDefinition$3 { + name: Identifier<"k">, + node: TSFunctionType$3, + }, + ], + name: "k", + references: Array [], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$4 { + defs: Array [ + TypeDefinition$4 { + name: Identifier<"I">, + node: TSTypeParameter$4, + }, + ], + name: "I", + references: Array [ + Reference$2 { + identifier: Identifier<"I">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$4, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$5, + isStrict: false, + references: Array [], + set: Map { + "Test" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + TypeScope$2 { + block: TSTypeAliasDeclaration$1, + isStrict: true, + references: Array [], + set: Map { + "U" => Variable$2, + }, + type: "type", + upper: GlobalScope$1, + variables: Array [ + Variable$2, + ], + }, + ConditionalTypeScope$3 { + block: TSConditionalType$6, + isStrict: true, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "I" => Variable$4, + }, + type: "conditionalType", + upper: TypeScope$2, + variables: Array [ + Variable$4, + ], + }, + FunctionTypeScope$4 { + block: TSFunctionType$3, + isStrict: true, + references: Array [], + set: Map { + "k" => Variable$3, + }, + type: "functionType", + upper: ConditionalTypeScope$3, + variables: Array [ + Variable$3, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts b/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts new file mode 100644 index 00000000000..01ca91d3215 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts @@ -0,0 +1 @@ +type Test = U extends { [k: string]: infer I } ? I : never; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts.shot b/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts.shot new file mode 100644 index 00000000000..13ba417eaf9 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional4.ts.shot @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-declaration conditional4 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + TypeDefinition$1 { + name: Identifier<"Test">, + node: TSTypeAliasDeclaration$1, + }, + ], + name: "Test", + references: Array [], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + TypeDefinition$2 { + name: Identifier<"U">, + node: TSTypeParameter$2, + }, + ], + name: "U", + references: Array [ + Reference$1 { + identifier: Identifier<"U">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$3 { + defs: Array [ + TypeDefinition$3 { + name: Identifier<"I">, + node: TSTypeParameter$3, + }, + ], + name: "I", + references: Array [ + Reference$2 { + identifier: Identifier<"I">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$3, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [], + set: Map { + "Test" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + TypeScope$2 { + block: TSTypeAliasDeclaration$1, + isStrict: true, + references: Array [], + set: Map { + "U" => Variable$2, + }, + type: "type", + upper: GlobalScope$1, + variables: Array [ + Variable$2, + ], + }, + ConditionalTypeScope$3 { + block: TSConditionalType$5, + isStrict: true, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "I" => Variable$3, + }, + type: "conditionalType", + upper: TypeScope$2, + variables: Array [ + Variable$3, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts b/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts new file mode 100644 index 00000000000..3d6a66f4d0f --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts @@ -0,0 +1,3 @@ +type Test = U extends (arg: { [k: string]: (arg2: infer I) => void }) => void + ? I + : never; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts.shot b/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts.shot new file mode 100644 index 00000000000..1102d182209 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/conditional5.ts.shot @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-declaration conditional5 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + TypeDefinition$1 { + name: Identifier<"Test">, + node: TSTypeAliasDeclaration$1, + }, + ], + name: "Test", + references: Array [], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + TypeDefinition$2 { + name: Identifier<"U">, + node: TSTypeParameter$2, + }, + ], + name: "U", + references: Array [ + Reference$1 { + identifier: Identifier<"U">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$3 { + defs: Array [ + ParameterDefinition$3 { + name: Identifier<"arg">, + node: TSFunctionType$3, + }, + ], + name: "arg", + references: Array [], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$4 { + defs: Array [ + ParameterDefinition$4 { + name: Identifier<"arg2">, + node: TSFunctionType$4, + }, + ], + name: "arg2", + references: Array [], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$5 { + defs: Array [ + TypeDefinition$5 { + name: Identifier<"I">, + node: TSTypeParameter$5, + }, + ], + name: "I", + references: Array [ + Reference$2 { + identifier: Identifier<"I">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$5, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$6, + isStrict: false, + references: Array [], + set: Map { + "Test" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + TypeScope$2 { + block: TSTypeAliasDeclaration$1, + isStrict: true, + references: Array [], + set: Map { + "U" => Variable$2, + }, + type: "type", + upper: GlobalScope$1, + variables: Array [ + Variable$2, + ], + }, + ConditionalTypeScope$3 { + block: TSConditionalType$7, + isStrict: true, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "I" => Variable$5, + }, + type: "conditionalType", + upper: TypeScope$2, + variables: Array [ + Variable$5, + ], + }, + FunctionTypeScope$4 { + block: TSFunctionType$3, + isStrict: true, + references: Array [], + set: Map { + "arg" => Variable$3, + }, + type: "functionType", + upper: ConditionalTypeScope$3, + variables: Array [ + Variable$3, + ], + }, + FunctionTypeScope$5 { + block: TSFunctionType$4, + isStrict: true, + references: Array [], + set: Map { + "arg2" => Variable$4, + }, + type: "functionType", + upper: FunctionTypeScope$4, + variables: Array [ + Variable$4, + ], + }, + ], +} +`;