diff --git a/packages/ast-spec/src/unions/EntityName.ts b/packages/ast-spec/src/unions/EntityName.ts index 82f7b1c9b0c..8a7364bef6b 100644 --- a/packages/ast-spec/src/unions/EntityName.ts +++ b/packages/ast-spec/src/unions/EntityName.ts @@ -1,4 +1,5 @@ import type { Identifier } from '../expression/Identifier/spec'; +import type { ThisExpression } from '../expression/ThisExpression/spec'; import type { TSQualifiedName } from '../type/TSQualifiedName/spec'; -export type EntityName = Identifier | TSQualifiedName; +export type EntityName = Identifier | ThisExpression | TSQualifiedName; diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 9cff63994d3..3335e04e741 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -248,6 +248,16 @@ class Foo {} ` export type AppState = typeof import('./src/store/reducers').default; `, + ` +let self: typeof this; +let foo: typeof this.foo; +const obj = { + foo: '', + bar() { + let self: typeof this; + }, +}; + `, ], invalid: [ { diff --git a/packages/scope-manager/src/referencer/ClassVisitor.ts b/packages/scope-manager/src/referencer/ClassVisitor.ts index 6f505044213..92dab53ca90 100644 --- a/packages/scope-manager/src/referencer/ClassVisitor.ts +++ b/packages/scope-manager/src/referencer/ClassVisitor.ts @@ -293,7 +293,7 @@ class ClassVisitor extends Visitor { node.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && this.#emitDecoratorMetadata ) { - let identifier: TSESTree.Identifier; + let entityName: TSESTree.Identifier | TSESTree.ThisExpression; if ( node.typeAnnotation.typeName.type === AST_NODE_TYPES.TSQualifiedName ) { @@ -301,13 +301,15 @@ class ClassVisitor extends Visitor { while (iter.left.type === AST_NODE_TYPES.TSQualifiedName) { iter = iter.left; } - identifier = iter.left; + entityName = iter.left; } else { - identifier = node.typeAnnotation.typeName; + entityName = node.typeAnnotation.typeName; } if (withDecorators) { - this.#referencer.currentScope().referenceDualValueType(identifier); + if (entityName.type === AST_NODE_TYPES.Identifier) { + this.#referencer.currentScope().referenceDualValueType(entityName); + } if (node.typeAnnotation.typeParameters) { this.visitType(node.typeAnnotation.typeParameters); diff --git a/packages/scope-manager/src/referencer/TypeVisitor.ts b/packages/scope-manager/src/referencer/TypeVisitor.ts index 0d345a149a2..4b39676850d 100644 --- a/packages/scope-manager/src/referencer/TypeVisitor.ts +++ b/packages/scope-manager/src/referencer/TypeVisitor.ts @@ -256,15 +256,20 @@ class TypeVisitor extends Visitor { // a type query `typeof foo` is a special case that references a _non-type_ variable, protected TSTypeQuery(node: TSESTree.TSTypeQuery): void { - if (node.exprName.type === AST_NODE_TYPES.Identifier) { - this.#referencer.currentScope().referenceValue(node.exprName); - } else { - let expr = node.exprName.left; - while (expr.type !== AST_NODE_TYPES.Identifier) { - expr = expr.left; + let entityName: TSESTree.Identifier | TSESTree.ThisExpression; + if (node.exprName.type === AST_NODE_TYPES.TSQualifiedName) { + let iter = node.exprName; + while (iter.left.type === AST_NODE_TYPES.TSQualifiedName) { + iter = iter.left; } - this.#referencer.currentScope().referenceValue(expr); + entityName = iter.left; + } else { + entityName = node.exprName; } + if (entityName.type === AST_NODE_TYPES.Identifier) { + this.#referencer.currentScope().referenceValue(entityName); + } + this.visit(node.typeParameters); } diff --git a/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts b/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts new file mode 100644 index 00000000000..1b68d92331b --- /dev/null +++ b/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts @@ -0,0 +1,5 @@ +function decorator() {} +@decorator +class Foo { + bar(baz: typeof this) {} +} diff --git a/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts.shot b/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts.shot new file mode 100644 index 00000000000..39c4468e53c --- /dev/null +++ b/packages/scope-manager/tests/fixtures/decorators/typeof-this.ts.shot @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`decorators typeof-this 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + FunctionNameDefinition$1 { + name: Identifier<"decorator">, + node: FunctionDeclaration$1, + }, + ], + name: "decorator", + references: Array [ + Reference$1 { + identifier: Identifier<"decorator">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$3 { + defs: Array [], + name: "arguments", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$4 { + defs: Array [ + ClassNameDefinition$2 { + name: Identifier<"Foo">, + node: ClassDeclaration$2, + }, + ], + name: "Foo", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$5 { + defs: Array [ + ClassNameDefinition$3 { + name: Identifier<"Foo">, + node: ClassDeclaration$2, + }, + ], + name: "Foo", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$6 { + defs: Array [], + name: "arguments", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$7 { + defs: Array [ + ParameterDefinition$4 { + name: Identifier<"baz">, + node: FunctionExpression$3, + }, + ], + name: "baz", + references: Array [], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "decorator" => Variable$2, + "Foo" => Variable$4, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + Variable$4, + ], + }, + FunctionScope$2 { + block: FunctionDeclaration$1, + isStrict: false, + references: Array [], + set: Map { + "arguments" => Variable$3, + }, + type: "function", + upper: GlobalScope$1, + variables: Array [ + Variable$3, + ], + }, + ClassScope$3 { + block: ClassDeclaration$2, + isStrict: true, + references: Array [], + set: Map { + "Foo" => Variable$5, + }, + type: "class", + upper: GlobalScope$1, + variables: Array [ + Variable$5, + ], + }, + FunctionScope$4 { + block: FunctionExpression$3, + isStrict: true, + references: Array [], + set: Map { + "arguments" => Variable$6, + "baz" => Variable$7, + }, + type: "function", + upper: ClassScope$3, + variables: Array [ + Variable$6, + Variable$7, + ], + }, + ], +} +`; diff --git a/packages/shared-fixtures/fixtures/typescript/types/typeof-this.src.ts b/packages/shared-fixtures/fixtures/typescript/types/typeof-this.src.ts new file mode 100644 index 00000000000..6cad5147a8a --- /dev/null +++ b/packages/shared-fixtures/fixtures/typescript/types/typeof-this.src.ts @@ -0,0 +1,2 @@ +let self: typeof this; +let foo: typeof this.foo; diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 864f85d2cd2..be09a2b387d 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -20,6 +20,7 @@ import { isComputedProperty, isESTreeClassMember, isOptional, + isThisInTypeQuery, TSError, unescapeStringLiteralText, } from './node-utils'; @@ -799,6 +800,13 @@ export class Converter { } case SyntaxKind.Identifier: { + if (isThisInTypeQuery(node)) { + // special case for `typeof this.foo` - TS emits an Identifier for `this` + // but we want to treat it as a ThisExpression for consistency + return this.createNode(node, { + type: AST_NODE_TYPES.ThisExpression, + }); + } return this.createNode(node, { type: AST_NODE_TYPES.Identifier, name: node.text, diff --git a/packages/typescript-estree/src/node-utils.ts b/packages/typescript-estree/src/node-utils.ts index 132971f6632..1f7b75651d5 100644 --- a/packages/typescript-estree/src/node-utils.ts +++ b/packages/typescript-estree/src/node-utils.ts @@ -662,3 +662,29 @@ export function firstDefined( } return undefined; } + +export function identifierIsThisKeyword(id: ts.Identifier): boolean { + return id.originalKeywordKind === SyntaxKind.ThisKeyword; +} + +export function isThisIdentifier( + node: ts.Node | undefined, +): node is ts.Identifier { + return ( + !!node && + node.kind === SyntaxKind.Identifier && + identifierIsThisKeyword(node as ts.Identifier) + ); +} + +export function isThisInTypeQuery(node: ts.Node): boolean { + if (!isThisIdentifier(node)) { + return false; + } + + while (ts.isQualifiedName(node.parent) && node.parent.left === node) { + node = node.parent; + } + + return node.parent.kind === SyntaxKind.TypeQuery; +} diff --git a/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts b/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts index 666909b5a71..82d5ace2c2a 100644 --- a/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts +++ b/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts @@ -149,7 +149,10 @@ export interface EstreeToTsNodeTypes { [AST_NODE_TYPES.TemplateLiteral]: | ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression; - [AST_NODE_TYPES.ThisExpression]: ts.ThisExpression | ts.KeywordTypeNode; + [AST_NODE_TYPES.ThisExpression]: + | ts.ThisExpression + | ts.KeywordTypeNode + | ts.Identifier; [AST_NODE_TYPES.ThrowStatement]: ts.ThrowStatement; [AST_NODE_TYPES.TryStatement]: ts.TryStatement; [AST_NODE_TYPES.TSAbstractPropertyDefinition]: ts.PropertyDeclaration; diff --git a/packages/typescript-estree/tests/ast-alignment/utils.ts b/packages/typescript-estree/tests/ast-alignment/utils.ts index e74d1daf3ff..f8c43458708 100644 --- a/packages/typescript-estree/tests/ast-alignment/utils.ts +++ b/packages/typescript-estree/tests/ast-alignment/utils.ts @@ -1,6 +1,6 @@ // babel types are something we don't really care about /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-plus-operands */ -import type { File, Program } from '@babel/types'; +import type { File, Identifier, Program, TSTypeQuery } from '@babel/types'; import { AST_NODE_TYPES, TSESTree } from '../../src/ts-estree'; import { deeplyCopy, omitDeep } from '../../tools/test-utils'; @@ -295,6 +295,30 @@ export function preprocessBabylonAST(ast: File): any { delete node.loc.start.index; } }, + /** + * ts-estree: `this` in `typeof this` has been converted from `Identifier` to `ThisExpression` + * @see https://github.com/typescript-eslint/typescript-eslint/pull/4382 + */ + TSTypeQuery(node: any) { + const { exprName } = node as TSTypeQuery; + let identifier: Identifier; + if (exprName.type === AST_NODE_TYPES.TSImportType) { + return; + } else if (exprName.type === AST_NODE_TYPES.TSQualifiedName) { + let iter = exprName; + while (iter.left.type === AST_NODE_TYPES.TSQualifiedName) { + iter = iter.left; + } + identifier = iter.left; + } else { + identifier = exprName; + } + + if (identifier.name === 'this') { + (identifier.type as string) = AST_NODE_TYPES.ThisExpression; + delete (identifier as { name?: string }).name; + } + }, }, ); } diff --git a/packages/typescript-estree/tests/lib/__snapshots__/semantic-diagnostics-enabled.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/semantic-diagnostics-enabled.test.ts.snap index 16e81894b38..6cabb17f516 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/semantic-diagnostics-enabled.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/semantic-diagnostics-enabled.test.ts.snap @@ -2786,6 +2786,8 @@ exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" e exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/types/typeof.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`; +exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/types/typeof-this.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`; + exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/types/typeof-with-type-parameters.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`; exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/types/union-intersection.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`; diff --git a/packages/typescript-estree/tests/snapshots/typescript/types/typeof-this.src.ts.shot b/packages/typescript-estree/tests/snapshots/typescript/types/typeof-this.src.ts.shot new file mode 100644 index 00000000000..c708076ab55 --- /dev/null +++ b/packages/typescript-estree/tests/snapshots/typescript/types/typeof-this.src.ts.shot @@ -0,0 +1,530 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`typescript types typeof-this.src 1`] = ` +Object { + "body": Array [ + Object { + "declarations": Array [ + Object { + "id": Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 4, + "line": 1, + }, + }, + "name": "self", + "range": Array [ + 4, + 21, + ], + "type": "Identifier", + "typeAnnotation": Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 8, + "line": 1, + }, + }, + "range": Array [ + 8, + 21, + ], + "type": "TSTypeAnnotation", + "typeAnnotation": Object { + "exprName": Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 17, + "line": 1, + }, + }, + "range": Array [ + 17, + 21, + ], + "type": "ThisExpression", + }, + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 10, + "line": 1, + }, + }, + "range": Array [ + 10, + 21, + ], + "type": "TSTypeQuery", + "typeParameters": undefined, + }, + }, + }, + "init": null, + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 4, + "line": 1, + }, + }, + "range": Array [ + 4, + 21, + ], + "type": "VariableDeclarator", + }, + ], + "kind": "let", + "loc": Object { + "end": Object { + "column": 22, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 22, + ], + "type": "VariableDeclaration", + }, + Object { + "declarations": Array [ + Object { + "id": Object { + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 4, + "line": 2, + }, + }, + "name": "foo", + "range": Array [ + 27, + 47, + ], + "type": "Identifier", + "typeAnnotation": Object { + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 7, + "line": 2, + }, + }, + "range": Array [ + 30, + 47, + ], + "type": "TSTypeAnnotation", + "typeAnnotation": Object { + "exprName": Object { + "left": Object { + "loc": Object { + "end": Object { + "column": 20, + "line": 2, + }, + "start": Object { + "column": 16, + "line": 2, + }, + }, + "range": Array [ + 39, + 43, + ], + "type": "ThisExpression", + }, + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 16, + "line": 2, + }, + }, + "range": Array [ + 39, + 47, + ], + "right": Object { + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 21, + "line": 2, + }, + }, + "name": "foo", + "range": Array [ + 44, + 47, + ], + "type": "Identifier", + }, + "type": "TSQualifiedName", + }, + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 9, + "line": 2, + }, + }, + "range": Array [ + 32, + 47, + ], + "type": "TSTypeQuery", + "typeParameters": undefined, + }, + }, + }, + "init": null, + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 4, + "line": 2, + }, + }, + "range": Array [ + 27, + 47, + ], + "type": "VariableDeclarator", + }, + ], + "kind": "let", + "loc": Object { + "end": Object { + "column": 25, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "range": Array [ + 23, + 48, + ], + "type": "VariableDeclaration", + }, + ], + "comments": Array [], + "loc": Object { + "end": Object { + "column": 0, + "line": 3, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 49, + ], + "sourceType": "script", + "tokens": Array [ + Object { + "loc": Object { + "end": Object { + "column": 3, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 3, + ], + "type": "Keyword", + "value": "let", + }, + Object { + "loc": Object { + "end": Object { + "column": 8, + "line": 1, + }, + "start": Object { + "column": 4, + "line": 1, + }, + }, + "range": Array [ + 4, + 8, + ], + "type": "Identifier", + "value": "self", + }, + Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 1, + }, + "start": Object { + "column": 8, + "line": 1, + }, + }, + "range": Array [ + 8, + 9, + ], + "type": "Punctuator", + "value": ":", + }, + Object { + "loc": Object { + "end": Object { + "column": 16, + "line": 1, + }, + "start": Object { + "column": 10, + "line": 1, + }, + }, + "range": Array [ + 10, + 16, + ], + "type": "Keyword", + "value": "typeof", + }, + Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 17, + "line": 1, + }, + }, + "range": Array [ + 17, + 21, + ], + "type": "Keyword", + "value": "this", + }, + Object { + "loc": Object { + "end": Object { + "column": 22, + "line": 1, + }, + "start": Object { + "column": 21, + "line": 1, + }, + }, + "range": Array [ + 21, + 22, + ], + "type": "Punctuator", + "value": ";", + }, + Object { + "loc": Object { + "end": Object { + "column": 3, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "range": Array [ + 23, + 26, + ], + "type": "Keyword", + "value": "let", + }, + Object { + "loc": Object { + "end": Object { + "column": 7, + "line": 2, + }, + "start": Object { + "column": 4, + "line": 2, + }, + }, + "range": Array [ + 27, + 30, + ], + "type": "Identifier", + "value": "foo", + }, + Object { + "loc": Object { + "end": Object { + "column": 8, + "line": 2, + }, + "start": Object { + "column": 7, + "line": 2, + }, + }, + "range": Array [ + 30, + 31, + ], + "type": "Punctuator", + "value": ":", + }, + Object { + "loc": Object { + "end": Object { + "column": 15, + "line": 2, + }, + "start": Object { + "column": 9, + "line": 2, + }, + }, + "range": Array [ + 32, + 38, + ], + "type": "Keyword", + "value": "typeof", + }, + Object { + "loc": Object { + "end": Object { + "column": 20, + "line": 2, + }, + "start": Object { + "column": 16, + "line": 2, + }, + }, + "range": Array [ + 39, + 43, + ], + "type": "Keyword", + "value": "this", + }, + Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 2, + }, + "start": Object { + "column": 20, + "line": 2, + }, + }, + "range": Array [ + 43, + 44, + ], + "type": "Punctuator", + "value": ".", + }, + Object { + "loc": Object { + "end": Object { + "column": 24, + "line": 2, + }, + "start": Object { + "column": 21, + "line": 2, + }, + }, + "range": Array [ + 44, + 47, + ], + "type": "Identifier", + "value": "foo", + }, + Object { + "loc": Object { + "end": Object { + "column": 25, + "line": 2, + }, + "start": Object { + "column": 24, + "line": 2, + }, + }, + "range": Array [ + 47, + 48, + ], + "type": "Punctuator", + "value": ";", + }, + ], + "type": "Program", +} +`;