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
44 changes: 44 additions & 0 deletions gitgalaxy/core/call_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,43 @@ def _ancestry(parsed_files: list[dict[str, Any]]) -> dict[tuple[str, str], set[s
return parents


def _expand_super_ctor(
func: dict[str, Any],
index: dict[tuple[str, str], "_Bucket"],
parents: dict[tuple[str, str], set[str]],
group: str,
lang: str,
local: dict[str, set[str]],
) -> None:
"""#3772: a constructor's `super(...)` becomes a call to its in-repo superclass's constructor.

The detector keeps `super` (with its argument counts) only in a constructor; here it is
replaced by the direct parent classes the scan knows. A parent outside the repo
(`Exception`) leaves no call. The counts move to the parent name for overload choice.
"""
owner = func.get("parent_class_name") or _leaf(str(func.get("name") or ""))[1]
okey = _key(_leaf(str(owner))[0], lang) if owner else ""
# a nested class name repeats across files (`HiColor`): the declaration in the caller's own file wins
direct = sorted(local.get(okey) or parents.get((group, okey)) or set()) if owner else []
# an `extends Base<T>` is recorded with its type arguments: `Base` names the class
# (a header broken after `extends A` can leave `A implements` as one recorded name)
named = sorted({_leaf(p.split("<")[0].split()[0])[0] for p in direct if p.strip()})
known = [p for p in named if _is_class(index, group, p, lang)]
arities = func.setdefault("calls_out_arities", {})
quals = func.setdefault("calls_out_qualifiers", {})
keyword = "super"
counts = arities.pop(keyword, None)
quals.pop(keyword, None)
calls = [c for c in func["calls_out_to"] if c != keyword]
for p in known:
if p not in calls:
calls.append(p)
if counts:
arities.setdefault(p, []).extend(a for a in counts if a not in arities.get(p, []))
quals.setdefault(p, [""])
func["calls_out_to"] = calls


def _is_class(index: dict[tuple[str, str], "_Bucket"], group: str, name: str, lang: str) -> bool:
bucket = index.get((group, _key(name, lang)))
return bucket is not None and any(d.kind == "class" for d in bucket.defs)
Expand Down Expand Up @@ -1437,6 +1474,11 @@ def resolve_calls(
{a: _alias_scope(t, imports, star_exports) for a, t in (namespace_aliases or {}).get(src_path, {}).items()},
)
lang_counts = by_lang.setdefault(lang, Counter())
file_parents: dict[str, set[str]] = {}
for cls in f.get("classes", []) or []:
if cls.get("name"):
bases = {_leaf(str(b).strip())[0] for b in cls.get("inheritance", []) or [] if str(b).strip()}
file_parents.setdefault(_key(str(cls["name"]), lang), set()).update(_key(b, lang) for b in bases)
# A module's own receiver types (`app = FastAPI()` at top level) hold in
# every function of the file that does not bind that name itself.
module_types: dict[str, str] = {}
Expand All @@ -1447,6 +1489,8 @@ def resolve_calls(
# #3362: calls, then unconditional transfers (COBOL GO TO). A transfer
# resolves by the same ladder (it names a unit the same way) but is
# its own `kind`: it never counts toward the call-resolution rates.
if lang in OVERLOAD_LANGS and "super" in (func.get("calls_out_to") or []):
_expand_super_ctor(func, index, parents, group, lang, file_parents)
callees = [(c, "call") for c in func.get("calls_out_to") or []]
callees += [(t, "transfer") for t in func.get("transfers_to") or []]
# Decorators: the decorated function -> each decorator applied to it
Expand Down
6 changes: 5 additions & 1 deletion gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10587,6 +10587,7 @@ def _calculate_block_metrics(
own_leaf: str | None = None
own_qualifiers: set[str] = set()
bare_self_hits = 0
kept_super = False
# #5699: an Objective-C method header (`- (int)f:(int)a`) is no call match, so its first bare `f(` is a body call
own_header_seen = self.primary_lang_id == "objective-c" and block.lstrip()[:1] in ("-", "+")
if invocation_pattern:
Expand Down Expand Up @@ -10657,6 +10658,9 @@ def _calculate_block_metrics(
if track_arities and callee == "this" and not qualifier and name[:1].isupper():
# #3772: `this(...)` chains to a sibling constructor, which is named like the unit
callee = name
if track_arities and callee == "super" and not qualifier:
# #3772: `super(...)` chains to the superclass constructor; the resolver names the parent
kept_super = True
is_decl = callee_pos == m.start(1) and _is_declaration_header(decl_headers, m)
own_header_call = callee == name and not qualifier and not own_header_seen
if callee == name and not qualifier:
Expand Down Expand Up @@ -10729,7 +10733,7 @@ def _calculate_block_metrics(
# #4305: a pattern alternative that only consumes text (COBOL `EXIT PERFORM`) captures ""
if c
and (c not in _CALLS_OUT_GLOBAL_IGNORE or c in global_exempt)
and (c.casefold() if _fold_ignore else c) not in lang_ignore
and ((c.casefold() if _fold_ignore else c) not in lang_ignore or (kept_super and c == "super"))
and c != self_name
)
)
Expand Down
8 changes: 4 additions & 4 deletions tests/call_graph_resolution_java_baseline.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"ambiguous_precision_pct": 13.5,
"confident_external": 105,
"confident_judged": 5684,
"confident_judged": 5708,
"confident_overload_blind_precision_pct": 99.8,
"confident_precision_pct": 99.8,
"confident_strict_precision_pct": 98.0,
"confident_unmapped": 73,
"confident_wrong_overload": 0,
"pyan_edges": 7858,
"recall_overload_blind_pct": 72.2,
"recall_pct": 72.2,
"recall_overload_blind_pct": 72.5,
"recall_pct": 72.5,
"reference_version": "0.12.3",
"resolution_recall_pct": 73.2
"resolution_recall_pct": 73.3
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ def test_assert_keyword_is_not_a_call(lang, code, fn):
"lang, code, fn",
[
("javascript", "class B extends A {\n constructor(x) {\n super(x);\n go(x);\n }\n}\n", "constructor"),
("java", "class B extends A {\n B(int x) {\n super(x);\n go(x);\n }\n}\n", "B"),
# typescript/groovy: #3361's `super` merged into #3359's keyword sets.
("typescript", "function run(x: number) {\n super(x);\n go(x);\n}\n", "run"),
("groovy", "class B extends A {\n B(int x) {\n super(x)\n go(x)\n }\n}\n", "B"),
Expand All @@ -111,6 +110,12 @@ def test_super_keyword_is_not_a_call(lang, code, fn):
assert "super" not in calls and "go" in calls


def test_java_super_ctor_is_a_call():
# #3772: java's `super(...)` is a call to the superclass constructor (the resolver edge needs it).
calls = _calls("java", "class B extends A {\n B(int x) {\n super(x);\n go(x);\n }\n}\n")["B"]
assert "super" in calls and "go" in calls


def test_python_super_builtin_is_a_call():
assert "super" in _calls("python", "def run(self):\n super().run()\n")["run"]

Expand Down
137 changes: 137 additions & 0 deletions tests/extraction/languages/test_calls_out_java_super_ctor_3772.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""#3772: a Java constructor's `super(...)` is a call to the superclass constructor. The detector keeps
it (constructors only, with argument counts); the resolver names the in-repo parent and links the
overload the counts pick. A parent outside the repo leaves no call, and `super.m()` is unchanged."""

from gitgalaxy.core.call_resolver import resolve_calls
from gitgalaxy.core.detector import StructuralExtractor
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

BASE = (
"package p;\n"
"public class Base {\n"
" public Base() { }\n"
" public Base(String a) { }\n"
" public Base(String a, int b) { }\n"
" public void hello() { }\n"
"}\n"
)
SUB = (
"package p;\n"
"public class Sub extends Base {\n"
" public Sub() { super(); }\n"
" public Sub(String a) { super(a); }\n"
" public Sub(String a, int b) { super(a, b); }\n"
" public void bye() { super.hello(); }\n"
" static class Inner extends Base {\n"
" Inner(String a) { super(a); }\n"
" }\n"
"}\n"
)
OUTSIDE = "package p;\npublic class Boom extends RuntimeException {\n public Boom(String m) { super(m); }\n}\n"


def _parse(src, path):
out = StructuralExtractor("java", LANGUAGE_DEFINITIONS).splice(src, "")
classes = out.get("classes", [])
for c in classes:
c.setdefault("inheritance", [])
return {"path": path, "lang_id": "java", "functions": out["functions"], "classes": classes}


def _resolve(*files):
parsed = [_parse(src, path) for path, src in files]
sites, _ = resolve_calls(parsed)
return parsed, sites


def _fn(parsed, path, line):
f = next(p for p in parsed if p["path"] == path)
return next(fn for fn in f["functions"] if fn["start_line"] == line)


def test_detector_keeps_super_only_in_a_constructor():
out = StructuralExtractor("java", LANGUAGE_DEFINITIONS).splice(SUB, "")
by_line = {f["start_line"]: f for f in out["functions"]}
assert by_line[4]["calls_out_to"] == ["super"]
assert by_line[4]["calls_out_arities"] == {"super": [1]}
assert "super" not in by_line[6]["calls_out_to"] # super.hello(): the call is hello, as before
assert by_line[6]["calls_out_to"] == ["hello"]


def test_super_chains_to_the_overload_the_counts_pick():
parsed, sites = _resolve(("p/Base.java", BASE), ("p/Sub.java", SUB))
edges = {
(s["src_line"], s["dst_line"])
for s in sites
if s["dst_path"] == "p/Base.java" and s["src_path"] == "p/Sub.java"
}
assert (3, 3) in edges # super() -> Base()
assert (4, 4) in edges # super(a) -> Base(String)
assert (5, 5) in edges # super(a, b) -> Base(String, int)
assert (4, 3) not in edges
assert (8, 4) in edges # a nested class chains to its own parent
assert _fn(parsed, "p/Sub.java", 4)["calls_out_to"] == ["Base"]


def test_super_method_call_is_unchanged():
_, sites = _resolve(("p/Base.java", BASE), ("p/Sub.java", SUB))
assert any(s["src_line"] == 6 and s["callee"] == "hello" for s in sites)


def test_super_to_a_parent_outside_the_repo_is_no_call():
parsed, sites = _resolve(("p/Boom.java", OUTSIDE))
assert not sites
assert _fn(parsed, "p/Boom.java", 3)["calls_out_to"] == []


GENERIC = (
"package p;\n"
"class Red<T> {\n"
" protected Red(T t) { }\n"
"}\n"
"class Blue<T> extends Red<T> {\n"
" Blue(T v) {\n"
" super(v);\n"
" }\n"
"}\n"
"abstract class Ad<T, B> {\n"
" Ad(int x) { }\n"
"}\n"
"final class Fra<T> extends Ad<T, String> {\n"
" Fra(int f) {\n"
" super(f);\n"
" }\n"
"}\n"
)


def test_a_generic_superclass_is_named_without_its_type_arguments():
_, sites = _resolve(("p/G.java", GENERIC))
edges = {(s["src_line"], s["dst_line"]) for s in sites if s["dst_line"]}
assert (6, 3) in edges # Blue -> Red<T>
assert (14, 11) in edges # Fra -> Ad<T, String>


def test_lowercase_class_and_a_header_broken_before_implements_still_chain():
base = "package p;\npublic abstract class vertex_t {\n public vertex_t() { }\n}\n"
sub = (
"package p;\npublic class z_vertex_t extends vertex_t implements\n Aa, Bb {\n"
" public z_vertex_t() {\n super();\n }\n}\n"
)
parsed, _ = _resolve(("p/vertex_t.java", base), ("p/z_vertex_t.java", sub))
assert _fn(parsed, "p/z_vertex_t.java", 4)["calls_out_to"] == ["vertex_t"]


def test_a_nested_class_name_repeated_across_files_uses_its_own_files_parent():
mk = lambda outer, parent: ( # noqa: E731
f"package p;\nclass {outer} {{\n static class Inner extends {parent} {{\n Inner() {{\n super();\n }}\n }}\n}}\n"
)
files = [
("p/A.java", mk("A", "PA")),
("p/B.java", mk("B", "PB")),
("p/PA.java", "package p;\nclass PA {\n PA() { }\n}\n"),
("p/PB.java", "package p;\nclass PB {\n PB() { }\n}\n"),
]
parsed, _ = _resolve(*files)
assert _fn(parsed, "p/A.java", 4)["calls_out_to"] == ["PA"]
assert _fn(parsed, "p/B.java", 4)["calls_out_to"] == ["PB"]
Binary file modified tests/golden_doom_rosetta/mochadoom.json.gz
Binary file not shown.
18 changes: 18 additions & 0 deletions tests/tools/doom_speedrun/ts_callgraph_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ def callee_name(node):
return None


def _super_ctor_target(node):
"""The superclass a Java ``super(...)`` chains to (#3772): the enclosing class's ``extends`` type, or None."""
if not node.text.startswith(b"super"):
return None
cls = node.parent
while cls is not None and cls.type != "class_declaration":
cls = cls.parent
parent = cls.child_by_field_name("superclass") if cls is not None else None
target = (parent.named_children or [None])[0] if parent is not None else None
if target is not None and target.type == "generic_type": # `extends Base<T>` names `Base`
target = (target.named_children or [None])[0]
return callee_name(target) if target is not None else None


def _has_arguments(call):
"""True when a Kotlin call carries a parenthesised argument list (not just a trailing lambda)."""
return any(c.type == "value_arguments" for k in call.children if k.type == "call_suffix" for c in k.children)
Expand Down Expand Up @@ -167,6 +181,10 @@ def __init__(self, node, spec):
self.calls.add(name)
if fn.type in NAME_NODES:
callees.add(fn.id)
elif n.type == "explicit_constructor_invocation":
name = _super_ctor_target(n)
if name is not None:
self.calls.add(name)
elif n.type in spec.ctors:
t = n.child_by_field_name("type") or n.child_by_field_name("constructor")
name = callee_name(t) if t is not None else None
Expand Down
Loading