From de96190401979ebebbc7089fe5ac87673f1a6191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Geis?= Date: Sat, 25 Mar 2023 23:25:27 +0100 Subject: [PATCH] Support control flow analysis for tagged template calls --- src/compiler/binder.ts | 22 +++++++--- src/compiler/checker.ts | 42 ++++++++++++++----- src/compiler/types.ts | 7 +++- .../reference/api/tsserverlibrary.d.ts | 3 +- tests/baselines/reference/api/typescript.d.ts | 3 +- .../reference/taggedTemplateWithAssertion.js | 23 ++++++++++ .../taggedTemplateWithAssertion.symbols | 22 ++++++++++ .../taggedTemplateWithAssertion.types | 26 ++++++++++++ .../taggedTemplateWithNeverReturnType.js | 27 ++++++++++++ .../taggedTemplateWithNeverReturnType.symbols | 23 ++++++++++ .../taggedTemplateWithNeverReturnType.types | 28 +++++++++++++ .../compiler/taggedTemplateWithAssertion.ts | 9 ++++ .../taggedTemplateWithNeverReturnType.ts | 11 +++++ 13 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 tests/baselines/reference/taggedTemplateWithAssertion.js create mode 100644 tests/baselines/reference/taggedTemplateWithAssertion.symbols create mode 100644 tests/baselines/reference/taggedTemplateWithAssertion.types create mode 100644 tests/baselines/reference/taggedTemplateWithNeverReturnType.js create mode 100644 tests/baselines/reference/taggedTemplateWithNeverReturnType.symbols create mode 100644 tests/baselines/reference/taggedTemplateWithNeverReturnType.types create mode 100644 tests/cases/compiler/taggedTemplateWithAssertion.ts create mode 100644 tests/cases/compiler/taggedTemplateWithNeverReturnType.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 1ca6187ac39a4..43849f54a7957 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -66,6 +66,7 @@ import { ExpressionStatement, findAncestor, FlowFlags, + FlowImpactingCallLikeExpression, FlowLabel, FlowNode, FlowReduceLabel, @@ -294,6 +295,7 @@ import { symbolName, SymbolTable, SyntaxKind, + TaggedTemplateExpression, TextRange, ThisExpression, ThrowStatement, @@ -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 }); } @@ -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; } } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 48a4bdf3a5992..e1eb78c540608 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -199,6 +199,7 @@ import { FlowCall, FlowCondition, FlowFlags, + FlowImpactingCallLikeExpression, FlowLabel, FlowNode, FlowReduceLabel, @@ -26185,19 +26186,20 @@ 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), @@ -26205,7 +26207,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ); } else { - funcType = checkNonNullExpression(node.expression); + funcType = checkNonNullExpression(expression); } } const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call); @@ -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; } @@ -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; } @@ -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; @@ -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)); } @@ -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); @@ -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. */ diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fef447fc2dd50..e0189b5ca28ba 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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; @@ -4163,7 +4168,7 @@ export interface FlowAssignment extends FlowNodeBase { } export interface FlowCall extends FlowNodeBase { - node: CallExpression; + node: FlowImpactingCallLikeExpression; antecedent: FlowNode; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 760a948efc21a..94a4f532cdebd 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -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; @@ -5976,7 +5977,7 @@ declare namespace ts { antecedent: FlowNode; } interface FlowCall extends FlowNodeBase { - node: CallExpression; + node: FlowImpactingCallLikeExpression; antecedent: FlowNode; } interface FlowCondition extends FlowNodeBase { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 85d2a5b10a201..43b02e4fc4455 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -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; @@ -2033,7 +2034,7 @@ declare namespace ts { antecedent: FlowNode; } interface FlowCall extends FlowNodeBase { - node: CallExpression; + node: FlowImpactingCallLikeExpression; antecedent: FlowNode; } interface FlowCondition extends FlowNodeBase { diff --git a/tests/baselines/reference/taggedTemplateWithAssertion.js b/tests/baselines/reference/taggedTemplateWithAssertion.js new file mode 100644 index 0000000000000..a75d6927db550 --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithAssertion.js @@ -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; diff --git a/tests/baselines/reference/taggedTemplateWithAssertion.symbols b/tests/baselines/reference/taggedTemplateWithAssertion.symbols new file mode 100644 index 0000000000000..2446c699840dc --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithAssertion.symbols @@ -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)) + diff --git a/tests/baselines/reference/taggedTemplateWithAssertion.types b/tests/baselines/reference/taggedTemplateWithAssertion.types new file mode 100644 index 0000000000000..3e8e7dd23dea7 --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithAssertion.types @@ -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 + diff --git a/tests/baselines/reference/taggedTemplateWithNeverReturnType.js b/tests/baselines/reference/taggedTemplateWithNeverReturnType.js new file mode 100644 index 0000000000000..5293248f51bca --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithNeverReturnType.js @@ -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; diff --git a/tests/baselines/reference/taggedTemplateWithNeverReturnType.symbols b/tests/baselines/reference/taggedTemplateWithNeverReturnType.symbols new file mode 100644 index 0000000000000..316d9ed709cb0 --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithNeverReturnType.symbols @@ -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)) + diff --git a/tests/baselines/reference/taggedTemplateWithNeverReturnType.types b/tests/baselines/reference/taggedTemplateWithNeverReturnType.types new file mode 100644 index 0000000000000..d473a86e4c9c0 --- /dev/null +++ b/tests/baselines/reference/taggedTemplateWithNeverReturnType.types @@ -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 + diff --git a/tests/cases/compiler/taggedTemplateWithAssertion.ts b/tests/cases/compiler/taggedTemplateWithAssertion.ts new file mode 100644 index 0000000000000..a6a1ccf5d2c84 --- /dev/null +++ b/tests/cases/compiler/taggedTemplateWithAssertion.ts @@ -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; diff --git a/tests/cases/compiler/taggedTemplateWithNeverReturnType.ts b/tests/cases/compiler/taggedTemplateWithNeverReturnType.ts new file mode 100644 index 0000000000000..5ae322ed98c47 --- /dev/null +++ b/tests/cases/compiler/taggedTemplateWithNeverReturnType.ts @@ -0,0 +1,11 @@ +function fail(strings?: TemplateStringsArray): never { + throw ""; +} + +let a!: number | string; + +if (typeof a === "string") { + fail``; +} + +const b: number = a;