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
35 changes: 29 additions & 6 deletions src/compiler/checker.ts
Expand Up @@ -726,6 +726,7 @@ namespace ts {
isPropertyAccessible,
getTypeOnlyAliasDeclaration,
getMemberOverrideModifierStatus,
isTypeParameterPossiblyReferenced,
};

function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
Expand Down Expand Up @@ -17047,7 +17048,7 @@ 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.
if (tp.symbol && tp.symbol.declarations && tp.symbol.declarations.length === 1) {
Expand All @@ -17068,6 +17069,15 @@ 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 = getSymbolAtLocation(getFirstIdentifier(entityName));
if (firstIdentifier?.declarations && firstIdentifier.declarations.length === 1 && tp.symbol) {
gabritto marked this conversation as resolved.
Show resolved Hide resolved
const symbols = getSymbolsInScope(firstIdentifier.declarations[0], SymbolFlags.All, /*allowDuplicates*/ true);
gabritto marked this conversation as resolved.
Show resolved Hide resolved
if (!symbols.find(sym => sym === tp.symbol)) {
Copy link
Member

Choose a reason for hiding this comment

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

Use the some function instead of forEach. Also, to get the declaration of tp use getDeclarationOfKind<TypeParameterDeclaration>(tp.symbol, SyntaxKind.TypeParameter)!. Although very unlikely, it is possible for there to be other declarations in the array, for example if the same name is used for a type parameter and a regular parameter.

const typeArguments = (node as TypeQueryNode).typeArguments;
return typeArguments? !!forEach(typeArguments, containsReference) : false;
}
}
return true;
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
Expand Down Expand Up @@ -42074,19 +42084,26 @@ namespace ts {

// Language service support

function getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[] {
function getSymbolsInScope(location: Node, meaning: SymbolFlags, allowDuplicates = false): Symbol[] {
if (location.flags & NodeFlags.InWithStatement) {
// We cannot answer semantic questions within a with block, do not proceed any further
return [];
}

const symbols = createSymbolTable();
const symbols = new Map<__String, Symbol[]>();
let isStaticSymbol = false;

populateSymbols();

symbols.delete(InternalSymbolName.This); // Not a symbol, a keyword
return symbolsToArray(symbols);

const result: Symbol[] = [];
symbols.forEach((symbolArray, id) => {
if (!isReservedMemberName(id)) {
result.push(...symbolArray);
}
});
return result;

function populateSymbols() {
while (location) {
Expand Down Expand Up @@ -42155,8 +42172,14 @@ namespace ts {
// We will copy all symbol regardless of its reserved name because
// symbolsToArray will check whether the key is a reserved name and
// it will not copy symbol with reserved name to the array
if (!symbols.has(id)) {
symbols.set(id, symbol);
if (allowDuplicates) {
if (!symbols.has(id)) {
symbols.set(id, []);
}
symbols.get(id)!.push(symbol);
}
else if (!symbols.has(id)) {
symbols.set(id, [symbol]);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Expand Up @@ -4624,6 +4624,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 @@ -79,6 +79,7 @@
"unittests/createMapShim.ts",
"unittests/createSetShim.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)