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
2 changes: 1 addition & 1 deletion internal/indexer/extractor_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var extractorVersions = map[string]int{
// "go": 2,
"c": generatedParserProjectionPolicyVersion, // generated parser projection covers all strictly detected table sizes
"php": 2, // class/interface inheritance now emits typed structural edges
"csharp": 11, // params parameters emit complete shape and arity evidence (was: qualified static-form extension calls pick the right overload)
"csharp": 12, // receiverless calls carry arg_count / type_arg_count for #559 (was: params parameters emit complete shape and arity evidence)
"scala": 2, // explicitly instantiated generic calls emit call edges
"go": 3, // generic instantiations are marked so indexing a func value cannot bind (was: generic calls emit call edges)
"cpp": 2, // templated and namespace-qualified calls emit call edges
Expand Down
4 changes: 2 additions & 2 deletions internal/indexer/extractor_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func TestStaleLangsDetection(t *testing.T) {
t.Errorf("stored pre-params C# version = %v, want [csharp]", got)
}
for _, path := range []string{"src/Handler.cs", "Views/Page.razor", "Views/Page.cshtml"} {
if got := merkleSaltFor(path); got != "csharp@11" {
t.Errorf("C# extractor salt for %s = %q, want csharp@11", path, got)
if got := merkleSaltFor(path); got != "csharp@12" {
t.Errorf("C# extractor salt for %s = %q, want csharp@12", path, got)
}
}
if got := merkleSaltFor("src/Handler.php"); got != "php@2" {
Expand Down
49 changes: 35 additions & 14 deletions internal/parser/languages/csharp.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,17 @@ func (e *CSharpExtractor) extractCSharp(filePath string, src []byte) (*parser.Ex
returnUsage: classifyReturnUsage(expr.Node, src, csharpReturnUsageSpec),
}, expr.Node))

// A receiverless call carries no applicability stamps: nothing
// resolves it through the extension binder, and the scope rules
// that do bind it never consult arity.
// Receiverless calls carry applicability stamps too: the
// same-file and locality tiers pick among ordinary overload
// sets, and without arg_count that pick is declaration order
// (#559).
case m.Captures["call.expr"] != nil:
expr := m.Captures["call.expr"]
calls = append(calls, csharpDeferredCall{
calls = append(calls, withCSharpCallArity(csharpDeferredCall{
name: m.Captures["call.name"].Text,
line: expr.StartLine + 1,
returnUsage: classifyReturnUsage(expr.Node, src, csharpReturnUsageSpec),
})
}, expr.Node))

case m.Captures["maccess.expr"] != nil:
accesses = append(accesses, csharpDeferredAccess{
Expand Down Expand Up @@ -715,6 +716,22 @@ func (e *CSharpExtractor) extractCSharp(filePath string, src []byte) (*parser.Ex
From: callerID, To: "unresolved::" + c.name,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
}
// Applicability evidence rides receiverless calls too: the
// resolver's same-file and locality tiers pick among ordinary
// overload sets, and without arg_count that pick is declaration
// order (#559).
if c.argKnown {
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["arg_count"] = c.argCount
}
if c.typeArgKnown {
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["type_arg_count"] = c.typeArgCount
}
stampReturnUsage(edge, c.returnUsage)
result.Edges = append(result.Edges, edge)
}
Expand Down Expand Up @@ -1251,10 +1268,22 @@ func (e *CSharpExtractor) emitMethod(m parser.QueryResult, filePath, fileID stri
// Extension method: a static method whose first parameter carries the
// `this` modifier. Record the receiver type it extends so member-call
// resolution can bind `x.Foo()` to it (the id stays <StaticClass>.<name>).
// The method's own type parameters are applicability evidence for
// EVERY method, not just extensions: an explicit `Foo<int>(x)` call
// splits a generic/non-generic ordinary overload pair only when the
// generic one is stamped (#559).
tparams := csharpMethodTypeParamNames(def.Node, src)
if len(tparams) > 0 {
names := make([]string, 0, len(tparams))
for n := range tparams {
names = append(names, n)
}
sort.Strings(names)
meta["method_type_params"] = strings.Join(names, ",")
}
if extType := csharpExtensionReceiverType(def.Node, src); extType != "" {
meta["extension"] = true
meta["this_param_type"] = extType
tparams := csharpMethodTypeParamNames(def.Node, src)
// `Foo<T>(this T v)` — the this-param names the method's own
// type parameter, i.e. it matches any receiver; the binder must
// not treat it as a concrete type named "T". A `where T : X`
Expand All @@ -1273,14 +1302,6 @@ func (e *CSharpExtractor) emitMethod(m parser.QueryResult, filePath, fileID stri
meta["this_param_shape"] = shape
}
}
if len(tparams) > 0 {
names := make([]string, 0, len(tparams))
for n := range tparams {
names = append(names, n)
}
sort.Strings(names)
meta["method_type_params"] = strings.Join(names, ",")
}
}
// Parameter arity — the evidence that splits a same-name overload set
// the receiver type alone cannot. Stamped on the node rather than read
Expand Down
11 changes: 8 additions & 3 deletions internal/parser/languages/csharp_overload_evidence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,14 @@ func TestCSharpExtractor_TypeArgCountAcrossCallShapes(t *testing.T) {

bare := callEdgesFrom(result.Edges, "App.cs::Runner.Bare", "Helper")
require.Len(t, bare, 1, "the receiverless call still emits its edge")
assert.NotContains(t, bare[0].Meta, "type_arg_count",
"a receiverless call has no extension binder to narrow, so it carries no stamp")
assert.NotContains(t, bare[0].Meta, "arg_count")
// #559: receiverless calls carry the applicability stamps too — the
// resolver's same-file and locality tiers pick among ordinary
// overload sets, and without arg_count that pick is declaration
// order.
assert.Equal(t, 1, bare[0].Meta["type_arg_count"],
"a receiverless generic call spells its type argument")
assert.Equal(t, 0, bare[0].Meta["arg_count"],
"zero arguments is a measured count, not missing evidence")
}

// The declaration side of the same evidence. `param_required` is stamped
Expand Down
49 changes: 49 additions & 0 deletions internal/resolver/csharp_applicability.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,55 @@ func csharpMethodTypeParamCount(c *graph.Node) int {
return n
}

// csharpNarrowMethodByApplicability filters a same-name ORDINARY C#
// candidate set (instance/static methods, not extensions) to the members
// the call site could invoke. Same shape as csharpNarrowByApplicability,
// with ordinary-arity semantics in the arg-count half — see
// csharpMethodAcceptsArgCount (#559).
func csharpNarrowMethodByApplicability(e *graph.Edge, cands []*graph.Node) []*graph.Node {
if len(cands) < 2 || e == nil || e.Meta == nil {
return cands
}
out := csharpKeepIf(cands, func(c *graph.Node) bool {
return csharpAcceptsTypeArgCount(e, c)
})
return csharpKeepIf(out, func(c *graph.Node) bool {
return csharpMethodAcceptsArgCount(e, c)
})
}

// csharpMethodAcceptsArgCount reports whether an ordinary C# method can
// accept the call's argument count — the [required, declared] window,
// widened by a trailing `params` array.
//
// Two deliberate differences from csharpExtensionAcceptsArgCount: there
// is no `this` slot to discount, and param_count == 0 is a REAL arity —
// an extension method always declares its `this` parameter, so only
// there does zero mean "no evidence".
func csharpMethodAcceptsArgCount(e *graph.Edge, c *graph.Node) bool {
argc, ok := metaIntValue(e.Meta["arg_count"])
if !ok || c == nil || c.Meta == nil {
return true
}
if isCSharpExtension(c) {
// Extensions are adjudicated by their own binder with the
// `this`-slot discount; the ordinary window would misread them.
return true
}
count, ok := metaIntValue(c.Meta["param_count"])
if !ok {
return true // no stamp — an older graph or an unexposed parameter list
}
required := count
if r, rok := metaIntValue(c.Meta["param_required"]); rok {
required = r
}
if variadic, _ := c.Meta["param_variadic"].(bool); variadic {
return argc >= required
}
return argc >= required && argc <= count
}

// csharpExtensionAcceptsArgCount reports whether a candidate extension
// method can accept the call's argument count.
//
Expand Down
Loading
Loading