Skip to content
Merged
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
24 changes: 24 additions & 0 deletions internal/graph/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,11 +720,35 @@ func affectedCallers(idx *edgeIndex, symbols []core.SymbolRecord, changedFiles m
// even when the changed file is out of scope.
if callSitesTouchNames(s.CallSites, nameDelta) || callSitesTouchNames(s.AttrSites, nameDelta) {
affected[s.ID] = true
continue
}
// TS member reads/writes (tsMemberRefEdges) come from the body text,
// not from sites: `inst._zod.parse = wrapped` re-binds when a `_zod`
// or `parse` declaration changes anywhere (fuzz 2026-10-16: a new
// file declaring another `_zod` left zod memoizer's write edges stale).
if tsFamilyLang(s.Language) && dottedTextTouchesNames(s.RawText, nameDelta) {
affected[s.ID] = true
}
}
return affected, true
}

// dottedTextTouchesNames reports whether any segment of a dotted chain in
// text (`a.b.c`) is in the lowercased name-delta set.
func dottedTextTouchesNames(text string, nameDelta map[string]bool) bool {
if len(nameDelta) == 0 || !strings.Contains(text, ".") {
return false
}
for _, chain := range tsDottedChainRe.FindAllString(text, -1) {
for _, seg := range strings.Split(chain, ".") {
if nameDelta[strings.ToLower(strings.TrimSpace(seg))] {
return true
}
}
}
return false
}

// callSitesTouchNames reports whether any site's callee (bare name, method
// name, or receiver qualifier) appears in the lowercased name-delta set.
func callSitesTouchNames(sites []core.CallSite, nameDelta map[string]bool) bool {
Expand Down
52 changes: 52 additions & 0 deletions internal/graph/tspropertytypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,55 @@ func TestTSPropertyRule_GenericConstraintAlone(t *testing.T) {
t.Errorf("`_zod: I` with I extends $SubInternals must reach _$Internals.parse")
}
}

// The member reads/writes come from the caller's text, not its sites, so the
// incremental name rule must look at the text: a new file declaring another
// `_zod` (whose type has its own parse) makes the binding ambiguous, and a
// caller in an untouched file must drop its write edge as a full build does.
func TestTSPropertyRule_DeltaRebindsMemberRefsOnNewDeclaration(t *testing.T) {
prev := tsPropFixture()
for i := range prev {
if prev[i].ID == "s::top" {
prev[i].ID, prev[i].FilePath = "w::top", "w.ts"
prev[i].CallSites = prev[i].CallSites[:1] // reads/writes only: no `.parse(` call site
}
}
add := func(id string, kind core.SymbolKind, name, parent, sig, raw string, line int) core.SymbolRecord {
qn := name
if parent != "" {
qn = parent + "." + name
}
return core.SymbolRecord{ID: id, FilePath: "fz.ts", BlobSHA: "2", Language: "typescript", Kind: kind,
Name: name, QualifiedName: qn, ParentSymbol: parent, Signature: sig, RawText: raw,
Span: core.LineRange{Start: line, End: line}}
}
next := append(append([]core.SymbolRecord{}, prev...),
add("fz::Fz", core.KindInterface, "Fz", "", "interface Fz", "interface Fz { _zod: Fz2 }", 1),
add("fz::Fz._zod", core.KindField, "_zod", "Fz", "_zod: Fz2", "_zod: Fz2", 1),
add("fz::Fz2", core.KindInterface, "Fz2", "", "interface Fz2", "interface Fz2 { parse(x: number): void }", 2),
add("fz::Fz2.parse", core.KindMethod, "parse", "Fz2", "parse(x: number): void", "parse(x: number): void", 2),
)
memberRefs := func(edges []core.Edge) map[string]bool {
out := map[string]bool{}
for _, e := range edges {
if e.From == "w::top" && e.Reason == core.ReasonMemberRef {
out[string(e.Type)+" "+e.To+" "+e.Lines] = true
}
}
return out
}
prevEdges := BuildEdges(prev)
if len(memberRefs(prevEdges)) == 0 {
t.Fatal("fixture: the caller should have member-ref edges before the edit")
}
full := memberRefs(BuildEdges(next))
delta := memberRefs(BuildEdgesDelta(prevEdges, prev, next, map[string]bool{"fz.ts": true}, nil))
if len(full) != len(delta) {
t.Fatalf("delta member-ref edges %v, full build %v", delta, full)
}
for k := range full {
if !delta[k] {
t.Fatalf("delta member-ref edges %v, full build %v", delta, full)
}
}
}
Loading