Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve check of whether type query node possibly contains reference to type parameter #50070

Merged
merged 13 commits into from Sep 19, 2022
28 changes: 26 additions & 2 deletions src/compiler/checker.ts
Expand Up @@ -728,6 +728,7 @@ namespace ts {
isPropertyAccessible,
getTypeOnlyAliasDeclaration,
getMemberOverrideModifierStatus,
isTypeParameterPossiblyReferenced,
};

function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<T>
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:
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/tsconfig.json
Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions src/testRunner/unittests/typeParameterIsPossiblyReferenced.ts
@@ -0,0 +1,33 @@
describe("unittests :: internalApi :: typeParameterIsPossiblyReferenced", () => {
it("with type parameter aliasing", () => {
const content = `
declare function foo<T>(b: T, f: <T>(a: typeof b) => typeof a): typeof f;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't come up with a test program such that type checking it observed the consequences of an incorrect containsReference/typeParameterIsPossiblyReferenced result in the presence of type parameter aliasing, so I wrote a unit test for it.

`;
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<T>
.parameters[1] // f
.type! as ts.FunctionTypeNode) // <T>(a: typeof b) => typeof a
.type as ts.TypeQueryNode) // typeof a
;
const typeParameterDecl = (file.statements[0] as ts.FunctionDeclaration).typeParameters![0]; // T in f<T>
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");
});
});
51 changes: 51 additions & 0 deletions tests/baselines/reference/typeofObjectInference.js
@@ -0,0 +1,51 @@
//// [typeofObjectInference.ts]
let val = 1

function decorateA<O extends any>(fn: (first: {value: typeof val}) => O) {
return (): O => fn({value: val})
}
let a = decorateA(({value}) => 5)

function decorateB<O extends any>(fn: (first: typeof val) => O) {
return (): O => fn(val)
}
let b = decorateB((value) => 5)

function decorateC<O extends any>(fn: (first: {value: number}) => O) {
return (): O => fn({value: val})
}
let c = decorateC(({value}) => 5)

type First = {value: typeof val}
function decorateD<O extends any>(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;
});
85 changes: 85 additions & 0 deletions 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<O extends any>(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<O extends any>(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<O extends any>(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<O extends any>(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))

96 changes: 96 additions & 0 deletions tests/baselines/reference/typeofObjectInference.types
@@ -0,0 +1,96 @@
=== tests/cases/compiler/typeofObjectInference.ts ===
let val = 1
>val : number
>1 : 1

function decorateA<O extends any>(fn: (first: {value: typeof val}) => O) {
>decorateA : <O extends unknown>(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 : <O extends unknown>(fn: (first: { value: number; }) => O) => () => O
>({value}) => 5 : ({ value }: { value: number; }) => number
>value : number
>5 : 5

function decorateB<O extends any>(fn: (first: typeof val) => O) {
>decorateB : <O extends unknown>(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 : <O extends unknown>(fn: (first: number) => O) => () => O
>(value) => 5 : (value: number) => number
>value : number
>5 : 5

function decorateC<O extends any>(fn: (first: {value: number}) => O) {
>decorateC : <O extends unknown>(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 : <O extends unknown>(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<O extends any>(fn: (first: First) => O) {
>decorateD : <O extends unknown>(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 : <O extends unknown>(fn: (first: First) => O) => () => O
>({value}) => 5 : ({ value }: First) => number
>value : number
>5 : 5

22 changes: 22 additions & 0 deletions tests/cases/compiler/typeofObjectInference.ts
@@ -0,0 +1,22 @@
let val = 1

function decorateA<O extends any>(fn: (first: {value: typeof val}) => O) {
return (): O => fn({value: val})
}
let a = decorateA(({value}) => 5)

function decorateB<O extends any>(fn: (first: typeof val) => O) {
return (): O => fn(val)
}
let b = decorateB((value) => 5)

function decorateC<O extends any>(fn: (first: {value: number}) => O) {
return (): O => fn({value: val})
}
let c = decorateC(({value}) => 5)

type First = {value: typeof val}
function decorateD<O extends any>(fn: (first: First) => O) {
return (): O => fn({value: val})
}
let d = decorateD(({value}) => 5)