Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files.
(path: Path)
| 12404 | |
| 12405 | |
| 12406 | def extract_objc(path: Path) -> dict: |
| 12407 | """Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files.""" |
| 12408 | try: |
| 12409 | import tree_sitter_objc as tsobjc |
| 12410 | from tree_sitter import Language, Parser |
| 12411 | except ImportError: |
| 12412 | return {"nodes": [], "edges": [], "error": "tree_sitter_objc not installed"} |
| 12413 | |
| 12414 | try: |
| 12415 | language = Language(tsobjc.language()) |
| 12416 | parser = Parser(language) |
| 12417 | source = path.read_bytes() |
| 12418 | # tree-sitter-objc cannot expand these argument-less annotation macros (no |
| 12419 | # trailing ';'), and their presence before @interface makes the parser fail to |
| 12420 | # emit a class_interface node (#1475). Blank them to equal-length spaces so byte |
| 12421 | # offsets / line numbers are preserved and the interface parses. |
| 12422 | _OBJC_BLANK_MACROS = (b"NS_ASSUME_NONNULL_BEGIN", b"NS_ASSUME_NONNULL_END") |
| 12423 | for _m in _OBJC_BLANK_MACROS: |
| 12424 | source = source.replace(_m, b" " * len(_m)) |
| 12425 | tree = parser.parse(source) |
| 12426 | root = tree.root_node |
| 12427 | except Exception as e: |
| 12428 | return {"nodes": [], "edges": [], "error": str(e)} |
| 12429 | |
| 12430 | stem = _file_stem(path) |
| 12431 | str_path = str(path) |
| 12432 | nodes: list[dict] = [] |
| 12433 | edges: list[dict] = [] |
| 12434 | seen_ids: set[str] = set() |
| 12435 | method_bodies: list[tuple[str, Any, str]] = [] |
| 12436 | # #1556: unresolved message sends saved for the cross-file ObjC resolver, plus a |
| 12437 | # per-file `var -> ClassName` table from `Foo *f = ...;` local declarations. |
| 12438 | raw_calls: list[dict] = [] |
| 12439 | objc_type_table: dict[str, str] = {} |
| 12440 | |
| 12441 | def add_node(nid: str, label: str, line: int) -> None: |
| 12442 | if nid not in seen_ids: |
| 12443 | seen_ids.add(nid) |
| 12444 | nodes.append({"id": nid, "label": label, "file_type": "code", |
| 12445 | "source_file": str_path, "source_location": f"L{line}"}) |
| 12446 | |
| 12447 | def add_edge(src: str, tgt: str, relation: str, line: int, |
| 12448 | confidence: str = "EXTRACTED", weight: float = 1.0, |
| 12449 | context: str | None = None) -> None: |
| 12450 | edge = {"source": src, "target": tgt, "relation": relation, |
| 12451 | "confidence": confidence, "source_file": str_path, |
| 12452 | "source_location": f"L{line}", "weight": weight} |
| 12453 | if context: |
| 12454 | edge["context"] = context |
| 12455 | edges.append(edge) |
| 12456 | |
| 12457 | file_nid = _make_id(str(path)) |
| 12458 | add_node(file_nid, path.name, 1) |
| 12459 | |
| 12460 | def _read(node) -> str: |
| 12461 | return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace") |
| 12462 | |
| 12463 | def _get_name(node, field: str) -> str | None: |