mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 16:22:30 +00:00
Applied from PR #4591 (authorship preserved); stale cache-schema bump dropped for one combined bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
14514b37f6
commit
87151e40ac
7 files changed
+518
-38
No files matched your search
+61
-2
@@ -183,7 +183,7 @@ from graphify.extractors.resolution import ( # noqa: E402,F401
|
||||
|
||||
from graphify.symbol_resolution import resolve_bash_source_edges # noqa: E402
|
||||
|
||||
from graphify.extractors.engine import PYTEST_FIXTURE_UNTYPED, REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401
|
||||
from graphify.extractors.engine import PYTEST_FIXTURE_UNTYPED, REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_namespace_id, _csharp_namespace_name, _csharp_pick_overload, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401
|
||||
|
||||
from graphify.extractors.pascal import _PAS_BEGIN_END_TOKEN_RE, _PAS_CALL_RE, _PAS_END_SEMI_RE, _PAS_IMPL_HEADER_RE, _PAS_KEYWORDS, _PAS_METHOD_DECL_RE, _PAS_MODULE_RE, _PAS_TOKEN_RE, _PAS_TYPE_HEADER_RE, _PAS_USES_RE, _extract_pascal_regex, _pascal_find_body, _pascal_split_bases, _pascal_split_sections, _pascal_split_uses, _pascal_strip_comments, extract_pascal # noqa: E402,F401
|
||||
|
||||
@@ -5534,6 +5534,46 @@ def _resolve_cpp_member_calls(
|
||||
})
|
||||
|
||||
|
||||
def _csharp_overload_index(nodes: list[dict], edges: list[dict]) -> dict:
|
||||
"""Method nodes by id and their owning type, for C# methods with an arity."""
|
||||
node_by_id = {n.get("id"): n for n in nodes}
|
||||
return {
|
||||
"node": node_by_id,
|
||||
"owner": {
|
||||
e.get("target"): e.get("source") for e in edges
|
||||
if e.get("relation") == "method"
|
||||
and "arity" in ((node_by_id.get(e.get("target")) or {}).get("metadata") or {})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _csharp_collapse_overloads(candidates: list[str], argc: object, index: dict) -> list[str]:
|
||||
"""Fold the overloads of one C# method among ``candidates`` into one (#4584).
|
||||
|
||||
Overloads of a name on one type are separate nodes, so a bare-name lookup
|
||||
sees them as rival definitions and, all in one file, no tie-breaker can
|
||||
choose. The call's argument count picks the overload when exactly one fits;
|
||||
otherwise the first declared one stands for the name, as it did when the
|
||||
overloads were a single node. ``index`` is ``_csharp_overload_index``.
|
||||
"""
|
||||
owner = index["owner"]
|
||||
groups: dict[tuple, list[str]] = {}
|
||||
for c in candidates:
|
||||
groups.setdefault((owner[c],) if c in owner else (None, c), []).append(c)
|
||||
if len(groups) == len(candidates):
|
||||
return candidates
|
||||
keep: set[str] = set()
|
||||
for members in groups.values():
|
||||
if len(members) == 1:
|
||||
keep.add(members[0])
|
||||
continue
|
||||
picked = _csharp_pick_overload(
|
||||
[index["node"][m] for m in members], argc if isinstance(argc, int) else None
|
||||
)
|
||||
keep.add(picked["id"] if picked is not None else members[0])
|
||||
return [c for c in candidates if c in keep]
|
||||
|
||||
|
||||
def _resolve_csharp_member_calls(
|
||||
per_file: list[dict],
|
||||
all_nodes: list[dict],
|
||||
@@ -5595,6 +5635,7 @@ def _resolve_csharp_member_calls(
|
||||
# (type_node_id, method_key) -> method_node_id, and caller -> enclosing type.
|
||||
# C# owns its methods via `method` edges.
|
||||
method_index: dict[tuple[str, str], str] = {}
|
||||
method_peers: dict[tuple[str, str], list[dict]] = {}
|
||||
enclosing_type: dict[str, str] = {}
|
||||
for e in all_edges:
|
||||
if e.get("relation") != "method":
|
||||
@@ -5604,7 +5645,11 @@ def _resolve_csharp_member_calls(
|
||||
if tnode is None:
|
||||
continue
|
||||
enclosing_type.setdefault(tgt, src)
|
||||
method_index[(src, _key(tnode.get("label", "")))] = tgt
|
||||
# Overloads share a key: the first declared one stands for the name,
|
||||
# and the call's argument count picks among them below (#4584).
|
||||
method_key = (src, _key(tnode.get("label", "")))
|
||||
method_index.setdefault(method_key, tgt)
|
||||
method_peers.setdefault(method_key, []).append(tnode)
|
||||
|
||||
# Base-class chain from `inherits` edges (C# files only). The type-reference
|
||||
# pass has already re-pointed each resolvable base to its real definition and
|
||||
@@ -5834,6 +5879,11 @@ def _resolve_csharp_member_calls(
|
||||
method_nid = _method_on_type_or_bases(type_nid, _key(callee))
|
||||
if not method_nid:
|
||||
continue # receiver typed, but the type has no such method — skip
|
||||
peers = method_peers.get((enclosing_type.get(method_nid), _key(callee)), [])
|
||||
if len(peers) > 1:
|
||||
picked = _csharp_pick_overload(peers, rc.get("arity"))
|
||||
if picked is not None:
|
||||
method_nid = picked["id"]
|
||||
if method_nid == caller or (caller, method_nid) in existing_pairs:
|
||||
continue
|
||||
existing_pairs.add((caller, method_nid))
|
||||
@@ -10507,6 +10557,7 @@ def extract(
|
||||
_csharp_stub_resolver: CsharpNameResolver | None = None # built on first use
|
||||
_csharp_method_owner: dict[str, str] | None = None # method nid -> type nid, built on first use
|
||||
_csharp_outer_type: dict[str, str] | None = None # nested type nid -> outer type label, built on first use
|
||||
_csharp_overload_cache: dict = {} # filled on first use (#4584)
|
||||
for rc in all_raw_calls:
|
||||
if rc.get("_ambiguous_python_import"):
|
||||
continue
|
||||
@@ -10560,6 +10611,14 @@ def extract(
|
||||
candidates = global_label_to_nids_ci.get(callee.lower(), [])
|
||||
if not candidates:
|
||||
continue
|
||||
if rc.get("lang") == "csharp" and not rc.get("csharp_new") and len(candidates) > 1:
|
||||
if not _csharp_overload_cache:
|
||||
_csharp_overload_cache.update(_csharp_overload_index(
|
||||
resolution_nodes, all_edges + list(resolution_context_edges or [])
|
||||
))
|
||||
candidates = _csharp_collapse_overloads(
|
||||
candidates, rc.get("arity"), _csharp_overload_cache
|
||||
)
|
||||
if rc.get("csharp_new") or rc.get("lang") == "csharp":
|
||||
# An enum member or a field/property is never a valid call target
|
||||
# either — not just never what `new X()` constructs. A bare call
|
||||
|
||||
@@ -190,6 +190,85 @@ def _csharp_type_name_for_id(node, class_name: str) -> str:
|
||||
return f"{class_name}`{arity}" if arity else class_name
|
||||
|
||||
|
||||
def _csharp_method_signature(node, source: bytes) -> tuple[str, dict]:
|
||||
"""A C# method's parameter signature and the argument counts it accepts.
|
||||
|
||||
The signature (``"`1:this Foo,ref int,params int[]"``) tells overloads
|
||||
apart; the metadata (``arity``, plus ``min_arity`` for optional, ``params``
|
||||
and extension parameters, and ``variadic`` for ``params``) lets a call site
|
||||
pick the overload its argument count fits (#4584).
|
||||
"""
|
||||
parts: list[str] = []
|
||||
arity = required = 0
|
||||
variadic = extension = False
|
||||
params = node.child_by_field_name("parameters")
|
||||
children = params.children if params is not None else []
|
||||
for i, c in enumerate(children):
|
||||
if c.type == "parameter":
|
||||
mods = [_read_text(m, source) for m in c.children if m.type == "modifier"]
|
||||
type_node = c.child_by_field_name("type")
|
||||
# No whitespace in a type, so `Dictionary<string, int>`, a line
|
||||
# break inside it, or CRLF never changes the id.
|
||||
type_text = "".join(_read_text(type_node, source).split()) if type_node is not None else ""
|
||||
parts.append(" ".join(mods + [type_text]))
|
||||
arity += 1
|
||||
if "this" in mods and arity == 1:
|
||||
extension = True
|
||||
if not any(x.type == "=" for x in c.children):
|
||||
required += 1
|
||||
elif c.type == "params":
|
||||
type_node = children[i + 1] if i + 1 < len(children) else None
|
||||
parts.append("params " + ("".join(_read_text(type_node, source).split()) if type_node is not None else ""))
|
||||
arity += 1
|
||||
variadic = True
|
||||
type_params = sum(
|
||||
1 for c in node.children if c.type == "type_parameter_list"
|
||||
for _p in c.children if _p.type in ("type_parameter", "identifier")
|
||||
)
|
||||
signature = ",".join(parts)
|
||||
if type_params:
|
||||
signature = f"`{type_params}:{signature}"
|
||||
meta: dict = {"arity": arity}
|
||||
# An extension method is also called as `x.M(rest)`, one argument short.
|
||||
min_arity = required - 1 if extension else required
|
||||
if min_arity < arity:
|
||||
meta["min_arity"] = min_arity
|
||||
if variadic:
|
||||
meta["variadic"] = True
|
||||
return signature, meta
|
||||
|
||||
|
||||
def _csharp_argc(call_node) -> int | None:
|
||||
"""Argument count of a C# invocation, or None for anything else."""
|
||||
if call_node.type != "invocation_expression":
|
||||
return None
|
||||
args = call_node.child_by_field_name("arguments")
|
||||
return sum(1 for c in args.children if c.type == "argument") if args is not None else 0
|
||||
|
||||
|
||||
def _csharp_pick_overload(overloads: list[dict], argc: int | None) -> dict | None:
|
||||
"""The one overload a call with ``argc`` arguments can mean, or None.
|
||||
|
||||
``overloads`` are the method nodes of one name on one type. Only the
|
||||
argument count is known at the call site, so two overloads that both
|
||||
accept it (``M(int)`` / ``M(string)``, or ``M(int)`` / ``M(params int[])``)
|
||||
stay undecided; C# would pick by argument types, which are not known here
|
||||
(#4584).
|
||||
"""
|
||||
if argc is None:
|
||||
return None
|
||||
fits = []
|
||||
for n in overloads:
|
||||
meta = n.get("metadata") or {}
|
||||
arity = meta.get("arity")
|
||||
if not isinstance(arity, int):
|
||||
return None
|
||||
low = meta.get("min_arity", arity)
|
||||
if low <= argc and (argc <= arity or meta.get("variadic")):
|
||||
fits.append(n)
|
||||
return fits[0] if len(fits) == 1 else None
|
||||
|
||||
|
||||
def _csharp_pre_scan_name_collisions(
|
||||
root_node, source: bytes, stem: str
|
||||
) -> tuple[dict[str, set[str]], dict[str, set[str]]]:
|
||||
@@ -6073,6 +6152,9 @@ def _extract_generic(
|
||||
# class nid -> method name -> one [return type, type-param count] per
|
||||
# overload, for typing `var v = R.M<A>()` cross-file (#4266).
|
||||
csharp_method_returns: dict[str, dict[str, list]] = {}
|
||||
# plain method nid -> {signature: node} for the overloads of one name on
|
||||
# one type, in declaration order (#4584).
|
||||
csharp_overloads: dict[str, dict[str, dict]] = {}
|
||||
|
||||
csharp_interface_names: set[str] = set()
|
||||
csharp_type_groups: dict[str, set[str]] = {}
|
||||
@@ -7754,6 +7836,18 @@ def _extract_generic(
|
||||
func_nid = _underscore_salted_nid(
|
||||
func_nid, sanitized_name, csharp_member_groups
|
||||
)
|
||||
if t == "method_declaration":
|
||||
# Overloads share a name, so they minted one id and
|
||||
# every later overload vanished into the first (#4584).
|
||||
# The first declaration keeps the plain id; each other
|
||||
# signature gets its own, salted like #3302/#4415.
|
||||
csharp_sig, csharp_sig_meta = _csharp_method_signature(node, source)
|
||||
group = csharp_overloads.setdefault(func_nid, {})
|
||||
if group and csharp_sig not in group:
|
||||
salt = hashlib.sha1(
|
||||
csharp_sig.encode("utf-8"), usedforsecurity=False
|
||||
).hexdigest()[:6]
|
||||
func_nid = _make_id(func_nid, salt)
|
||||
elif (config.ts_module in ("tree_sitter_javascript", "tree_sitter_typescript")
|
||||
and t in _JS_MEMBER_METHOD_TYPES):
|
||||
func_nid = _js_member_nid(
|
||||
@@ -7779,12 +7873,18 @@ def _extract_generic(
|
||||
existing_metadata = dict(existing_method.get("metadata") or {})
|
||||
existing_metadata.update(ruby_method_metadata)
|
||||
existing_method["metadata"] = sanitize_metadata(existing_metadata)
|
||||
method_metadata = ruby_method_metadata
|
||||
if config.ts_module == "tree_sitter_c_sharp" and t == "method_declaration":
|
||||
method_metadata = {**csharp_sig_meta, "signature": csharp_sig}
|
||||
add_node(
|
||||
func_nid,
|
||||
f".{func_name}()",
|
||||
line,
|
||||
metadata=ruby_method_metadata,
|
||||
metadata=method_metadata,
|
||||
)
|
||||
if (config.ts_module == "tree_sitter_c_sharp" and t == "method_declaration"
|
||||
and csharp_sig not in group and nodes and nodes[-1]["id"] == func_nid):
|
||||
group[csharp_sig] = nodes[-1]
|
||||
add_edge(parent_class_nid, func_nid, "method", line)
|
||||
if config.ts_module == "tree_sitter_c_sharp" and t == "method_declaration":
|
||||
csharp_method_returns.setdefault(parent_class_nid, {}).setdefault(
|
||||
@@ -8602,6 +8702,13 @@ def _extract_generic(
|
||||
label_to_nid.setdefault(normalised, n["id"])
|
||||
label_to_nid_ci.setdefault(normalised.lower(), n["id"])
|
||||
|
||||
# method nid -> its overloads (declaration order), for the call walk.
|
||||
csharp_overload_peers: dict[str, list[dict]] = {
|
||||
peer["id"]: list(group.values())
|
||||
for group in csharp_overloads.values() if len(group) > 1
|
||||
for peer in group.values()
|
||||
}
|
||||
|
||||
seen_call_pairs: set[tuple[str, str]] = set()
|
||||
seen_indirect_pairs: set[tuple[str, str]] = set() # Python indirect_call dedup
|
||||
seen_dyn_import_pairs: set[tuple[str, str]] = set()
|
||||
@@ -9704,6 +9811,12 @@ def _extract_generic(
|
||||
# file (an annotation elsewhere in this file created it), so the
|
||||
# call still goes to raw_calls for cross-file resolution, which
|
||||
# skips source-less candidates on its own (#3888).
|
||||
# C#: the name found one overload; the argument count picks
|
||||
# the one this call means, else the first declared (#4584).
|
||||
if tgt_nid in csharp_overload_peers:
|
||||
peers = csharp_overload_peers[tgt_nid]
|
||||
picked = _csharp_pick_overload(peers, _csharp_argc(node))
|
||||
tgt_nid = (picked or peers[0])["id"]
|
||||
external_stub_target = bool(tgt_nid and not nid_to_sf.get(tgt_nid))
|
||||
if tgt_nid and not external_stub_target:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
@@ -9807,6 +9920,9 @@ def _extract_generic(
|
||||
# class fields/properties are the base scope.
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
rc_entry["lang"] = "csharp"
|
||||
argc = _csharp_argc(node)
|
||||
if argc is not None:
|
||||
rc_entry["arity"] = argc
|
||||
# `new X()` can only construct a type, never an enum
|
||||
# member or property that happens to share X's name.
|
||||
if node.type == "object_creation_expression":
|
||||
|
||||
@@ -18,7 +18,8 @@ implementers the answer is all N, not none. Each implementer is still subject
|
||||
to the single-owner guard `resolve_pascal_inherited_calls` and
|
||||
`resolve_ruby_member_calls` already use: an implementer that itself declares
|
||||
more than one method of the same name is ambiguous and is skipped on its own,
|
||||
independently of its siblings.
|
||||
independently of its siblings, unless the overloads pair up one-to-one by
|
||||
signature or parameter count (C#, #4584).
|
||||
|
||||
The join is per method rather than per call site, so one edge per implementer
|
||||
reconnects every call that reaches the interface method. Confidence is
|
||||
@@ -168,28 +169,56 @@ def resolve_interface_dispatch(
|
||||
for impl_nid, impl_node in gated_impls:
|
||||
impl_methods = methods_of.get(impl_nid, {})
|
||||
for name, declared in interface_methods.items():
|
||||
if len(declared) != 1:
|
||||
continue
|
||||
# Ambiguity is per implementer: one implementer overloading a
|
||||
# name does not disqualify its siblings from a clean match.
|
||||
candidates = impl_methods.get(name, set())
|
||||
if len(candidates) != 1:
|
||||
continue
|
||||
source = next(iter(declared))
|
||||
target = next(iter(candidates))
|
||||
if source == target or (source, target) in existing_pairs:
|
||||
continue
|
||||
existing_pairs.add((source, target))
|
||||
new_edges.append({
|
||||
"source": source,
|
||||
"target": target,
|
||||
"relation": DISPATCH_RELATION,
|
||||
"context": "call",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": score,
|
||||
"source_file": str(impl_node.get("source_file", "")),
|
||||
"source_location": impl_node.get("source_location"),
|
||||
"weight": 1.0,
|
||||
})
|
||||
for source, target in _overload_pairs(
|
||||
declared, impl_methods.get(name, set()), node_by_id
|
||||
):
|
||||
if source == target or (source, target) in existing_pairs:
|
||||
continue
|
||||
existing_pairs.add((source, target))
|
||||
new_edges.append({
|
||||
"source": source,
|
||||
"target": target,
|
||||
"relation": DISPATCH_RELATION,
|
||||
"context": "call",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": score,
|
||||
"source_file": str(impl_node.get("source_file", "")),
|
||||
"source_location": impl_node.get("source_location"),
|
||||
"weight": 1.0,
|
||||
})
|
||||
|
||||
all_edges.extend(new_edges)
|
||||
|
||||
|
||||
def _overload_pairs(
|
||||
declared: set[str], candidates: set[str], node_by_id: dict
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Pair an interface's methods of one name with an implementer's.
|
||||
|
||||
One of each pairs up as always. Otherwise (C# overloads, #4584) each
|
||||
interface method pairs with the one implementer method of the same declared
|
||||
signature, else of the same parameter count among those no signature
|
||||
claimed; no unique match, no edge. An implementer method takes at most one
|
||||
interface method, so an overload implemented on a base class is not pinned
|
||||
on a sibling of the same count.
|
||||
"""
|
||||
if len(declared) == 1 and len(candidates) == 1:
|
||||
return [(next(iter(declared)), next(iter(candidates)))]
|
||||
|
||||
def _meta(nid: str) -> dict:
|
||||
meta = (node_by_id.get(nid) or {}).get("metadata")
|
||||
return meta if isinstance(meta, dict) else {}
|
||||
|
||||
pairs: dict[str, str] = {}
|
||||
for key in ("signature", "arity"):
|
||||
taken = set(pairs.values())
|
||||
for source in sorted(declared):
|
||||
want = _meta(source).get(key)
|
||||
if source in pairs or want is None:
|
||||
continue
|
||||
hits = [c for c in candidates if c not in taken and _meta(c).get(key) == want]
|
||||
if len(hits) == 1:
|
||||
pairs[source] = hits[0]
|
||||
# Two interface methods on one implementer method: neither is certain.
|
||||
targets = list(pairs.values())
|
||||
return [(s, t) for s, t in pairs.items() if targets.count(t) == 1]
|
||||
@@ -2061,6 +2061,11 @@ def _rebuild_code(
|
||||
"module",
|
||||
"name",
|
||||
"arity",
|
||||
# C# overloads (#4584): which one a call's
|
||||
# argument count fits.
|
||||
"min_arity",
|
||||
"variadic",
|
||||
"signature",
|
||||
)
|
||||
if key in metadata
|
||||
}
|
||||
|
||||
@@ -127,10 +127,9 @@ def test_three_implementers_floor_at_the_weak_inference_tier(tmp_path):
|
||||
|
||||
|
||||
def test_one_ambiguous_implementer_does_not_suppress_its_siblings(tmp_path):
|
||||
# B owns two distinct nodes named `.M()` (synthetic: the real extractor
|
||||
# collapses same-named overloads into one node, so this models a case
|
||||
# where it would not, e.g. partial classes merged incorrectly). B's
|
||||
# ambiguity must stay scoped to B and not suppress A's clean match.
|
||||
# B owns two distinct nodes named `.M()` with nothing (signature or
|
||||
# arity) to tell them apart. B's ambiguity must stay scoped to B and not
|
||||
# suppress A's clean match.
|
||||
from graphify.csharp_dispatch import resolve_csharp_interface_dispatch
|
||||
|
||||
nodes = [
|
||||
@@ -163,11 +162,9 @@ def test_no_matching_member_produces_no_edge(tmp_path):
|
||||
assert not dispatch
|
||||
|
||||
|
||||
def test_overloads_collapsed_into_one_node_still_link(tmp_path):
|
||||
# The extractor mints one node per method name, so two overloads share it.
|
||||
# The guard counts distinct nodes, not `method` edge multiplicity, so this
|
||||
# is one candidate rather than a tie. Same reasoning as the dedup comment in
|
||||
# resolve_pascal_inherited_calls.
|
||||
def test_overloaded_implementer_still_links(tmp_path):
|
||||
# A's overloads are two nodes since #4584; the interface's M() pairs with
|
||||
# the one of the same parameter count.
|
||||
dispatch, r = _extract(tmp_path, {"S.cs": (
|
||||
"public interface IR { void M(); }\n"
|
||||
"public class A : IR { public void M() { } public void M(int x) { } }\n"
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""C# overloads get a node each, and calls pick theirs by argument count (#4584).
|
||||
|
||||
Overloads share a name, so they minted one id: ``add_node`` kept the first
|
||||
declaration, the others vanished, every caller of any overload pointed at the
|
||||
first one, and a later overload's own calls were credited to it. Now the first
|
||||
declared overload keeps the plain id (so a type without overloads, and the
|
||||
first overload, keep the ids they had) and every other signature gets a salted
|
||||
one. A call binds to the overload its argument count fits; when that is not
|
||||
unique (``M(int)`` / ``M(string)``) it binds to the first, as before.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
_MESSAGES = """namespace Demo
|
||||
{
|
||||
public static class Messages
|
||||
{
|
||||
public static string Banner(int code)
|
||||
{
|
||||
return Format(code);
|
||||
}
|
||||
|
||||
public static string Banner(int code, string format)
|
||||
{
|
||||
return Pad(format);
|
||||
}
|
||||
|
||||
private static string Format(int code) { return code.ToString(); }
|
||||
private static string Pad(string text) { return text; }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
_CALLER = """namespace Demo
|
||||
{
|
||||
public class Caller
|
||||
{
|
||||
public string OneArg() { return Messages.Banner(1); }
|
||||
public string TwoArgs() { return Messages.Banner(1, "x"); }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _extract(tmp_path, files: dict[str, str]):
|
||||
for name, body in files.items():
|
||||
p = tmp_path / name
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body)
|
||||
old = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
return extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
|
||||
finally:
|
||||
os.chdir(old)
|
||||
|
||||
|
||||
def _by_line(r, label):
|
||||
return {
|
||||
n["source_location"]: n["id"]
|
||||
for n in r["nodes"] if n["label"] == label and n.get("source_file")
|
||||
}
|
||||
|
||||
|
||||
def _edges(r, relation="calls"):
|
||||
return {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == relation}
|
||||
|
||||
|
||||
def _id(r, label):
|
||||
ids = [n["id"] for n in r["nodes"] if n["label"] == label and n.get("source_file")]
|
||||
assert len(ids) == 1, (label, ids)
|
||||
return ids[0]
|
||||
|
||||
|
||||
def test_issue_repro_each_overload_keeps_its_node_callers_and_callees(tmp_path):
|
||||
r = _extract(tmp_path, {"Messages.cs": _MESSAGES, "Caller.cs": _CALLER})
|
||||
banner = _by_line(r, ".Banner()")
|
||||
assert set(banner) == {"L5", "L10"}
|
||||
one, two = banner["L5"], banner["L10"]
|
||||
# The first overload keeps the id the single node had.
|
||||
assert one == "messages_demo_messages_banner"
|
||||
calls = _edges(r)
|
||||
assert (_id(r, ".OneArg()"), one) in calls
|
||||
assert (_id(r, ".TwoArgs()"), two) in calls
|
||||
assert (_id(r, ".OneArg()"), two) not in calls
|
||||
assert (_id(r, ".TwoArgs()"), one) not in calls
|
||||
assert (one, _id(r, ".Format()")) in calls
|
||||
assert (two, _id(r, ".Pad()")) in calls
|
||||
assert (one, _id(r, ".Pad()")) not in calls
|
||||
|
||||
|
||||
def test_method_without_overloads_is_unchanged(tmp_path):
|
||||
r = _extract(tmp_path, {"Messages.cs": _MESSAGES})
|
||||
fmt = next(n for n in r["nodes"] if n["label"] == ".Format()")
|
||||
assert fmt["id"] == "messages_demo_messages_format"
|
||||
|
||||
|
||||
def test_in_file_and_instance_calls_pick_by_argument_count(tmp_path):
|
||||
r = _extract(tmp_path, {
|
||||
"Svc.cs": (
|
||||
"public class Svc {\n"
|
||||
" public void Run() { }\n"
|
||||
" public void Run(int a) { Run(); }\n"
|
||||
" public void Twice() { Run(1); }\n"
|
||||
"}\n"
|
||||
),
|
||||
"Use.cs": (
|
||||
"public class Use {\n"
|
||||
" public void Go(Svc s) { s.Run(); }\n"
|
||||
" public void Go1(Svc s) { s.Run(2); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
run = _by_line(r, ".Run()")
|
||||
calls = _edges(r)
|
||||
assert (run["L3"], run["L2"]) in calls
|
||||
assert (_id(r, ".Twice()"), run["L3"]) in calls
|
||||
assert (_id(r, ".Twice()"), run["L2"]) not in calls
|
||||
assert (_id(r, ".Go()"), run["L2"]) in calls
|
||||
assert (_id(r, ".Go1()"), run["L3"]) in calls
|
||||
|
||||
|
||||
def test_same_count_overloads_bind_to_the_first(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public static class S {\n"
|
||||
" public static void Put(int x) { }\n"
|
||||
" public static void Put(string x) { }\n"
|
||||
" public static void Call() { Put(1); }\n"
|
||||
"}\n"
|
||||
)})
|
||||
put = _by_line(r, ".Put()")
|
||||
assert len(put) == 2
|
||||
calls = _edges(r)
|
||||
assert (_id(r, ".Call()"), put["L2"]) in calls
|
||||
assert (_id(r, ".Call()"), put["L3"]) not in calls
|
||||
|
||||
|
||||
def test_optional_and_params_arguments_count(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public static class S {\n"
|
||||
" public static void Opt(string s) { }\n"
|
||||
" public static void Opt(int a, int b, int c = 1) { }\n"
|
||||
" public static void Many(int a) { }\n"
|
||||
" public static void Many(int a, int b, params int[] rest) { }\n"
|
||||
" public static void Call() { Opt(1, 2); Many(1, 2, 3, 4); }\n"
|
||||
"}\n"
|
||||
)})
|
||||
calls = _edges(r)
|
||||
caller = _id(r, ".Call()")
|
||||
assert (caller, _by_line(r, ".Opt()")["L3"]) in calls
|
||||
assert (caller, _by_line(r, ".Many()")["L5"]) in calls
|
||||
|
||||
|
||||
def test_same_file_partial_halves_keep_their_overloads(tmp_path):
|
||||
r = _extract(tmp_path, {"P.cs": (
|
||||
"public partial class P { public void M(int a) { } }\n"
|
||||
"public partial class P { public void M(int a, int b) { M(a); } }\n"
|
||||
)})
|
||||
m = _by_line(r, ".M()")
|
||||
assert set(m) == {"L1", "L2"}
|
||||
assert (m["L2"], m["L1"]) in _edges(r)
|
||||
|
||||
|
||||
def test_identical_signature_behind_preprocessor_stays_one_node(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class S {\n"
|
||||
"#if A\n"
|
||||
" public void M(int a) { }\n"
|
||||
"#else\n"
|
||||
" public void M(int a) { }\n"
|
||||
"#endif\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert len(_by_line(r, ".M()")) == 1
|
||||
|
||||
|
||||
def test_interface_overloads_dispatch_to_their_own_implementation(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public interface IR { void M(int a); void M(int a, string b); }\n"
|
||||
"public class A : IR {\n"
|
||||
" public void M(int a) { }\n"
|
||||
" public void M(int a, string b) { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
iface = _by_line(r, ".M()")
|
||||
dispatch = _edges(r, "dispatches_to")
|
||||
ir1 = next(n["id"] for n in r["nodes"] if n["id"].startswith("s_ir_m") and n["source_location"] == "L1"
|
||||
and n["metadata"].get("arity") == 1)
|
||||
ir2 = next(n["id"] for n in r["nodes"] if n["id"].startswith("s_ir_m")
|
||||
and n["metadata"].get("arity") == 2)
|
||||
assert dispatch == {(ir1, iface["L3"]), (ir2, iface["L4"])}
|
||||
|
||||
|
||||
def test_overload_ids_ignore_whitespace_and_line_endings(tmp_path):
|
||||
src = (
|
||||
"public class S {\n"
|
||||
" public void M(int a) { }\n"
|
||||
" public void M(System.Collections.Generic.Dictionary<string, int> d) { }\n"
|
||||
"}\n"
|
||||
)
|
||||
a = _extract(tmp_path / "a", {"S.cs": src})
|
||||
b = _extract(tmp_path / "b", {"S.cs": src.replace(", int>", ",\n int>").replace("\n", "\r\n")})
|
||||
assert sorted(_by_line(a, ".M()").values()) == sorted(_by_line(b, ".M()").values())
|
||||
|
||||
|
||||
def test_interface_overload_implemented_on_a_base_does_not_dispatch_to_a_sibling(tmp_path):
|
||||
# Impl declares only M(int); M(string) comes from Base. IR.M(string) must
|
||||
# not be pinned on Impl.M(int) just because both take one argument.
|
||||
r = _extract(tmp_path, {"D.cs": (
|
||||
"public interface IR { void M(int a); void M(string a); }\n"
|
||||
"public class Base { public void M(string a) { } }\n"
|
||||
"public class Impl : Base, IR { public void M(int a) { } }\n"
|
||||
)})
|
||||
by_sig = {
|
||||
n["metadata"].get("signature"): n["id"]
|
||||
for n in r["nodes"] if n["id"].startswith("d_ir_m")
|
||||
}
|
||||
impl = next(n["id"] for n in r["nodes"] if n["id"] == "d_impl_m")
|
||||
assert _edges(r, "dispatches_to") == {(by_sig["int"], impl)}
|
||||
|
||||
|
||||
def test_collapse_keeps_distinct_names_on_one_type_apart(tmp_path):
|
||||
# Review: overloads are folded per owner type, but only among candidates
|
||||
# of one name, so `Put` / `_Put` / `put` stay separate targets.
|
||||
r = _extract(tmp_path, {
|
||||
"A.cs": (
|
||||
"public partial class S {\n"
|
||||
" public void Put(int a) { }\n"
|
||||
" public void _Put(int a, int b) { }\n"
|
||||
" public void put(int a, int b, int c) { }\n"
|
||||
" public void Put(string a, int b) { }\n"
|
||||
"}\n"
|
||||
),
|
||||
"B.cs": (
|
||||
"public partial class S {\n"
|
||||
" public void Go() { Put(1); _Put(1, 2); put(1, 2, 3); Put(\"x\", 1); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
go = _id(r, ".Go()")
|
||||
calls = {t for s, t in _edges(r) if s == go}
|
||||
put = _by_line(r, ".Put()")
|
||||
assert calls == {put["L2"], put["L5"], _id(r, "._Put()"), _id(r, ".put()")}
|
||||
|
||||
|
||||
def test_params_parameter_is_variadic_and_optional(tmp_path):
|
||||
r = _extract(tmp_path, {"P.cs": (
|
||||
"public static class P {\n"
|
||||
" public static void Log(string m) { }\n"
|
||||
" public static void Log(string m, params object[] args) { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
meta = {n["source_location"]: n["metadata"] for n in r["nodes"] if n["label"] == ".Log()"}
|
||||
assert meta["L3"]["arity"] == 2 and meta["L3"]["min_arity"] == 1 and meta["L3"]["variadic"]
|
||||
assert "variadic" not in meta["L2"]
|
||||
|
||||
|
||||
def test_overloads_in_struct_record_and_beside_other_members(tmp_path):
|
||||
r = _extract(tmp_path, {"L.cs": (
|
||||
"public struct St { public void M(int a) { } public int M2 { get; } public void M(int a, int b) { } }\n"
|
||||
"public record R { public void N() { } class Inner { public void N(int x) { } } public void N(int x) { } }\n"
|
||||
)})
|
||||
assert len([n for n in r["nodes"] if n["id"].startswith("l_st_m") and n["label"] == ".M()"]) == 2
|
||||
assert {n["id"] for n in r["nodes"] if n["label"] == ".N()"} == {"l_r_n", "l_r_n_46f8ab", "l_inner_n"}
|
||||
@@ -116,14 +116,19 @@ def test_case_only_method_pair_keeps_both_methods(tmp_path):
|
||||
assert _one(r, ".Load()") != _one(r, ".load()")
|
||||
|
||||
|
||||
def test_overloads_stay_one_node(tmp_path):
|
||||
def test_overloads_are_not_an_underscore_collision(tmp_path):
|
||||
# Overloads are one name, so this rule leaves them alone; they get a node
|
||||
# each through the overload rule instead (#4584), the first keeping the
|
||||
# plain id.
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public void Save() { }\n"
|
||||
" public void Save(int x) { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
_one(r, ".Save()")
|
||||
ids = _ids(r, ".Save()")
|
||||
assert len(ids) == 2
|
||||
assert "s_host_save" in ids
|
||||
|
||||
|
||||
def test_member_behind_preprocessor_directive_is_counted(tmp_path):
|
||||
|
||||
Reference in new issue
Block a user