diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index adcc4523eabca..d1c9c196f252e 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -65,10 +65,15 @@ import { Expression, ExpressionStatement, findAncestor, + FlowArrayMutation, + FlowAssignment, + FlowCall, + FlowCondition, FlowFlags, FlowLabel, FlowNode, FlowReduceLabel, + FlowSwitchClause, forEach, forEachChild, ForInOrOfStatement, @@ -494,9 +499,9 @@ export const enum ContainerFlags { IsObjectLiteralOrClassExpressionMethodOrAccessor = 1 << 7, } -function initFlowNode(node: T) { - Debug.attachFlowNodeDebugInfo(node); - return node; +/** @internal */ +export function createFlowNode(flags: FlowFlags, node: unknown, antecedent: FlowNode | FlowNode[] | undefined): FlowNode { + return Debug.attachFlowNodeDebugInfo({ flags, id: 0, node, antecedent } as FlowNode); } const binder = /* @__PURE__ */ createBinder(); @@ -557,8 +562,8 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { var Symbol: new (flags: SymbolFlags, name: __String) => Symbol; var classifiableNames: Set<__String>; - var unreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; - var reportedUnreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; + var unreachableFlow = createFlowNode(FlowFlags.Unreachable, /*node*/ undefined, /*antecedent*/ undefined); + var reportedUnreachableFlow = createFlowNode(FlowFlags.Unreachable, /*node*/ undefined, /*antecedent*/ undefined); var bindBinaryExpressionFlow = createBindBinaryExpressionFlow(); /* eslint-enable no-var */ @@ -1013,7 +1018,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { // A non-async, non-generator IIFE is considered part of the containing control flow. Return statements behave // similarly to break statements that exit to a label just past the statement body. if (!isImmediatelyInvoked) { - currentFlow = initFlowNode({ flags: FlowFlags.Start }); + currentFlow = createFlowNode(FlowFlags.Start, /*node*/ undefined, /*antecedent*/ undefined); if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethodOrAccessor)) { currentFlow.node = node as FunctionExpression | ArrowFunction | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration; } @@ -1332,16 +1337,16 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { return containsNarrowableReference(expr); } - function createBranchLabel(): FlowLabel { - return initFlowNode({ flags: FlowFlags.BranchLabel, antecedents: undefined }); + function createBranchLabel() { + return createFlowNode(FlowFlags.BranchLabel, /*node*/ undefined, /*antecedent*/ undefined) as FlowLabel; } - function createLoopLabel(): FlowLabel { - return initFlowNode({ flags: FlowFlags.LoopLabel, antecedents: undefined }); + function createLoopLabel() { + return createFlowNode(FlowFlags.LoopLabel, /*node*/ undefined, /*antecedent*/ undefined) as FlowLabel; } - function createReduceLabel(target: FlowLabel, antecedents: FlowNode[], antecedent: FlowNode): FlowReduceLabel { - return initFlowNode({ flags: FlowFlags.ReduceLabel, target, antecedents, antecedent }); + function createReduceLabel(target: FlowLabel, antecedents: FlowNode[], antecedent: FlowNode) { + return createFlowNode(FlowFlags.ReduceLabel, { target, antecedents }, antecedent) as FlowReduceLabel; } function setFlowNodeReferenced(flow: FlowNode) { @@ -1350,13 +1355,13 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { } function addAntecedent(label: FlowLabel, antecedent: FlowNode): void { - if (!(antecedent.flags & FlowFlags.Unreachable) && !contains(label.antecedents, antecedent)) { - (label.antecedents || (label.antecedents = [])).push(antecedent); + if (!(antecedent.flags & FlowFlags.Unreachable) && !contains(label.antecedent, antecedent)) { + (label.antecedent || (label.antecedent = [])).push(antecedent); setFlowNodeReferenced(antecedent); } } - function createFlowCondition(flags: FlowFlags, antecedent: FlowNode, expression: Expression | undefined): FlowNode { + function createFlowCondition(flags: FlowFlags.TrueCondition | FlowFlags.FalseCondition, antecedent: FlowNode, expression: Expression | undefined) { if (antecedent.flags & FlowFlags.Unreachable) { return antecedent; } @@ -1374,32 +1379,32 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { return antecedent; } setFlowNodeReferenced(antecedent); - return initFlowNode({ flags, antecedent, node: expression }); + return createFlowNode(flags, expression, antecedent) as FlowCondition; } - function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode { + function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { setFlowNodeReferenced(antecedent); - return initFlowNode({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd }); + return createFlowNode(FlowFlags.SwitchClause, { switchStatement, clauseStart, clauseEnd }, antecedent) as FlowSwitchClause; } - function createFlowMutation(flags: FlowFlags, antecedent: FlowNode, node: Expression | VariableDeclaration | ArrayBindingElement): FlowNode { + function createFlowMutation(flags: FlowFlags.Assignment | FlowFlags.ArrayMutation, antecedent: FlowNode, node: Expression | VariableDeclaration | ArrayBindingElement) { setFlowNodeReferenced(antecedent); hasFlowEffects = true; - const result = initFlowNode({ flags, antecedent, node }); + const result = createFlowNode(flags, node, antecedent) as FlowAssignment | FlowArrayMutation; if (currentExceptionTarget) { addAntecedent(currentExceptionTarget, result); } return result; } - function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode { + function createFlowCall(antecedent: FlowNode, node: CallExpression) { setFlowNodeReferenced(antecedent); hasFlowEffects = true; - return initFlowNode({ flags: FlowFlags.Call, antecedent, node }); + return createFlowNode(FlowFlags.Call, node, antecedent) as FlowCall; } function finishFlowLabel(flow: FlowLabel): FlowNode { - const antecedents = flow.antecedents; + const antecedents = flow.antecedent; if (!antecedents) { return unreachableFlow; } @@ -1658,7 +1663,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { // set of antecedents for the pre-finally label. As control flow analysis passes by a ReduceLabel // node, the pre-finally label is temporarily switched to the reduced antecedent set. const finallyLabel = createBranchLabel(); - finallyLabel.antecedents = concatenate(concatenate(normalExitLabel.antecedents, exceptionLabel.antecedents), returnLabel.antecedents); + finallyLabel.antecedent = concatenate(concatenate(normalExitLabel.antecedent, exceptionLabel.antecedent), returnLabel.antecedent); currentFlow = finallyLabel; bind(node.finallyBlock); if (currentFlow.flags & FlowFlags.Unreachable) { @@ -1668,18 +1673,18 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { else { // If we have an IIFE return target and return statements in the try or catch blocks, add a control // flow that goes back through the finally block and back through only the return statements. - if (currentReturnTarget && returnLabel.antecedents) { - addAntecedent(currentReturnTarget, createReduceLabel(finallyLabel, returnLabel.antecedents, currentFlow)); + if (currentReturnTarget && returnLabel.antecedent) { + addAntecedent(currentReturnTarget, createReduceLabel(finallyLabel, returnLabel.antecedent, currentFlow)); } // If we have an outer exception target (i.e. a containing try-finally or try-catch-finally), add a // control flow that goes back through the finally blok and back through each possible exception source. - if (currentExceptionTarget && exceptionLabel.antecedents) { - addAntecedent(currentExceptionTarget, createReduceLabel(finallyLabel, exceptionLabel.antecedents, currentFlow)); + if (currentExceptionTarget && exceptionLabel.antecedent) { + addAntecedent(currentExceptionTarget, createReduceLabel(finallyLabel, exceptionLabel.antecedent, currentFlow)); } // If the end of the finally block is reachable, but the end of the try and catch blocks are not, // convert the current flow to unreachable. For example, 'try { return 1; } finally { ... }' should // result in an unreachable current control flow. - currentFlow = normalExitLabel.antecedents ? createReduceLabel(finallyLabel, normalExitLabel.antecedents, currentFlow) : unreachableFlow; + currentFlow = normalExitLabel.antecedent ? createReduceLabel(finallyLabel, normalExitLabel.antecedent, currentFlow) : unreachableFlow; } } else { @@ -1700,7 +1705,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { // We mark a switch statement as possibly exhaustive if it has no default clause and if all // case clauses have unreachable end points (e.g. they all return). Note, we no longer need // this property in control flow analysis, it's there only for backwards compatibility. - node.possiblyExhaustive = !hasDefault && !postSwitchLabel.antecedents; + node.possiblyExhaustive = !hasDefault && !postSwitchLabel.antecedent; if (!hasDefault) { addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0)); } @@ -1712,7 +1717,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { function bindCaseBlock(node: CaseBlock): void { const clauses = node.clauses; const isNarrowingSwitch = node.parent.expression.kind === SyntaxKind.TrueKeyword || isNarrowingExpression(node.parent.expression); - let fallthroughFlow = unreachableFlow; + let fallthroughFlow: FlowNode = unreachableFlow; for (let i = 0; i < clauses.length; i++) { const clauseStart = i; @@ -2432,7 +2437,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { const host = typeAlias.parent.parent; container = (getEnclosingContainer(host) as IsContainer | undefined) || file; blockScopeContainer = (getEnclosingBlockScopeContainer(host) as IsBlockScopedContainer | undefined) || file; - currentFlow = initFlowNode({ flags: FlowFlags.Start }); + currentFlow = createFlowNode(FlowFlags.Start, /*node*/ undefined, /*antecedent*/ undefined); parent = typeAlias; bind(typeAlias.typeExpression); const declName = getNameOfDeclaration(typeAlias); @@ -2504,7 +2509,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { const enclosingBlockScopeContainer = host ? getEnclosingBlockScopeContainer(host) as IsBlockScopedContainer | undefined : undefined; container = enclosingContainer || file; blockScopeContainer = enclosingBlockScopeContainer || file; - currentFlow = initFlowNode({ flags: FlowFlags.Start }); + currentFlow = createFlowNode(FlowFlags.Start, /*node*/ undefined, /*antecedent*/ undefined); parent = jsDocImportTag; bind(jsDocImportTag.importClause); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3d8ab4ee2124a..8b1477961ab2d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -111,6 +111,7 @@ import { createEmptyExports, createEvaluator, createFileDiagnostic, + createFlowNode, createGetCanonicalFileName, createGetSymbolWalker, createModeAwareCacheKey, @@ -211,6 +212,7 @@ import { FlowReduceLabel, FlowStart, FlowSwitchClause, + FlowSwitchClauseData, FlowType, forEach, forEachChild, @@ -27143,7 +27145,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function getFlowNodeId(flow: FlowNode): number { - if (!flow.id || flow.id < 0) { + if (flow.id <= 0) { flow.id = nextFlowId; nextFlowId++; } @@ -27942,10 +27944,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } else if (flags & FlowFlags.BranchLabel) { // A branching point is reachable if any branch is reachable. - return some((flow as FlowLabel).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); + return some((flow as FlowLabel).antecedent, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); } else if (flags & FlowFlags.LoopLabel) { - const antecedents = (flow as FlowLabel).antecedents; + const antecedents = (flow as FlowLabel).antecedent; if (antecedents === undefined || antecedents.length === 0) { return false; } @@ -27955,7 +27957,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else if (flags & FlowFlags.SwitchClause) { // The control flow path representing an unmatched value in a switch statement with // no default clause is unreachable if the switch statement is exhaustive. - if ((flow as FlowSwitchClause).clauseStart === (flow as FlowSwitchClause).clauseEnd && isExhaustiveSwitchStatement((flow as FlowSwitchClause).switchStatement)) { + const data = (flow as FlowSwitchClause).node; + if (data.clauseStart === data.clauseEnd && isExhaustiveSwitchStatement(data.switchStatement)) { return false; } flow = (flow as FlowSwitchClause).antecedent; @@ -27963,11 +27966,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else if (flags & FlowFlags.ReduceLabel) { // Cache is unreliable once we start adjusting labels lastFlowNode = undefined; - const target = (flow as FlowReduceLabel).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow as FlowReduceLabel).antecedents; + const target = (flow as FlowReduceLabel).node.target; + const saveAntecedents = target.antecedent; + target.antecedent = (flow as FlowReduceLabel).node.antecedents; const result = isReachableFlowNodeWorker((flow as FlowReduceLabel).antecedent, /*noCacheCheck*/ false); - target.antecedents = saveAntecedents; + target.antecedent = saveAntecedents; return result; } else { @@ -28000,18 +28003,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } else if (flags & FlowFlags.BranchLabel) { // A branching point is post-super if every branch is post-super. - return every((flow as FlowLabel).antecedents, f => isPostSuperFlowNode(f, /*noCacheCheck*/ false)); + return every((flow as FlowLabel).antecedent, f => isPostSuperFlowNode(f, /*noCacheCheck*/ false)); } else if (flags & FlowFlags.LoopLabel) { // A loop is post-super if the control flow path that leads to the top is post-super. - flow = (flow as FlowLabel).antecedents![0]; + flow = (flow as FlowLabel).antecedent![0]; } else if (flags & FlowFlags.ReduceLabel) { - const target = (flow as FlowReduceLabel).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow as FlowReduceLabel).antecedents; + const target = (flow as FlowReduceLabel).node.target; + const saveAntecedents = target.antecedent; + target.antecedent = (flow as FlowReduceLabel).node.antecedents; const result = isPostSuperFlowNode((flow as FlowReduceLabel).antecedent, /*noCacheCheck*/ false); - target.antecedents = saveAntecedents; + target.antecedent = saveAntecedents; return result; } else { @@ -28124,8 +28127,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { type = getTypeAtSwitchClause(flow as FlowSwitchClause); } else if (flags & FlowFlags.Label) { - if ((flow as FlowLabel).antecedents!.length === 1) { - flow = (flow as FlowLabel).antecedents![0]; + if ((flow as FlowLabel).antecedent!.length === 1) { + flow = (flow as FlowLabel).antecedent![0]; continue; } type = flags & FlowFlags.BranchLabel ? @@ -28140,11 +28143,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } else if (flags & FlowFlags.ReduceLabel) { - const target = (flow as FlowReduceLabel).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow as FlowReduceLabel).antecedents; + const target = (flow as FlowReduceLabel).node.target; + const saveAntecedents = target.antecedent; + target.antecedent = (flow as FlowReduceLabel).node.antecedents; type = getTypeAtFlowNode((flow as FlowReduceLabel).antecedent); - target.antecedents = saveAntecedents; + target.antecedent = saveAntecedents; } else if (flags & FlowFlags.Start) { // Check if we should continue with the control flow of the containing function. @@ -28331,30 +28334,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function getTypeAtSwitchClause(flow: FlowSwitchClause): FlowType { - const expr = skipParentheses(flow.switchStatement.expression); + const expr = skipParentheses(flow.node.switchStatement.expression); const flowType = getTypeAtFlowNode(flow.antecedent); let type = getTypeFromFlowType(flowType); if (isMatchingReference(reference, expr)) { - type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + type = narrowTypeBySwitchOnDiscriminant(type, flow.node); } else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) { - type = narrowTypeBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + type = narrowTypeBySwitchOnTypeOf(type, flow.node); } else if (expr.kind === SyntaxKind.TrueKeyword) { - type = narrowTypeBySwitchOnTrue(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + type = narrowTypeBySwitchOnTrue(type, flow.node); } else { if (strictNullChecks) { if (optionalChainContainsReference(expr, reference)) { - type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & (TypeFlags.Undefined | TypeFlags.Never))); + type = narrowTypeBySwitchOptionalChainContainment(type, flow.node, t => !(t.flags & (TypeFlags.Undefined | TypeFlags.Never))); } else if (expr.kind === SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as TypeOfExpression).expression, reference)) { - type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & TypeFlags.Never || t.flags & TypeFlags.StringLiteral && (t as StringLiteralType).value === "undefined")); + type = narrowTypeBySwitchOptionalChainContainment(type, flow.node, t => !(t.flags & TypeFlags.Never || t.flags & TypeFlags.StringLiteral && (t as StringLiteralType).value === "undefined")); } } const access = getDiscriminantPropertyAccess(expr, type); if (access) { - type = narrowTypeBySwitchOnDiscriminantProperty(type, access, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + type = narrowTypeBySwitchOnDiscriminantProperty(type, access, flow.node); } } return createFlowType(type, isIncomplete(flowType)); @@ -28365,8 +28368,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let subtypeReduction = false; let seenIncomplete = false; let bypassFlow: FlowSwitchClause | undefined; - for (const antecedent of flow.antecedents!) { - if (!bypassFlow && antecedent.flags & FlowFlags.SwitchClause && (antecedent as FlowSwitchClause).clauseStart === (antecedent as FlowSwitchClause).clauseEnd) { + for (const antecedent of flow.antecedent!) { + if (!bypassFlow && antecedent.flags & FlowFlags.SwitchClause && (antecedent as FlowSwitchClause).node.clauseStart === (antecedent as FlowSwitchClause).node.clauseEnd) { // The antecedent is the bypass branch of a potentially exhaustive switch statement. bypassFlow = antecedent as FlowSwitchClause; continue; @@ -28397,7 +28400,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If the bypass flow contributes a type we haven't seen yet and the switch statement // isn't exhaustive, process the bypass flow type. Since exhaustiveness checks increase // the risk of circularities, we only want to perform them when they make a difference. - if (!(type.flags & TypeFlags.Never) && !contains(antecedentTypes, type) && !isExhaustiveSwitchStatement(bypassFlow.switchStatement)) { + if (!(type.flags & TypeFlags.Never) && !contains(antecedentTypes, type) && !isExhaustiveSwitchStatement(bypassFlow.node.switchStatement)) { if (type === declaredType && declaredType === initialType) { return type; } @@ -28445,7 +28448,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const antecedentTypes: Type[] = []; let subtypeReduction = false; let firstAntecedentType: FlowType | undefined; - for (const antecedent of flow.antecedents!) { + for (const antecedent of flow.antecedent!) { let flowType; if (!firstAntecedentType) { // The first antecedent of a loop junction is always the non-looping control @@ -28609,15 +28612,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return narrowTypeByDiscriminant(type, access, t => narrowTypeByEquality(t, operator, value, assumeTrue)); } - function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { - if (clauseStart < clauseEnd && type.flags & TypeFlags.Union && getKeyPropertyName(type as UnionType) === getAccessedPropertyName(access)) { - const clauseTypes = getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd); + function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, data: FlowSwitchClauseData) { + if (data.clauseStart < data.clauseEnd && type.flags & TypeFlags.Union && getKeyPropertyName(type as UnionType) === getAccessedPropertyName(access)) { + const clauseTypes = getSwitchClauseTypes(data.switchStatement).slice(data.clauseStart, data.clauseEnd); const candidate = getUnionType(map(clauseTypes, t => getConstituentTypeForKeyType(type as UnionType, t) || unknownType)); if (candidate !== unknownType) { return candidate; } } - return narrowTypeByDiscriminant(type, access, t => narrowTypeBySwitchOnDiscriminant(t, switchStatement, clauseStart, clauseEnd)); + return narrowTypeByDiscriminant(type, access, t => narrowTypeBySwitchOnDiscriminant(t, data)); } function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type { @@ -28857,12 +28860,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { getAdjustedTypeWithFacts(type, typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject); } - function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) { + function narrowTypeBySwitchOptionalChainContainment(type: Type, { switchStatement, clauseStart, clauseEnd }: FlowSwitchClauseData, clauseCheck: (type: Type) => boolean) { const everyClauseChecks = clauseStart !== clauseEnd && every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck); return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; } - function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { + function narrowTypeBySwitchOnDiscriminant(type: Type, { switchStatement, clauseStart, clauseEnd }: FlowSwitchClauseData) { // We only narrow if all case expressions specify // values with unit types, except for the case where // `type` is unknown. In this instance we map object @@ -28943,7 +28946,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { neverType); } - function narrowTypeBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type { + function narrowTypeBySwitchOnTypeOf(type: Type, { switchStatement, clauseStart, clauseEnd }: FlowSwitchClauseData): Type { const witnesses = getSwitchClauseTypeOfWitnesses(switchStatement); if (!witnesses) { return type; @@ -28961,7 +28964,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return getUnionType(map(clauseWitnesses, text => text ? narrowTypeByTypeName(type, text) : neverType)); } - function narrowTypeBySwitchOnTrue(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type { + function narrowTypeBySwitchOnTrue(type: Type, { switchStatement, clauseStart, clauseEnd }: FlowSwitchClauseData): Type { const defaultIndex = findIndex(switchStatement.caseBlock.clauses, clause => clause.kind === SyntaxKind.DefaultClause); const hasDefaultClause = clauseStart === clauseEnd || (defaultIndex >= clauseStart && defaultIndex < clauseEnd); @@ -37831,23 +37834,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function checkIfExpressionRefinesParameter(func: FunctionLikeDeclaration, expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode || - { flags: FlowFlags.Start }; - const trueCondition: FlowCondition = { - flags: FlowFlags.TrueCondition, - antecedent, - node: expr, - }; + createFlowNode(FlowFlags.Start, /*node*/ undefined, /*antecedent*/ undefined); + const trueCondition = createFlowNode(FlowFlags.TrueCondition, expr, antecedent); const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); if (trueType === initType) return undefined; // "x is T" means that x is T if and only if it returns true. If it returns false then x is not T. // This means that if the function is called with an argument of type trueType, there can't be anything left in the `else` branch. It must reduce to `never`. - const falseCondition: FlowCondition = { - flags: FlowFlags.FalseCondition, - antecedent, - node: expr, - }; + const falseCondition = createFlowNode(FlowFlags.FalseCondition, expr, antecedent); const falseSubtype = getFlowTypeOfReference(param.name, initType, trueType, func, falseCondition); return falseSubtype.flags & TypeFlags.Never ? trueType : undefined; } diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 8912ffee391f1..5f516bbfbc321 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -10,7 +10,6 @@ import { FlowFlags, FlowLabel, FlowNode, - FlowNodeBase, FlowSwitchClause, getEffectiveModifierFlagsNoCache, getEmitFlags, @@ -506,14 +505,14 @@ export namespace Debug { let isDebugInfoEnabled = false; - let flowNodeProto: FlowNodeBase | undefined; + let flowNodeProto: FlowNode | undefined; - function attachFlowNodeDebugInfoWorker(flowNode: FlowNodeBase) { + function attachFlowNodeDebugInfoWorker(flowNode: FlowNode) { if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line local/no-in-operator Object.defineProperties(flowNode, { // for use with vscode-js-debug's new customDescriptionGenerator in launch.json __tsDebuggerDisplay: { - value(this: FlowNodeBase) { + value(this: FlowNode) { const flowHeader = this.flags & FlowFlags.Start ? "FlowStart" : this.flags & FlowFlags.BranchLabel ? "FlowBranchLabel" : this.flags & FlowFlags.LoopLabel ? "FlowLoopLabel" : @@ -531,12 +530,12 @@ export namespace Debug { }, }, __debugFlowFlags: { - get(this: FlowNodeBase) { + get(this: FlowNode) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); }, }, __debugToString: { - value(this: FlowNodeBase) { + value(this: FlowNode) { return formatControlFlowGraph(this); }, }, @@ -544,13 +543,13 @@ export namespace Debug { } } - export function attachFlowNodeDebugInfo(flowNode: FlowNodeBase) { + export function attachFlowNodeDebugInfo(flowNode: FlowNode) { if (isDebugInfoEnabled) { if (typeof Object.setPrototypeOf === "function") { // if we're in es2015, attach the method to a shared prototype for `FlowNode` // so the method doesn't show up in the watch window. if (!flowNodeProto) { - flowNodeProto = Object.create(Object.prototype) as FlowNodeBase; + flowNodeProto = Object.create(Object.prototype) as FlowNode; attachFlowNodeDebugInfoWorker(flowNodeProto); } Object.setPrototypeOf(flowNode, flowNodeProto); @@ -560,6 +559,7 @@ export namespace Debug { attachFlowNodeDebugInfoWorker(flowNode); } } + return flowNode; } let nodeArrayProto: NodeArray | undefined; @@ -953,8 +953,8 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n") return !!(f.flags & FlowFlags.SwitchClause); } - function hasAntecedents(f: FlowNode): f is FlowLabel & { antecedents: FlowNode[]; } { - return !!(f.flags & FlowFlags.Label) && !!(f as FlowLabel).antecedents; + function hasAntecedents(f: FlowNode): f is FlowLabel & { antecedent: FlowNode[]; } { + return !!(f.flags & FlowFlags.Label) && !!(f as FlowLabel).antecedent; } function hasAntecedent(f: FlowNode): f is Extract { @@ -1008,7 +1008,7 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n") links[id] = graphNode = { id, flowNode, edges: [], text: "", lane: -1, endLane: -1, level: -1, circular: false }; nodes.push(graphNode); if (hasAntecedents(flowNode)) { - for (const antecedent of flowNode.antecedents) { + for (const antecedent of flowNode.antecedent) { buildGraphEdge(graphNode, antecedent, seen); } } @@ -1097,15 +1097,11 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n") if (circular) { text = `${text}#${getDebugFlowNodeId(flowNode)}`; } - if (hasNode(flowNode)) { - if (flowNode.node) { - text += ` (${getNodeText(flowNode.node)})`; - } - } - else if (isFlowSwitchClause(flowNode)) { + if (isFlowSwitchClause(flowNode)) { const clauses: string[] = []; - for (let i = flowNode.clauseStart; i < flowNode.clauseEnd; i++) { - const clause = flowNode.switchStatement.caseBlock.clauses[i]; + const { switchStatement, clauseStart, clauseEnd } = flowNode.node; + for (let i = clauseStart; i < clauseEnd; i++) { + const clause = switchStatement.caseBlock.clauses[i]; if (isDefaultClause(clause)) { clauses.push("default"); } @@ -1115,6 +1111,11 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n") } text += ` (${clauses.join(", ")})`; } + else if (hasNode(flowNode)) { + if (flowNode.node) { + text += ` (${getNodeText(flowNode.node)})`; + } + } return circular === "circularity" ? `Circular(${text})` : text; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f3ac579029fc3..80288c6883dd8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4092,6 +4092,7 @@ export const enum FlowFlags { /** @internal */ export type FlowNode = + | FlowUnreachable | FlowStart | FlowLabel | FlowAssignment @@ -4104,7 +4105,15 @@ export type FlowNode = /** @internal */ export interface FlowNodeBase { flags: FlowFlags; - id?: number; // Node id used by flow type cache in checker + id: number; // Node id used by flow type cache in checker + node: unknown; // Node or other data + antecedent: FlowNode | FlowNode[] | undefined; +} + +/** @internal */ +export interface FlowUnreachable extends FlowNodeBase { + node: undefined; + antecedent: undefined; } // FlowStart represents the start of a control flow. For a function expression or arrow @@ -4112,13 +4121,15 @@ export interface FlowNodeBase { // property for the containing control flow). /** @internal */ export interface FlowStart extends FlowNodeBase { - node?: FunctionExpression | ArrowFunction | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration; + node: FunctionExpression | ArrowFunction | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | undefined; + antecedent: undefined; } // FlowLabel represents a junction with multiple possible preceding control flows. /** @internal */ export interface FlowLabel extends FlowNodeBase { - antecedents: FlowNode[] | undefined; + node: undefined; + antecedent: FlowNode[] | undefined; } // FlowAssignment represents a node that assigns a value to a narrowable reference, @@ -4146,12 +4157,17 @@ export interface FlowCondition extends FlowNodeBase { // dprint-ignore /** @internal */ export interface FlowSwitchClause extends FlowNodeBase { - switchStatement: SwitchStatement; - clauseStart: number; // Start index of case/default clause range - clauseEnd: number; // End index of case/default clause range + node: FlowSwitchClauseData; antecedent: FlowNode; } +/** @internal */ +export interface FlowSwitchClauseData { + switchStatement: SwitchStatement; + clauseStart: number; // Start index of case/default clause range + clauseEnd: number; // End index of case/default clause range +} + // FlowArrayMutation represents a node potentially mutates an array, i.e. an // operation of the form 'x.push(value)', 'x.unshift(value)' or 'x[n] = value'. /** @internal */ @@ -4162,9 +4178,14 @@ export interface FlowArrayMutation extends FlowNodeBase { /** @internal */ export interface FlowReduceLabel extends FlowNodeBase { + node: FlowReduceLabelData; + antecedent: FlowNode; +} + +/** @internal */ +export interface FlowReduceLabelData { target: FlowLabel; antecedents: FlowNode[]; - antecedent: FlowNode; } export type FlowType = Type | IncompleteType;