Skip to content

Support control flow analysis for tagged template calls #53962

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
ExpressionStatement,
findAncestor,
FlowFlags,
FlowImpactingCallLikeExpression,
FlowLabel,
FlowNode,
FlowReduceLabel,
Expand Down Expand Up @@ -294,6 +295,7 @@ import {
symbolName,
SymbolTable,
SyntaxKind,
TaggedTemplateExpression,
TextRange,
ThisExpression,
ThrowStatement,
Expand Down Expand Up @@ -1344,7 +1346,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
return result;
}

function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode {
function createFlowCall(antecedent: FlowNode, node: FlowImpactingCallLikeExpression): FlowNode {
setFlowNodeReferenced(antecedent);
return initFlowNode({ flags: FlowFlags.Call, antecedent, node });
}
Expand Down Expand Up @@ -1695,11 +1697,19 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
function maybeBindExpressionFlowIfCall(node: Expression) {
// A top level or comma expression call expression with a dotted function name and at least one argument
// is potentially an assertion and is therefore included in the control flow.
if (node.kind === SyntaxKind.CallExpression) {
const call = node as CallExpression;
if (call.expression.kind !== SyntaxKind.SuperKeyword && isDottedName(call.expression)) {
currentFlow = createFlowCall(currentFlow, call);
}
switch (node.kind) {
case SyntaxKind.CallExpression:
const call = node as CallExpression;
if (call.expression.kind !== SyntaxKind.SuperKeyword && isDottedName(call.expression)) {
currentFlow = createFlowCall(currentFlow, call);
}
break;
case SyntaxKind.TaggedTemplateExpression:
const taggedTemplate = node as TaggedTemplateExpression;
if (isDottedName(taggedTemplate.tag)) {
currentFlow = createFlowCall(currentFlow, taggedTemplate);
}
break;
}
}

Expand Down
42 changes: 31 additions & 11 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ import {
FlowCall,
FlowCondition,
FlowFlags,
FlowImpactingCallLikeExpression,
FlowLabel,
FlowNode,
FlowReduceLabel,
Expand Down Expand Up @@ -26185,27 +26186,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function getEffectsSignature(node: CallExpression) {
function getEffectsSignature(node: FlowImpactingCallLikeExpression) {
const links = getNodeLinks(node);
let signature = links.effectsSignature;
if (signature === undefined) {
const expression = getFlowCallNodeExpression(node);
// A call expression parented by an expression statement is a potential assertion. Other call
// expressions are potential type predicate function calls. In order to avoid triggering
// circularities in control flow analysis, we use getTypeOfDottedName when resolving the call
// target expression of an assertion.
let funcType: Type | undefined;
if (node.parent.kind === SyntaxKind.ExpressionStatement) {
funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined);
funcType = getTypeOfDottedName(expression, /*diagnostic*/ undefined);
}
else if (node.expression.kind !== SyntaxKind.SuperKeyword) {
else if (expression.kind !== SyntaxKind.SuperKeyword) {
if (isOptionalChain(node)) {
funcType = checkNonNullType(
getOptionalExpressionType(checkExpression(node.expression), node.expression),
node.expression
);
}
else {
funcType = checkNonNullExpression(node.expression);
funcType = checkNonNullExpression(expression);
}
}
const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call);
Expand All @@ -26222,11 +26224,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & TypeFlags.Never);
}

function getTypePredicateArgument(predicate: TypePredicate, callExpression: CallExpression) {
function getTypePredicateArgument(predicate: TypePredicate, expression: FlowImpactingCallLikeExpression) {
if (predicate.kind === TypePredicateKind.Identifier || predicate.kind === TypePredicateKind.AssertsIdentifier) {
return callExpression.arguments[predicate.parameterIndex];
return getFlowCallNodeArguments(expression)[predicate.parameterIndex];
}
const invokedExpression = skipParentheses(callExpression.expression);
const invokedExpression = skipParentheses(getFlowCallNodeExpression(expression));
return isAccessExpression(invokedExpression) ? skipParentheses(invokedExpression.expression) : undefined;
}

Expand Down Expand Up @@ -26273,7 +26275,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (signature) {
const predicate = getTypePredicateOfSignature(signature);
if (predicate && predicate.kind === TypePredicateKind.AssertsIdentifier && !predicate.type) {
const predicateArgument = (flow as FlowCall).node.arguments[predicate.parameterIndex];
const predicateArgument = getFlowCallNodeArguments((flow as FlowCall).node)[predicate.parameterIndex];
if (predicateArgument && isFalseExpression(predicateArgument)) {
return false;
}
Expand Down Expand Up @@ -26337,7 +26339,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
flow = (flow as FlowAssignment | FlowCondition | FlowArrayMutation | FlowSwitchClause).antecedent;
}
else if (flags & FlowFlags.Call) {
if ((flow as FlowCall).node.expression.kind === SyntaxKind.SuperKeyword) {
const node = (flow as FlowCall).node;
if (node.kind === SyntaxKind.CallExpression && node.expression.kind === SyntaxKind.SuperKeyword) {
return true;
}
flow = (flow as FlowCall).antecedent;
Expand Down Expand Up @@ -26596,8 +26599,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (predicate && (predicate.kind === TypePredicateKind.AssertsThis || predicate.kind === TypePredicateKind.AssertsIdentifier)) {
const flowType = getTypeAtFlowNode(flow.antecedent);
const type = finalizeEvolvingArrayType(getTypeFromFlowType(flowType));
let callArguments: readonly Expression[];
const narrowedType = predicate.type ? narrowTypeByTypePredicate(type, predicate, flow.node, /*assumeTrue*/ true) :
predicate.kind === TypePredicateKind.AssertsIdentifier && predicate.parameterIndex >= 0 && predicate.parameterIndex < flow.node.arguments.length ? narrowTypeByAssertion(type, flow.node.arguments[predicate.parameterIndex]) :
predicate.kind === TypePredicateKind.AssertsIdentifier && predicate.parameterIndex >= 0 && predicate.parameterIndex < (callArguments = getFlowCallNodeArguments(flow.node)).length ? narrowTypeByAssertion(type, callArguments[predicate.parameterIndex]) :
type;
return narrowedType === type ? flowType : createFlowType(narrowedType, isIncomplete(flowType));
}
Expand Down Expand Up @@ -27419,7 +27423,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}

function narrowTypeByTypePredicate(type: Type, predicate: TypePredicate, callExpression: CallExpression, assumeTrue: boolean): Type {
function narrowTypeByTypePredicate(type: Type, predicate: TypePredicate, callExpression: FlowImpactingCallLikeExpression, assumeTrue: boolean): Type {
// Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function'
if (predicate.type && !(isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType))) {
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
Expand Down Expand Up @@ -32676,6 +32680,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}

/**
* Returns the called expression of the node, i.e. its {@link CallExpression.expression expression}
* or its {@link TaggedTemplateExpression.tag tag}.
*/
function getFlowCallNodeExpression(node: FlowImpactingCallLikeExpression): LeftHandSideExpression {
return node.kind === SyntaxKind.CallExpression ? node.expression : node.tag;
}

/**
* Returns the arguments of the node, i.e. its direct {@link CallExpression.arguments arguments}
* or the {@link getEffectiveCallArguments effective arguments} of the template literal.
*/
function getFlowCallNodeArguments(node: FlowImpactingCallLikeExpression): readonly Expression[] {
return node.kind === SyntaxKind.CallExpression ? node.arguments : getEffectiveCallArguments(node);
}

/**
* Returns the effective arguments for an expression that works like a function invocation.
*/
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3109,6 +3109,11 @@ export type CallLikeExpression =
| JsxOpeningLikeElement
;

export type FlowImpactingCallLikeExpression =
| CallExpression
| TaggedTemplateExpression
;

export interface AsExpression extends Expression {
readonly kind: SyntaxKind.AsExpression;
readonly expression: Expression;
Expand Down Expand Up @@ -4163,7 +4168,7 @@ export interface FlowAssignment extends FlowNodeBase {
}

export interface FlowCall extends FlowNodeBase {
node: CallExpression;
node: FlowImpactingCallLikeExpression;
antecedent: FlowNode;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5254,6 +5254,7 @@ declare namespace ts {
readonly template: TemplateLiteral;
}
type CallLikeExpression = CallExpression | NewExpression | TaggedTemplateExpression | Decorator | JsxOpeningLikeElement;
type FlowImpactingCallLikeExpression = CallExpression | TaggedTemplateExpression;
interface AsExpression extends Expression {
readonly kind: SyntaxKind.AsExpression;
readonly expression: Expression;
Expand Down Expand Up @@ -5976,7 +5977,7 @@ declare namespace ts {
antecedent: FlowNode;
}
interface FlowCall extends FlowNodeBase {
node: CallExpression;
node: FlowImpactingCallLikeExpression;
antecedent: FlowNode;
}
interface FlowCondition extends FlowNodeBase {
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,7 @@ declare namespace ts {
readonly template: TemplateLiteral;
}
type CallLikeExpression = CallExpression | NewExpression | TaggedTemplateExpression | Decorator | JsxOpeningLikeElement;
type FlowImpactingCallLikeExpression = CallExpression | TaggedTemplateExpression;
interface AsExpression extends Expression {
readonly kind: SyntaxKind.AsExpression;
readonly expression: Expression;
Expand Down Expand Up @@ -2033,7 +2034,7 @@ declare namespace ts {
antecedent: FlowNode;
}
interface FlowCall extends FlowNodeBase {
node: CallExpression;
node: FlowImpactingCallLikeExpression;
antecedent: FlowNode;
}
interface FlowCondition extends FlowNodeBase {
Expand Down
23 changes: 23 additions & 0 deletions tests/baselines/reference/taggedTemplateWithAssertion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//// [taggedTemplateWithAssertion.ts]
function assert(strings: TemplateStringsArray, condition: boolean): asserts condition {}

let a!: number | string;

if (typeof a === "string") {
assert`uh-oh: ${false}`;
}

const b: number = a;


//// [taggedTemplateWithAssertion.js]
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
};
function assert(strings, condition) { }
var a;
if (typeof a === "string") {
assert(__makeTemplateObject(["uh-oh: ", ""], ["uh-oh: ", ""]), false);
}
var b = a;
22 changes: 22 additions & 0 deletions tests/baselines/reference/taggedTemplateWithAssertion.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
=== tests/cases/compiler/taggedTemplateWithAssertion.ts ===
function assert(strings: TemplateStringsArray, condition: boolean): asserts condition {}
>assert : Symbol(assert, Decl(taggedTemplateWithAssertion.ts, 0, 0))
>strings : Symbol(strings, Decl(taggedTemplateWithAssertion.ts, 0, 16))
>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.es5.d.ts, --, --))
>condition : Symbol(condition, Decl(taggedTemplateWithAssertion.ts, 0, 46))
>condition : Symbol(condition, Decl(taggedTemplateWithAssertion.ts, 0, 46))

let a!: number | string;
>a : Symbol(a, Decl(taggedTemplateWithAssertion.ts, 2, 3))

if (typeof a === "string") {
>a : Symbol(a, Decl(taggedTemplateWithAssertion.ts, 2, 3))

assert`uh-oh: ${false}`;
>assert : Symbol(assert, Decl(taggedTemplateWithAssertion.ts, 0, 0))
}

const b: number = a;
>b : Symbol(b, Decl(taggedTemplateWithAssertion.ts, 8, 5))
>a : Symbol(a, Decl(taggedTemplateWithAssertion.ts, 2, 3))

26 changes: 26 additions & 0 deletions tests/baselines/reference/taggedTemplateWithAssertion.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
=== tests/cases/compiler/taggedTemplateWithAssertion.ts ===
function assert(strings: TemplateStringsArray, condition: boolean): asserts condition {}
>assert : (strings: TemplateStringsArray, condition: boolean) => asserts condition
>strings : TemplateStringsArray
>condition : boolean

let a!: number | string;
>a : string | number

if (typeof a === "string") {
>typeof a === "string" : boolean
>typeof a : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a : string | number
>"string" : "string"

assert`uh-oh: ${false}`;
>assert`uh-oh: ${false}` : void
>assert : (strings: TemplateStringsArray, condition: boolean) => asserts condition
>`uh-oh: ${false}` : string
>false : false
}

const b: number = a;
>b : number
>a : number

27 changes: 27 additions & 0 deletions tests/baselines/reference/taggedTemplateWithNeverReturnType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//// [taggedTemplateWithNeverReturnType.ts]
function fail(strings?: TemplateStringsArray): never {
throw "";
}

let a!: number | string;

if (typeof a === "string") {
fail``;
}

const b: number = a;


//// [taggedTemplateWithNeverReturnType.js]
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
};
function fail(strings) {
throw "";
}
var a;
if (typeof a === "string") {
fail(__makeTemplateObject([""], [""]));
}
var b = a;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
=== tests/cases/compiler/taggedTemplateWithNeverReturnType.ts ===
function fail(strings?: TemplateStringsArray): never {
>fail : Symbol(fail, Decl(taggedTemplateWithNeverReturnType.ts, 0, 0))
>strings : Symbol(strings, Decl(taggedTemplateWithNeverReturnType.ts, 0, 14))
>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.es5.d.ts, --, --))

throw "";
}

let a!: number | string;
>a : Symbol(a, Decl(taggedTemplateWithNeverReturnType.ts, 4, 3))

if (typeof a === "string") {
>a : Symbol(a, Decl(taggedTemplateWithNeverReturnType.ts, 4, 3))

fail``;
>fail : Symbol(fail, Decl(taggedTemplateWithNeverReturnType.ts, 0, 0))
}

const b: number = a;
>b : Symbol(b, Decl(taggedTemplateWithNeverReturnType.ts, 10, 5))
>a : Symbol(a, Decl(taggedTemplateWithNeverReturnType.ts, 4, 3))

28 changes: 28 additions & 0 deletions tests/baselines/reference/taggedTemplateWithNeverReturnType.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
=== tests/cases/compiler/taggedTemplateWithNeverReturnType.ts ===
function fail(strings?: TemplateStringsArray): never {
>fail : (strings?: TemplateStringsArray) => never
>strings : TemplateStringsArray

throw "";
>"" : ""
}

let a!: number | string;
>a : string | number

if (typeof a === "string") {
>typeof a === "string" : boolean
>typeof a : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a : string | number
>"string" : "string"

fail``;
>fail`` : never
>fail : (strings?: TemplateStringsArray) => never
>`` : ""
}

const b: number = a;
>b : number
>a : number

9 changes: 9 additions & 0 deletions tests/cases/compiler/taggedTemplateWithAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function assert(strings: TemplateStringsArray, condition: boolean): asserts condition {}

let a!: number | string;

if (typeof a === "string") {
assert`uh-oh: ${false}`;
}

const b: number = a;
11 changes: 11 additions & 0 deletions tests/cases/compiler/taggedTemplateWithNeverReturnType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function fail(strings?: TemplateStringsArray): never {
throw "";
}

let a!: number | string;

if (typeof a === "string") {
fail``;
}

const b: number = a;