Skip to content

Commit

Permalink
Improve check of whether type query node possibly contains reference …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
gabritto committed Sep 19, 2022
1 parent af9ced1 commit 48a8e89
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 2 deletions.
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;
`;
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)

0 comments on commit 48a8e89

Please sign in to comment.