Skip to content

Commit

Permalink
feat: treat this in typeof this as a ThisExpression (#4382)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zzzen committed Jun 27, 2022
1 parent 8d2d25f commit b04b2ce
Show file tree
Hide file tree
Showing 13 changed files with 774 additions and 14 deletions.
3 changes: 2 additions & 1 deletion 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;
10 changes: 10 additions & 0 deletions packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts
Expand Up @@ -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: [
{
Expand Down
10 changes: 6 additions & 4 deletions packages/scope-manager/src/referencer/ClassVisitor.ts
Expand Up @@ -293,21 +293,23 @@ 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
) {
let iter = node.typeAnnotation.typeName;
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);
Expand Down
19 changes: 12 additions & 7 deletions packages/scope-manager/src/referencer/TypeVisitor.ts
Expand Up @@ -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);
}

Expand Down
@@ -0,0 +1,5 @@
function decorator() {}
@decorator
class Foo {
bar(baz: typeof this) {}
}
142 changes: 142 additions & 0 deletions 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,
],
},
],
}
`;
@@ -0,0 +1,2 @@
let self: typeof this;
let foo: typeof this.foo;
8 changes: 8 additions & 0 deletions packages/typescript-estree/src/convert.ts
Expand Up @@ -20,6 +20,7 @@ import {
isComputedProperty,
isESTreeClassMember,
isOptional,
isThisInTypeQuery,
TSError,
unescapeStringLiteralText,
} from './node-utils';
Expand Down Expand Up @@ -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<TSESTree.ThisExpression>(node, {
type: AST_NODE_TYPES.ThisExpression,
});
}
return this.createNode<TSESTree.Identifier>(node, {
type: AST_NODE_TYPES.Identifier,
name: node.text,
Expand Down
26 changes: 26 additions & 0 deletions packages/typescript-estree/src/node-utils.ts
Expand Up @@ -662,3 +662,29 @@ export function firstDefined<T, U>(
}
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;
}
Expand Up @@ -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;
Expand Down
26 changes: 25 additions & 1 deletion 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';

Expand Down Expand Up @@ -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;
}
},
},
);
}
Expand Down
Expand Up @@ -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"`;
Expand Down

0 comments on commit b04b2ce

Please sign in to comment.