-
-
Notifications
You must be signed in to change notification settings - Fork 11.4k
Finish Objective-C member calls across a repository boundary (#3152) #3385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v8
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,7 @@ | |
| "cpp": frozenset({".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h", ".cu", ".cuh"}), | ||
| "csharp": frozenset({".cs"}), | ||
| "java": frozenset({".java"}), | ||
| "objc": frozenset({".h", ".m", ".mm"}), | ||
| "swift": frozenset({".swift"}), | ||
| } | ||
|
|
||
|
|
@@ -57,7 +58,8 @@ def _key(label: object) -> str: | |
| Type labels are plain (``Greeter``) while method labels carry the extractor's | ||
| decoration (``.greet()``). Case is preserved: every language that parks calls | ||
| here is case-sensitive, and folding case would let `greeter` answer for | ||
| `Greeter`. | ||
| `Greeter`. Objective-C's ``+``/``-`` sigil is deliberately kept, so an ObjC | ||
| ``-greet`` and a C++ ``.greet()`` stay distinct members of a shared header. | ||
| """ | ||
| return str(label or "").strip().removeprefix(".").removesuffix("()") | ||
|
|
||
|
|
@@ -148,6 +150,20 @@ def _member_relations(lang: str) -> tuple[str, ...]: | |
| return ("method", "defines") if lang == "cpp" else ("method",) | ||
|
|
||
|
|
||
| def _member_keys(lang: str, callee: str) -> tuple[str, ...]: | ||
| """Which member-index keys a parked callee may answer to. | ||
|
|
||
| An ObjC selector carries no class/instance distinction, so both sigils are | ||
| tried; a type declaring `+greet` and `-greet` both is an ambiguity, not a hit. | ||
| A selector cannot itself begin with a sigil, so stripping one first keeps an | ||
| entry that already carries it from asking for `--greet`. | ||
| """ | ||
| if lang == "objc": | ||
| selector = callee.lstrip("+-") | ||
| return (f"-{selector}", f"+{selector}") | ||
| return (callee,) | ||
|
|
||
|
|
||
| def link_cross_repo_member_calls(merged: "nx.Graph") -> int: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 8 callees (efferent coupling); 23 callers depend on it (afferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 8 callees (efferent coupling); 23 callers depend on it (afferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. |
||
| """Emit `calls` edges for parked member calls another repo answers. | ||
|
|
||
|
|
@@ -190,7 +206,9 @@ def link_cross_repo_member_calls(merged: "nx.Graph") -> int: | |
| continue | ||
| targets: list[str] = [] | ||
| for relation in _member_relations(lang): | ||
| targets = members_by_relation[relation].get((candidates[0], callee), []) | ||
| members = members_by_relation[relation] | ||
| for member_key in _member_keys(lang, callee): | ||
| targets.extend(members.get((candidates[0], member_key), [])) | ||
| if targets: | ||
| break | ||
| if len(targets) != 1: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,11 +6,11 @@ | |
| `merge-graphs` and `global add` read. The two-repo graph was missing precisely the | ||
| edges that make it a call graph. | ||
|
|
||
| The Java, C++, C# and Swift resolvers now park those calls on the caller node and | ||
| this pass finishes them after the merge. The cases below pin what it must NOT do | ||
| as much as what it must: the single-definition guard, the cross-repo-only scope, | ||
| and the language guard are what keep it from fabricating an edge from a name | ||
| collision. | ||
| The Java, C++, C#, Objective-C and Swift resolvers now park those calls on the | ||
| caller node and this pass finishes them after the merge. The cases below pin what | ||
| it must NOT do as much as what it must: the single-definition guard, the | ||
| cross-repo-only scope, and the language guard are what keep it from fabricating an | ||
| edge from a name collision. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
|
|
@@ -44,6 +44,7 @@ def _needs(module: str): | |
| needs_java = _needs("tree_sitter_java") | ||
| needs_cpp = _needs("tree_sitter_cpp") | ||
| needs_csharp = _needs("tree_sitter_c_sharp") | ||
| needs_objc = _needs("tree_sitter_objc") | ||
| needs_swift = _needs("tree_sitter_swift") | ||
|
|
||
|
|
||
|
|
@@ -177,6 +178,79 @@ def test_a_defines_member_does_not_answer_a_java_call(): | |
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| PARKED_OBJC = [{"callee": "greet", "receiver_type": "Greeter", "lang": "objc", "line": "L5"}] | ||
|
|
||
|
|
||
| def test_an_objc_selector_answers_through_either_sigil(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. |
||
| # An ObjC method label keeps its +/- sigil while a parked selector carries no | ||
| # class/instance distinction, so both spellings have to be tried. | ||
| for label in ("-greet", "+greet"): | ||
| G = _graph( | ||
| caller=_caller("a", PARKED_OBJC, source_file="src/App.m"), | ||
| declarations=[(_declaration("b", "Greeter", "Greeter.m"), | ||
| _method("b", label, source_file="Greeter.m"))], | ||
| ) | ||
| assert link_cross_repo_member_calls(G) == 1, label | ||
| assert _added_calls(G) == {("a::app_run", "b::greeter_greet")} | ||
|
|
||
|
|
||
| def test_an_objc_callee_that_already_carries_a_sigil_still_binds(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. |
||
| # A parked selector is written bare, but the payload comes from `graph.json` | ||
| # and a sigiled one must not ask the index for `--greet`. | ||
| G = _graph( | ||
| caller=_caller("a", [dict(PARKED_OBJC[0], callee="-greet")], | ||
| source_file="src/App.m"), | ||
| declarations=[(_declaration("b", "Greeter", "Greeter.m"), | ||
| _method("b", "-greet", source_file="Greeter.m"))], | ||
| ) | ||
| assert link_cross_repo_member_calls(G) == 1 | ||
| assert _added_calls(G) == {("a::app_run", "b::greeter_greet")} | ||
|
|
||
|
|
||
| def test_a_type_declaring_both_objc_sigils_binds_nothing(): | ||
| # `+greet` and `-greet` on one class are two different methods and the parked | ||
| # selector cannot say which was meant. | ||
| decl = _declaration("b", "Greeter", "Greeter.m") | ||
| G = _graph( | ||
| caller=_caller("a", PARKED_OBJC, source_file="src/App.m"), | ||
| declarations=[(decl, _method("b", "-greet", "greeter_inst", "Greeter.m"))], | ||
| ) | ||
| class_method = _method("b", "+greet", "greeter_cls", "Greeter.m") | ||
| G.add_node(class_method[0], **class_method[1]) | ||
| G.add_edge(decl[0], class_method[0], relation="method") | ||
|
|
||
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| def test_an_objc_call_does_not_bind_to_a_cpp_member_of_a_shared_header(): | ||
| # `.h` belongs to both languages, so the sigil is what keeps an ObjC `-greet` | ||
| # and a C++ `.greet()` from answering for each other inside one header. | ||
| G = _graph( | ||
| caller=_caller("a", PARKED_OBJC, source_file="src/App.m"), | ||
| declarations=[(_declaration("b", "Greeter", "greeter.h"), | ||
| _method("b", ".greet()", source_file="greeter.h"))], | ||
| ) | ||
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
| G = _graph( | ||
| caller=_caller("a", PARKED_CPP, source_file="src/app.cpp"), | ||
| declarations=[(_declaration("b", "Greeter", "greeter.h"), | ||
| _method("b", "-greet", source_file="greeter.h"))], | ||
| ) | ||
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| def test_an_objc_protocol_does_not_answer_a_parked_call(): | ||
| # A protocol carries the same markers as a class but is labelled `<Greeter>`, | ||
| # which no parked receiver type ever spells. | ||
| G = _graph( | ||
| caller=_caller("a", PARKED_OBJC, source_file="src/App.m"), | ||
| declarations=[(_declaration("b", "<Greeter>", "Greeter.h"), | ||
| _method("b", "-greet", source_file="Greeter.h"))], | ||
| ) | ||
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| def test_the_definition_answers_before_a_same_named_declaration(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
fans out to 6 callees (efferent coupling). Grounded coupling-delta finding (deterministic), not an LLM guess. |
||
| # A C++ class declares `void greet();` in its header (`defines`) and defines | ||
| # it out of line in the `.cpp` (`method`). Both hang off the one folded class | ||
|
|
@@ -364,6 +438,22 @@ def test_a_java_build_parks_the_call_and_the_merge_finishes_it(tmp_path: Path): | |
| ("src/Greeter.cs", "class Greeter { public void Greet() {} }\n"), | ||
| marks=needs_csharp, id="csharp-field-receiver", | ||
| ), | ||
| pytest.param( | ||
| "objc", "greet", | ||
| ("src/App.m", "@interface App : NSObject\n" | ||
| "@property (nonatomic, strong) Greeter *greeter;\n" | ||
| "@end\n" | ||
| "@implementation App\n" | ||
| "- (void)run { [self.greeter greet]; }\n" | ||
| "@end\n"), | ||
| ("src/Greeter.m", "@interface Greeter : NSObject\n" | ||
| "- (void)greet;\n" | ||
| "@end\n" | ||
| "@implementation Greeter\n" | ||
| "- (void)greet {}\n" | ||
| "@end\n"), | ||
| marks=needs_objc, id="objc-property-receiver", | ||
| ), | ||
| pytest.param( | ||
| "swift", "greet", | ||
| ("src/App.swift", "class App {\n" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
link_cross_repo_member_calls()fans out to 8 callees (efferent coupling); 22 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.