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
5 changes: 5 additions & 0 deletions internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ const (
ReasonFunctionRef EdgeReason = "function-ref" // C/C++ function passed as an argument (function-pointer reach)
ReasonFuncField EdgeReason = "func-field" // a call through a function-valued field reaches a function stored in it
ReasonImplicitDunder EdgeReason = "implicit-dunder" // language-implicit protocol method (e.g. Python __setattr__ on "x.y = v") with no call site in source
// ReasonMemberRef: a non-call reference to a method member through a
// resolved receiver (`inst._zod.parse = ...`, `const base =
// inst._zod.parse`), recorded as a writes/reads edge to the member's
// declaration: a rename or signature change breaks it.
ReasonMemberRef EdgeReason = "member-ref"
)

type EvidenceRef struct {
Expand Down
9 changes: 6 additions & 3 deletions internal/graph/changeimpact.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ type callerLines map[string]map[string]bool
func (c callerLines) note(e core.Edge, resolved callerLines) {
// A compiler-resolved value reference (`f := pkg.Do`) is an edit site
// like a call.
nativeRef := e.Source == core.EvidenceSourceNative && (e.Type == core.EdgeReads || e.Type == core.EdgeWrites)
nativeRef := e.Source == core.EvidenceSourceNative && (e.Type == core.EdgeReads || e.Type == core.EdgeWrites) ||
e.Reason == core.ReasonMemberRef
if e.Type != core.EdgeCalls && !nativeRef || e.Lines == "" {
return
}
Expand Down Expand Up @@ -596,7 +597,8 @@ func (g *CodeGraph) changeImpactScoped(query, file string) (*ChangeImpactResult,
// rename/signature change: this.match = match, Router<T>['match'],
// an object literal's match: (m, p) => ... typed as Router.
nativeRef := edge.Source == core.EvidenceSourceNative &&
(edge.Type == core.EdgeReads || edge.Type == core.EdgeWrites)
(edge.Type == core.EdgeReads || edge.Type == core.EdgeWrites) ||
edge.Reason == core.ReasonMemberRef // inst._zod.parse = ...
if edge.Type != core.EdgeCalls && !nativeRef {
continue
}
Expand Down Expand Up @@ -1289,7 +1291,8 @@ func (g *CodeGraph) freeFunctionImpactLocked(query, file string) *ChangeImpactRe
edge := g.edges[ei]
// A compiler-resolved value reference to a function
// (`apply(Handle)`) breaks under a signature change like a call.
nativeRef := edge.Source == core.EvidenceSourceNative && (edge.Type == core.EdgeReads || edge.Type == core.EdgeWrites)
nativeRef := edge.Source == core.EvidenceSourceNative && (edge.Type == core.EdgeReads || edge.Type == core.EdgeWrites) ||
edge.Reason == core.ReasonMemberRef
accept := edge.Type == core.EdgeCalls || nativeRef ||
(dataAnchor && (edge.Type == core.EdgeReads || edge.Type == core.EdgeWrites || edge.Type == core.EdgeRedefines))
if !accept {
Expand Down
12 changes: 12 additions & 0 deletions internal/graph/edges.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ type edgeIndex struct {
// (phpClassTraitRules) under a "php-traits\x00" key prefix.
classBodies sync.Map
maskedLines sync.Map
// tsPropMembers memoizes tspropertytypes.go lookups, each a function of
// the index alone: member targets ("m\x00prop\x00member"), property
// declarations ("d\x00prop"), masked type bodies ("b\x00id").
tsPropMembers sync.Map
// tsTypeContainersList: the TS/JS interfaces and type aliases, built
// once by tsTypeContainers.
tsTypeContainersOnce sync.Once
tsTypeContainersList []*core.SymbolRecord

// Rust crate topology: visibility in Rust is crate-wide (any item is
// reachable through crate:: paths without a per-file use), so scope is
Expand Down Expand Up @@ -2485,6 +2493,10 @@ func buildCalls(idx *edgeIndex, symbols []core.SymbolRecord, sat *interfaceSatis
// resolveCallEdges resolves one caller's outgoing call/property edges.
func resolveCallEdges(idx *edgeIndex, symbol core.SymbolRecord, sat *interfaceSatisfaction) []core.Edge {
edges := resolveSymbolCallEdges(idx, symbol, sat)
if tsFamilyLang(symbol.Language) {
// Non-call references to a method member (`inst._zod.parse = ...`).
edges = append(edges, tsMemberRefEdges(idx, &symbol)...)
}
if symbol.Language == "python" {
// Function values flowing through this symbol (pyvalueflow.go),
// whether or not it has call sites of its own.
Expand Down
4 changes: 2 additions & 2 deletions internal/graph/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ func BuildEdgesDeltaMetaWith(prevEdges []core.Edge, prevSymbols, symbols []core.

var prevCalls []core.Edge
for _, e := range prevEdges {
if e.Type != core.EdgeCalls && !resolverMarker(idx, e) {
continue
if e.Type != core.EdgeCalls && !resolverMarker(idx, e) && e.Reason != core.ReasonMemberRef {
continue // member refs come from the call pass (tsMemberRefEdges)
}
if e.Type == core.EdgeCalls && (e.Reason == core.ReasonFuncField ||
e.Reason == core.ReasonDispatch && e.Source == core.EvidenceSourceNative) {
Expand Down
2 changes: 1 addition & 1 deletion internal/graph/resolverversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "github.com/provasign/grove/internal/core"

// Bump when edge resolution changes for unchanged source. Stored call edges
// cannot be reused across this boundary merely because blob hashes match.
const ResolverVersion = "2026-10-18-language-graph-v40"
const ResolverVersion = "2026-10-16-language-graph-v41"

// CurrentNativeEdges keeps supplemental evidence that remains valid during
// a resolver upgrade. Legacy Python native calls were name-only bindings.
Expand Down
23 changes: 21 additions & 2 deletions internal/graph/tslocaltypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,20 @@ func rustBaseClasses(chosen *core.SymbolRecord) []string {
// inheritanceClauseTypes returns every top-level type in one extends or
// implements clause. Commas inside generic arguments do not split the list.
func inheritanceClauseTypes(sig, keyword string, stopKeywords ...string) []string {
clause, ok := inheritanceClauseText(sig, keyword, stopKeywords...)
if !ok {
return nil
}
return nominalTypeList(clause, false)
}

// inheritanceClauseText is the raw text of sig's keyword clause (after the
// keyword, up to a top-level stop keyword or '{'); ok is false when sig has
// no such clause.
func inheritanceClauseText(sig, keyword string, stopKeywords ...string) (string, bool) {
start := inheritanceKeyword(sig, keyword)
if start < 0 {
return nil
return "", false
}
rest := sig[start+len(keyword):]
end := len(rest)
Expand Down Expand Up @@ -218,7 +229,7 @@ func inheritanceClauseTypes(sig, keyword string, stopKeywords ...string) []strin
}
}
}
return nominalTypeList(rest[:end], false)
return rest[:end], true
}

func inheritanceKeyword(sig, keyword string) int {
Expand Down Expand Up @@ -965,6 +976,14 @@ func javaClassFieldTypes(idx *edgeIndex, className, preferFile string, out map[s
func narrowByChainType(idx *edgeIndex, sat *interfaceSatisfaction, localTypes map[string]string, symbol *core.SymbolRecord, fullChain, calleeName string, cands []*core.SymbolRecord) (kept, dispatch []*core.SymbolRecord, decided bool) {
typ := tsResolveReceiverChain(idx, localTypes, fullChain, symbol.FilePath, symbol.Language)
if typ == "" {
if tsFamilyLang(symbol.Language) {
// `inst._zod.parse(...)` with inst untyped: the property's
// project-wide declarations may still pin the member
// (tspropertytypes.go).
if targets := tsUntypedPropertyChainTargets(idx, symbol, localTypes, fullChain, calleeName); len(targets) > 0 {
return targets, nil, true
}
}
return cands, nil, false
}
if bare := strings.TrimPrefix(typ, "extern:"); tsFamilyLang(symbol.Language) &&
Expand Down
Loading
Loading