mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
Mark ObjC declarations with _callable / _callable_class (#3228)
Every other extractor stamps its definitions through the generic engine: `_callable` for "a real callable, not a same-named data symbol" (#2438), narrowed to `_callable_class` for a type, which is callable only through a constructor (#2137). The ObjC extractor builds its nodes by hand and set neither, so an ObjC class was invisible to every pass that indexes declarations by those markers — the `same_type_as` pass from #3007, the qualified-name indexes, and anything new that needs to tell a declaration from a reference — and an ObjC method could not be told apart from a data symbol of the same name. `@interface`, `@implementation` and `@protocol` nodes now carry both markers (a protocol is a type declaration, the same way the engine marks a Java or C# interface), and method nodes carry `_callable`. The file node and the stubs minted for dangling references stay unmarked: a stub has no declaration behind it.
This commit is contained in:
1 parent
a9860f70dc
commit
9a64af9be1
2 files changed
+126
-7
No files matched your search
@@ -13,6 +13,14 @@ import re
|
||||
# `C++Bridge.h` and `Foo+.h` are left intact.
|
||||
_OBJC_STEM_PART = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||||
|
||||
# Declaration markers, matching what the generic engine puts on every other
|
||||
# language's definitions (#2438): `_callable` says "a real callable, not a
|
||||
# same-named data symbol", and `_callable_class` narrows that to a type, which is
|
||||
# callable only through a constructor. Passes that index declarations gate on
|
||||
# these, so a node without them is invisible to them.
|
||||
_CALLABLE = ("_callable",)
|
||||
_CALLABLE_CLASS = ("_callable", "_callable_class")
|
||||
|
||||
|
||||
def _objc_category_base_stem(stem: str) -> str:
|
||||
"""Strip an ObjC category/extension suffix from a file stem (``Foo+Cat`` -> ``Foo``).
|
||||
@@ -116,11 +124,14 @@ def extract_objc(path: Path) -> dict:
|
||||
# same (class, field) tombstones the entry (None) — drop, don't guess.
|
||||
objc_field_types: dict[str, dict[str, str | None]] = {}
|
||||
|
||||
def add_node(nid: str, label: str, line: int) -> None:
|
||||
def add_node(nid: str, label: str, line: int, markers: tuple[str, ...] = ()) -> None:
|
||||
if nid not in seen_ids:
|
||||
seen_ids.add(nid)
|
||||
nodes.append({"id": nid, "label": label, "file_type": "code",
|
||||
"source_file": str_path, "source_location": f"L{line}"})
|
||||
node = {"id": nid, "label": label, "file_type": "code",
|
||||
"source_file": str_path, "source_location": f"L{line}"}
|
||||
for marker in markers:
|
||||
node[marker] = True
|
||||
nodes.append(node)
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
@@ -322,7 +333,7 @@ def extract_objc(path: Path) -> dict:
|
||||
# produced fine when the members lived in `Foo.h` (#1556).
|
||||
cls_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
|
||||
cls_nid = _make_id(cls_stem, name)
|
||||
add_node(cls_nid, name, line)
|
||||
add_node(cls_nid, name, line, _CALLABLE_CLASS)
|
||||
add_edge(file_nid, cls_nid, "contains", line)
|
||||
# superclass is second identifier after ':'
|
||||
colon_seen = False
|
||||
@@ -387,7 +398,7 @@ def extract_objc(path: Path) -> dict:
|
||||
impl_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
|
||||
impl_nid = _make_id(impl_stem, name)
|
||||
if impl_nid not in seen_ids:
|
||||
add_node(impl_nid, name, line)
|
||||
add_node(impl_nid, name, line, _CALLABLE_CLASS)
|
||||
add_edge(file_nid, impl_nid, "contains", line)
|
||||
for child in node.children:
|
||||
if child.type == "instance_variables":
|
||||
@@ -405,7 +416,9 @@ def extract_objc(path: Path) -> dict:
|
||||
break
|
||||
if name:
|
||||
proto_nid = _make_id(stem, name)
|
||||
add_node(proto_nid, f"<{name}>", line)
|
||||
# A protocol is a type declaration like any other interface, and
|
||||
# the engine marks a Java/C# interface the same way.
|
||||
add_node(proto_nid, f"<{name}>", line, _CALLABLE_CLASS)
|
||||
add_edge(file_nid, proto_nid, "contains", line)
|
||||
# Adopted protocols: `@protocol Derived <Base, Other>`. These
|
||||
# nest under a protocol_reference_list node (distinct from the
|
||||
@@ -440,7 +453,7 @@ def extract_objc(path: Path) -> dict:
|
||||
method_name = "".join(parts) if parts else None
|
||||
if method_name:
|
||||
method_nid = _make_id(container, method_name)
|
||||
add_node(method_nid, f"{prefix}{method_name}", line)
|
||||
add_node(method_nid, f"{prefix}{method_name}", line, _CALLABLE)
|
||||
add_edge(container, method_nid, "method", line)
|
||||
if t == "method_definition":
|
||||
method_bodies.append((method_nid, node, container))
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""ObjC declarations carry the `_callable` / `_callable_class` markers.
|
||||
|
||||
Every other extractor stamps its definitions with `_callable` — "a real callable,
|
||||
not a same-named data symbol" (#2438) — and narrows a type to `_callable_class`,
|
||||
callable only through a constructor (#2137). The generic engine does it for all of
|
||||
them in one place (`extractors/engine.py`, `callable_def_nids` /
|
||||
`callable_class_nids`).
|
||||
|
||||
The ObjC extractor builds its nodes by hand and set neither, so an ObjC class was
|
||||
invisible to every pass that indexes declarations by those markers, and an ObjC
|
||||
method could never be told apart from a data symbol of the same name. This pins the
|
||||
markers onto the four node kinds ObjC produces, including the two that must stay
|
||||
unmarked.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
GREETER_H = (
|
||||
"@interface Greeter : NSObject\n"
|
||||
"- (void)greet;\n"
|
||||
"+ (instancetype)shared;\n"
|
||||
"@end\n"
|
||||
)
|
||||
GREETER_M = (
|
||||
"#import \"Greeter.h\"\n"
|
||||
"@implementation Greeter\n"
|
||||
"- (void)greet {}\n"
|
||||
"@end\n"
|
||||
)
|
||||
|
||||
|
||||
def _extract(tmp_path: Path, files: dict[str, str]) -> dict:
|
||||
paths = []
|
||||
for name, body in files.items():
|
||||
path = tmp_path / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(body, encoding="utf-8")
|
||||
paths.append(path)
|
||||
return extract(paths, cache_root=tmp_path / "graphify-out")
|
||||
|
||||
|
||||
def _node(result: dict, label: str) -> dict:
|
||||
matches = [n for n in result["nodes"] if n.get("label") == label]
|
||||
assert len(matches) == 1, [n.get("label") for n in result["nodes"]]
|
||||
return matches[0]
|
||||
|
||||
|
||||
def test_a_class_is_marked_as_a_type(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"Greeter.h": GREETER_H})
|
||||
greeter = _node(result, "Greeter")
|
||||
assert greeter.get("_callable") is True
|
||||
assert greeter.get("_callable_class") is True
|
||||
|
||||
|
||||
def test_an_implementation_only_class_is_marked_too(tmp_path: Path):
|
||||
# A class whose `@interface` is not in this corpus is still a declaration.
|
||||
result = _extract(tmp_path, {"Greeter.m": "@implementation Greeter\n- (void)greet {}\n@end\n"})
|
||||
greeter = _node(result, "Greeter")
|
||||
assert greeter.get("_callable") is True
|
||||
assert greeter.get("_callable_class") is True
|
||||
|
||||
|
||||
def test_a_header_and_impl_pair_keeps_the_markers_after_folding(tmp_path: Path):
|
||||
# `_merge_decl_def_classes` folds the `.h`/`.m` pair into one node; the markers
|
||||
# have to be on whichever node survives.
|
||||
result = _extract(tmp_path, {"Greeter.h": GREETER_H, "Greeter.m": GREETER_M})
|
||||
greeter = _node(result, "Greeter")
|
||||
assert greeter.get("_callable") is True
|
||||
assert greeter.get("_callable_class") is True
|
||||
|
||||
|
||||
def test_a_method_is_callable_but_is_not_a_type(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"Greeter.h": GREETER_H})
|
||||
for label in ("-greet", "+shared"):
|
||||
method = _node(result, label)
|
||||
assert method.get("_callable") is True, label
|
||||
assert "_callable_class" not in method, label
|
||||
|
||||
|
||||
def test_a_protocol_is_marked_like_any_other_interface(tmp_path: Path):
|
||||
# A Java or C# interface gets `_callable_class` from the generic engine, and a
|
||||
# protocol is the same kind of declaration.
|
||||
result = _extract(tmp_path, {"Greeting.h": "@protocol Greeting\n- (void)greet;\n@end\n"})
|
||||
protocol = _node(result, "<Greeting>")
|
||||
assert protocol.get("_callable") is True
|
||||
assert protocol.get("_callable_class") is True
|
||||
|
||||
|
||||
def test_a_dangling_reference_is_not_marked(tmp_path: Path):
|
||||
# `NSObject` is a stub minted for a name this corpus never declares, so it has
|
||||
# no source file and no declaration behind it.
|
||||
result = _extract(tmp_path, {"Greeter.h": GREETER_H})
|
||||
stub = _node(result, "NSObject")
|
||||
assert not stub.get("source_file")
|
||||
assert "_callable" not in stub
|
||||
assert "_callable_class" not in stub
|
||||
|
||||
|
||||
def test_the_file_node_is_not_marked(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"Greeter.h": GREETER_H})
|
||||
file_node = _node(result, "Greeter.h")
|
||||
assert "_callable" not in file_node
|
||||
assert "_callable_class" not in file_node
|
||||
Reference in new issue
Block a user