Skip to content
Open
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
89 changes: 84 additions & 5 deletions tsc/internal/ast/flow.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ast

import "unsafe"
import (
"hash/maphash"
"unsafe"
)

// FlowFlags

Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions tsc/internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
34 changes: 34 additions & 0 deletions tsc/internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading