diff --git a/tsc/internal/ast/flow.go b/tsc/internal/ast/flow.go index 43a4bc2f0dfea..739876adc1b38 100644 --- a/tsc/internal/ast/flow.go +++ b/tsc/internal/ast/flow.go @@ -1,6 +1,9 @@ package ast -import "unsafe" +import ( + "hash/maphash" + "unsafe" +) // FlowFlags @@ -27,10 +30,86 @@ const ( // FlowNode type FlowNode struct { - Flags FlowFlags - Node *Node // Associated AST node - Antecedent *FlowNode // Antecedent for all but FlowLabel - Antecedents *FlowList // Linked list of antecedents for FlowLabel + Flags FlowFlags + // For assignment nodes, describes the identifier the assignment may target + // (see FlowAssignmentTargetHash). Fits in padding after Flags. + AssignmentTarget uint32 + Node *Node // Associated AST node + Antecedent *FlowNode // Antecedent for all but FlowLabel + Antecedents *FlowList // Linked list of antecedents for FlowLabel +} + +const ( + // The assignment must be checked against every reference. + FlowAssignmentTargetUnknown uint32 = 0 + // The assignment cannot target an identifier reference. + FlowAssignmentTargetNotIdentifier uint32 = 1 +) + +// FlowAssignmentTargetAccess marks an AssignmentTarget whose target is a property +// or element access chain rooted at the hashed identifier. +const FlowAssignmentTargetAccess uint32 = 1 << 31 + +var flowIdentifierHashSeed = maphash.MakeSeed() + +// FlowIdentifierHash hashes an identifier name for comparison with +// FlowNode.AssignmentTarget. The result is never a reserved value and never +// has FlowAssignmentTargetAccess set. +func FlowIdentifierHash(name string) uint32 { + h := uint32(maphash.String(flowIdentifierHashSeed, name)) + h &^= FlowAssignmentTargetAccess + if h <= FlowAssignmentTargetNotIdentifier { + h += 2 + } + return h +} + +// FlowAssignmentTargetHash summarizes which references an assignment flow node +// can affect, so that flow walks can skip assignments to unrelated names without +// visiting their syntax. An identifier or declaration target records the hash of +// its name. An access chain target records the hash of the identifier it is +// rooted at, with FlowAssignmentTargetAccess set. This mirrors the checker's +// matching of references against assignment targets, which only matches +// references rooted at the same identifier. +func FlowAssignmentTargetHash(target *Node) uint32 { + access := uint32(0) + for { + switch target.Kind { + case KindParenthesizedExpression, KindNonNullExpression: + target = target.Expression() + continue + case KindPropertyAccessExpression, KindElementAccessExpression: + access = FlowAssignmentTargetAccess + target = target.Expression() + continue + case KindBinaryExpression: + bin := target.AsBinaryExpression() + switch { + case IsAssignmentExpression(target, false): + target = bin.Left + continue + case bin.OperatorToken.Kind == KindCommaToken: + target = bin.Right + continue + } + return FlowAssignmentTargetNotIdentifier + case KindIdentifier: + return FlowIdentifierHash(target.AsIdentifier().Text) | access + case KindVariableDeclaration, KindBindingElement: + if access != 0 { + return FlowAssignmentTargetNotIdentifier + } + // A for-in variable also narrows the iterated expression. + if IsVariableDeclaration(target) && target.Parent != nil && target.Parent.Parent != nil && IsForInStatement(target.Parent.Parent) { + return FlowAssignmentTargetUnknown + } + if name := target.Name(); name != nil && IsIdentifier(name) { + return FlowIdentifierHash(name.AsIdentifier().Text) + } + return FlowAssignmentTargetNotIdentifier + } + return FlowAssignmentTargetNotIdentifier + } } type FlowList struct { diff --git a/tsc/internal/binder/binder.go b/tsc/internal/binder/binder.go index a95bb19ecdff7..2eb5a9db6f60a 100644 --- a/tsc/internal/binder/binder.go +++ b/tsc/internal/binder/binder.go @@ -500,6 +500,9 @@ func (b *Binder) createFlowMutation(flags ast.FlowFlags, antecedent *ast.FlowNod setFlowNodeReferenced(antecedent) b.hasFlowEffects = true result := b.newFlowNodeEx(flags, node, antecedent) + if flags&ast.FlowFlagsAssignment != 0 { + result.AssignmentTarget = ast.FlowAssignmentTargetHash(node) + } if b.currentExceptionTarget != nil { b.addAntecedent(b.currentExceptionTarget, result) } diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d2831231772fe..f0d4433161442 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -43,6 +43,8 @@ type FlowState struct { initialType *Type flowContainer *ast.Node refKey CacheHashKey + refNameHash uint32 // FlowIdentifierHash of the identifier a reference is rooted at, else 0 + refIsAccess bool // Whether the reference is an access chain rooted at that identifier depth int sharedFlowStart int reduceLabels []*ast.FlowReduceLabelData @@ -94,6 +96,7 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty f.initialType = core.Coalesce(initialType, declaredType) f.flowContainer = flowContainer f.sharedFlowStart = len(c.sharedFlows) + f.refNameHash, f.refIsAccess = getFlowReferenceRootHash(reference) c.flowInvocationCount++ evolvedType := c.getTypeAtFlowNode(f, flowNode).t c.sharedFlows = c.sharedFlows[:f.sharedFlowStart] @@ -114,6 +117,31 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty return resultType } +// getFlowReferenceRootHash returns the FlowIdentifierHash of the identifier a reference +// is rooted at, and whether the reference is an access chain on it. An identifier is +// only affected by assignments to that identifier. An access chain is also affected by +// assignments to access chains rooted at it. Other references return zero and are +// matched against every assignment. +func getFlowReferenceRootHash(reference *ast.Node) (uint32, bool) { + isAccess := false + for { + switch reference.Kind { + case ast.KindIdentifier: + if ast.IsThisInTypeQuery(reference) { + return 0, false + } + return ast.FlowIdentifierHash(reference.Text()), isAccess + case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression: + isAccess = true + reference = reference.Expression() + case ast.KindParenthesizedExpression, ast.KindNonNullExpression, ast.KindSatisfiesExpression: + reference = reference.Expression() + default: + return 0, false + } + } +} + func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType { if f.depth == 2000 { // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error @@ -144,6 +172,12 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType { var t FlowType switch { case flags&ast.FlowFlagsAssignment != 0: + if f.refNameHash != 0 && flow.AssignmentTarget != ast.FlowAssignmentTargetUnknown && + (flow.AssignmentTarget != f.refNameHash && (!f.refIsAccess || flow.AssignmentTarget != f.refNameHash|ast.FlowAssignmentTargetAccess)) { + // The assignment is rooted at a different name, so it cannot affect this reference. + flow = flow.Antecedent + continue + } t = c.getTypeAtFlowAssignment(f, flow) if t.isNil() { flow = flow.Antecedent