From 48a8e8953a1e609970dc85e08f99ac499bfe8356 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 19 Sep 2022 01:13:30 -0300 Subject: [PATCH] Improve check of whether type query node possibly contains reference to type parameter (#50070) * WIP * implement typequery contains reference check + tests * add unit test * fix unit test * use symbols in scope to check type query type parameter references * remove comment on unit test * remove comment * use isNodeDescendantOf implementation to check scoping * CR: small fixes * treat the different kinds of type parameter declarations * undo test change --- src/compiler/checker.ts | 28 +++++- src/compiler/types.ts | 1 + src/testRunner/tsconfig.json | 1 + .../typeParameterIsPossiblyReferenced.ts | 33 +++++++ .../reference/typeofObjectInference.js | 51 ++++++++++ .../reference/typeofObjectInference.symbols | 85 ++++++++++++++++ .../reference/typeofObjectInference.types | 96 +++++++++++++++++++ tests/cases/compiler/typeofObjectInference.ts | 22 +++++ 8 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 src/testRunner/unittests/typeParameterIsPossiblyReferenced.ts create mode 100644 tests/baselines/reference/typeofObjectInference.js create mode 100644 tests/baselines/reference/typeofObjectInference.symbols create mode 100644 tests/baselines/reference/typeofObjectInference.types create mode 100644 tests/cases/compiler/typeofObjectInference.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 37cb09394e1aa..8541917621bb4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -728,6 +728,7 @@ namespace ts { isPropertyAccessible, getTypeOnlyAliasDeclaration, getMemberOverrideModifierStatus, + isTypeParameterPossiblyReferenced, }; function runWithInferenceBlockedFromSourceNode(node: Node | undefined, fn: () => T): T { @@ -17193,9 +17194,10 @@ namespace ts { } function isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node) { - // If the type parameter doesn't have exactly one declaration, if there are invening statement blocks + // If the type parameter doesn't have exactly one declaration, if there are intervening statement blocks // between the node and the type parameter declaration, if the node contains actual references to the - // type parameter, or if the node contains type queries, we consider the type parameter possibly referenced. + // type parameter, or if the node contains type queries that we can't prove couldn't contain references to the type parameter, + // we consider the type parameter possibly referenced. if (tp.symbol && tp.symbol.declarations && tp.symbol.declarations.length === 1) { const container = tp.symbol.declarations[0].parent; for (let n = node; n !== container; n = n.parent) { @@ -17214,6 +17216,28 @@ namespace ts { return !tp.isThisType && isPartOfTypeNode(node) && maybeTypeParameterReference(node) && getTypeFromTypeNodeWorker(node as TypeNode) === tp; // use worker because we're looking for === equality case SyntaxKind.TypeQuery: + const entityName = (node as TypeQueryNode).exprName; + const firstIdentifier = getFirstIdentifier(entityName); + const firstIdentifierSymbol = getResolvedSymbol(firstIdentifier); + const tpDeclaration = tp.symbol.declarations![0]; // There is exactly one declaration, otherwise `containsReference` is not called + let tpScope: Node; + if (tpDeclaration.kind === SyntaxKind.TypeParameter) { // Type parameter is a regular type parameter, e.g. foo + tpScope = tpDeclaration.parent; + } + else if (tp.isThisType) { + // Type parameter is the this type, and its declaration is the class declaration. + tpScope = tpDeclaration; + } + else { + // Type parameter's declaration was unrecognized. + // This could happen if the type parameter comes from e.g. a JSDoc annotation, so we default to returning true. + return true; + } + + if (firstIdentifierSymbol.declarations) { + return some(firstIdentifierSymbol.declarations, idDecl => isNodeDescendantOf(idDecl, tpScope)) || + some((node as TypeQueryNode).typeArguments, containsReference); + } return true; case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c9a3590a4dbfb..1753770f44ce4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4854,6 +4854,7 @@ namespace ts { /* @internal */ isPropertyAccessible(node: Node, isSuper: boolean, isWrite: boolean, containingType: Type, property: Symbol): boolean; /* @internal */ getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyAliasDeclaration | undefined; /* @internal */ getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement): MemberOverrideStatus; + /* @internal */ isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node): boolean; } /* @internal */ diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index b7c699fcea611..54221930c8ff6 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -76,6 +76,7 @@ "unittests/reuseProgramStructure.ts", "unittests/semver.ts", "unittests/transform.ts", + "unittests/typeParameterIsPossiblyReferenced.ts", "unittests/config/commandLineParsing.ts", "unittests/config/configurationExtension.ts", "unittests/config/convertCompilerOptionsFromJson.ts", diff --git a/src/testRunner/unittests/typeParameterIsPossiblyReferenced.ts b/src/testRunner/unittests/typeParameterIsPossiblyReferenced.ts new file mode 100644 index 0000000000000..a34819dbd914e --- /dev/null +++ b/src/testRunner/unittests/typeParameterIsPossiblyReferenced.ts @@ -0,0 +1,33 @@ +describe("unittests :: internalApi :: typeParameterIsPossiblyReferenced", () => { + it("with type parameter aliasing", () => { + const content = ` + declare function foo(b: T, f: (a: typeof b) => typeof a): typeof f; + `; + const host = new fakes.CompilerHost(vfs.createFromFileSystem( + Harness.IO, + /*ignoreCases*/ true, + { + documents: [ + new documents.TextDocument("/file.ts", content) + ], + cwd: "/", + } + )); + const program = ts.createProgram({ + host, + rootNames: ["/file.ts"], + options: { strict: true }, + }); + const checker = program.getTypeChecker(); + const file = program.getSourceFile("/file.ts")!; + const typeQueryNode = (((file.statements[0] as ts.FunctionDeclaration) // function f + .parameters[1] // f + .type! as ts.FunctionTypeNode) // (a: typeof b) => typeof a + .type as ts.TypeQueryNode) // typeof a + ; + const typeParameterDecl = (file.statements[0] as ts.FunctionDeclaration).typeParameters![0]; // T in f + const typeParameter = checker.getTypeAtLocation(typeParameterDecl)! as ts.TypeParameter; + const isReferenced = checker.isTypeParameterPossiblyReferenced(typeParameter, typeQueryNode); + assert.ok(isReferenced, "Type parameter is referenced in type query node"); + }); +}); diff --git a/tests/baselines/reference/typeofObjectInference.js b/tests/baselines/reference/typeofObjectInference.js new file mode 100644 index 0000000000000..26f2620eb005c --- /dev/null +++ b/tests/baselines/reference/typeofObjectInference.js @@ -0,0 +1,51 @@ +//// [typeofObjectInference.ts] +let val = 1 + +function decorateA(fn: (first: {value: typeof val}) => O) { + return (): O => fn({value: val}) +} +let a = decorateA(({value}) => 5) + +function decorateB(fn: (first: typeof val) => O) { + return (): O => fn(val) +} +let b = decorateB((value) => 5) + +function decorateC(fn: (first: {value: number}) => O) { + return (): O => fn({value: val}) +} +let c = decorateC(({value}) => 5) + +type First = {value: typeof val} +function decorateD(fn: (first: First) => O) { + return (): O => fn({value: val}) +} +let d = decorateD(({value}) => 5) + +//// [typeofObjectInference.js] +var val = 1; +function decorateA(fn) { + return function () { return fn({ value: val }); }; +} +var a = decorateA(function (_a) { + var value = _a.value; + return 5; +}); +function decorateB(fn) { + return function () { return fn(val); }; +} +var b = decorateB(function (value) { return 5; }); +function decorateC(fn) { + return function () { return fn({ value: val }); }; +} +var c = decorateC(function (_a) { + var value = _a.value; + return 5; +}); +function decorateD(fn) { + return function () { return fn({ value: val }); }; +} +var d = decorateD(function (_a) { + var value = _a.value; + return 5; +}); diff --git a/tests/baselines/reference/typeofObjectInference.symbols b/tests/baselines/reference/typeofObjectInference.symbols new file mode 100644 index 0000000000000..df39e97a4f324 --- /dev/null +++ b/tests/baselines/reference/typeofObjectInference.symbols @@ -0,0 +1,85 @@ +=== tests/cases/compiler/typeofObjectInference.ts === +let val = 1 +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) + +function decorateA(fn: (first: {value: typeof val}) => O) { +>decorateA : Symbol(decorateA, Decl(typeofObjectInference.ts, 0, 11)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 2, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 2, 34)) +>first : Symbol(first, Decl(typeofObjectInference.ts, 2, 39)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 2, 47)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 2, 19)) + + return (): O => fn({value: val}) +>O : Symbol(O, Decl(typeofObjectInference.ts, 2, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 2, 34)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 3, 24)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +} +let a = decorateA(({value}) => 5) +>a : Symbol(a, Decl(typeofObjectInference.ts, 5, 3)) +>decorateA : Symbol(decorateA, Decl(typeofObjectInference.ts, 0, 11)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 5, 20)) + +function decorateB(fn: (first: typeof val) => O) { +>decorateB : Symbol(decorateB, Decl(typeofObjectInference.ts, 5, 33)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 7, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 7, 34)) +>first : Symbol(first, Decl(typeofObjectInference.ts, 7, 39)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 7, 19)) + + return (): O => fn(val) +>O : Symbol(O, Decl(typeofObjectInference.ts, 7, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 7, 34)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +} +let b = decorateB((value) => 5) +>b : Symbol(b, Decl(typeofObjectInference.ts, 10, 3)) +>decorateB : Symbol(decorateB, Decl(typeofObjectInference.ts, 5, 33)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 10, 19)) + +function decorateC(fn: (first: {value: number}) => O) { +>decorateC : Symbol(decorateC, Decl(typeofObjectInference.ts, 10, 31)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 12, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 12, 34)) +>first : Symbol(first, Decl(typeofObjectInference.ts, 12, 39)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 12, 47)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 12, 19)) + + return (): O => fn({value: val}) +>O : Symbol(O, Decl(typeofObjectInference.ts, 12, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 12, 34)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 13, 24)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +} +let c = decorateC(({value}) => 5) +>c : Symbol(c, Decl(typeofObjectInference.ts, 15, 3)) +>decorateC : Symbol(decorateC, Decl(typeofObjectInference.ts, 10, 31)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 15, 20)) + +type First = {value: typeof val} +>First : Symbol(First, Decl(typeofObjectInference.ts, 15, 33)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 17, 14)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) + +function decorateD(fn: (first: First) => O) { +>decorateD : Symbol(decorateD, Decl(typeofObjectInference.ts, 17, 32)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 18, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 18, 34)) +>first : Symbol(first, Decl(typeofObjectInference.ts, 18, 39)) +>First : Symbol(First, Decl(typeofObjectInference.ts, 15, 33)) +>O : Symbol(O, Decl(typeofObjectInference.ts, 18, 19)) + + return (): O => fn({value: val}) +>O : Symbol(O, Decl(typeofObjectInference.ts, 18, 19)) +>fn : Symbol(fn, Decl(typeofObjectInference.ts, 18, 34)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 19, 24)) +>val : Symbol(val, Decl(typeofObjectInference.ts, 0, 3)) +} +let d = decorateD(({value}) => 5) +>d : Symbol(d, Decl(typeofObjectInference.ts, 21, 3)) +>decorateD : Symbol(decorateD, Decl(typeofObjectInference.ts, 17, 32)) +>value : Symbol(value, Decl(typeofObjectInference.ts, 21, 20)) + diff --git a/tests/baselines/reference/typeofObjectInference.types b/tests/baselines/reference/typeofObjectInference.types new file mode 100644 index 0000000000000..71ee207d606df --- /dev/null +++ b/tests/baselines/reference/typeofObjectInference.types @@ -0,0 +1,96 @@ +=== tests/cases/compiler/typeofObjectInference.ts === +let val = 1 +>val : number +>1 : 1 + +function decorateA(fn: (first: {value: typeof val}) => O) { +>decorateA : (fn: (first: { value: typeof val;}) => O) => () => O +>fn : (first: { value: typeof val;}) => O +>first : { value: typeof val; } +>value : number +>val : number + + return (): O => fn({value: val}) +>(): O => fn({value: val}) : () => O +>fn({value: val}) : O +>fn : (first: { value: number; }) => O +>{value: val} : { value: number; } +>value : number +>val : number +} +let a = decorateA(({value}) => 5) +>a : () => number +>decorateA(({value}) => 5) : () => number +>decorateA : (fn: (first: { value: number; }) => O) => () => O +>({value}) => 5 : ({ value }: { value: number; }) => number +>value : number +>5 : 5 + +function decorateB(fn: (first: typeof val) => O) { +>decorateB : (fn: (first: typeof val) => O) => () => O +>fn : (first: typeof val) => O +>first : number +>val : number + + return (): O => fn(val) +>(): O => fn(val) : () => O +>fn(val) : O +>fn : (first: number) => O +>val : number +} +let b = decorateB((value) => 5) +>b : () => number +>decorateB((value) => 5) : () => number +>decorateB : (fn: (first: number) => O) => () => O +>(value) => 5 : (value: number) => number +>value : number +>5 : 5 + +function decorateC(fn: (first: {value: number}) => O) { +>decorateC : (fn: (first: { value: number;}) => O) => () => O +>fn : (first: { value: number;}) => O +>first : { value: number; } +>value : number + + return (): O => fn({value: val}) +>(): O => fn({value: val}) : () => O +>fn({value: val}) : O +>fn : (first: { value: number; }) => O +>{value: val} : { value: number; } +>value : number +>val : number +} +let c = decorateC(({value}) => 5) +>c : () => number +>decorateC(({value}) => 5) : () => number +>decorateC : (fn: (first: { value: number; }) => O) => () => O +>({value}) => 5 : ({ value }: { value: number; }) => number +>value : number +>5 : 5 + +type First = {value: typeof val} +>First : { value: typeof val; } +>value : number +>val : number + +function decorateD(fn: (first: First) => O) { +>decorateD : (fn: (first: First) => O) => () => O +>fn : (first: First) => O +>first : First + + return (): O => fn({value: val}) +>(): O => fn({value: val}) : () => O +>fn({value: val}) : O +>fn : (first: First) => O +>{value: val} : { value: number; } +>value : number +>val : number +} +let d = decorateD(({value}) => 5) +>d : () => number +>decorateD(({value}) => 5) : () => number +>decorateD : (fn: (first: First) => O) => () => O +>({value}) => 5 : ({ value }: First) => number +>value : number +>5 : 5 + diff --git a/tests/cases/compiler/typeofObjectInference.ts b/tests/cases/compiler/typeofObjectInference.ts new file mode 100644 index 0000000000000..e48a66c817700 --- /dev/null +++ b/tests/cases/compiler/typeofObjectInference.ts @@ -0,0 +1,22 @@ +let val = 1 + +function decorateA(fn: (first: {value: typeof val}) => O) { + return (): O => fn({value: val}) +} +let a = decorateA(({value}) => 5) + +function decorateB(fn: (first: typeof val) => O) { + return (): O => fn(val) +} +let b = decorateB((value) => 5) + +function decorateC(fn: (first: {value: number}) => O) { + return (): O => fn({value: val}) +} +let c = decorateC(({value}) => 5) + +type First = {value: typeof val} +function decorateD(fn: (first: First) => O) { + return (): O => fn({value: val}) +} +let d = decorateD(({value}) => 5) \ No newline at end of file