diff --git a/README.md b/README.md index 89a326162..6fa3f3855 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Type | Extensions | |------|-----------| -| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .metal .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | +| Code (37 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .metal .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml .ada .adb .ads` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | | Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) | | Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) | | MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements | diff --git a/graphify/detect.py b/graphify/detect.py index 92b773f7b..f7ed3ce1b 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -27,7 +27,7 @@ class FileType(str, Enum): _MANIFEST_PATH = str(out_path("manifest.json")) -CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'} +CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.sol', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.ada', '.adb', '.ads'} DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} diff --git a/graphify/extract.py b/graphify/extract.py index 75efb65b0..2033bcdb8 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -6115,6 +6115,675 @@ def walk(node, scope_nid: str) -> None: return {"nodes": nodes, "edges": edges} +# ── Ada extractor (custom walk) ──────────────────────────────────────────── + +def _ada_extract_selected_component_names(node, source: bytes) -> list[str]: + """Extract all identifier names from a selected_component node (e.g., Ada.Text_IO -> ['Ada', 'Text_IO']).""" + names = [] + for child in node.children: + if child.type == "identifier": + name = _read_text(child, source) + if name: + names.append(name) + elif child.type == "selected_component": + names.extend(_ada_extract_selected_component_names(child, source)) + return names + + +def _ada_get_name_from_node(node, source: bytes) -> str | None: + """Extract name from a node using 'name' field or first identifier child.""" + name_node = node.child_by_field_name("name") + if name_node: + return _read_text(name_node, source) + for child in node.children: + if child.type == "identifier": + return _read_text(child, source) + return None + + +def _ada_collect_subtype_mark_refs(node, source: bytes, out: list[tuple[str, str]]) -> None: + """Recursively collect type references from parameter/result/type nodes. + + Handles both subtype_mark nodes and plain identifier nodes in type positions. + """ + if node is None: + return + + # Handle subtype_mark nodes + if node.type == "subtype_mark": + name = _read_text(node, source) + if name: + out.append((name, "type")) + return + + # Handle selected_component (e.g., Ada.Text_IO) + if node.type == "selected_component": + names = _ada_extract_selected_component_names(node, source) + if names: + out.append((".".join(names), "type")) + return + + # Handle parameter_specification specially: skip param name, collect type + if node.type == "parameter_specification": + identifiers = [c for c in node.children if c.type == "identifier"] + # In Ada, parameter_specification is: identifier ':' type + # So the type is the last identifier + if len(identifiers) >= 2: + type_id = identifiers[-1] + name = _read_text(type_id, source) + if name: + out.append((name, "type")) + elif len(identifiers) == 1: + # Single identifier could be the type (anonymous parameter) + name = _read_text(identifiers[0], source) + if name: + out.append((name, "type")) + return + + # Handle result_profile: collect the identifier after 'return' + if node.type == "result_profile": + for child in node.children: + if child.type == "identifier": + name = _read_text(child, source) + if name: + out.append((name, "type")) + elif child.type == "subtype_mark": + name = _read_text(child, source) + if name: + out.append((name, "type")) + elif child.is_named and child.type not in ("identifier",): + _ada_collect_subtype_mark_refs(child, source, out) + return + + # Handle plain identifier nodes (in type positions like component_definition) + if node.type == "identifier": + name = _read_text(node, source) + if name: + out.append((name, "type")) + return + + # Recurse into children for other node types + for child in node.children: + if child.is_named: + _ada_collect_subtype_mark_refs(child, source, out) + + +def _ada_extract_call_name(node, source: bytes) -> str | None: + """Extract the callable name from a function_call or procedure_call_statement node.""" + name_node = node.child_by_field_name("name") + if not name_node: + return None + if name_node.type == "selected_component": + names = _ada_extract_selected_component_names(name_node, source) + return names[-1] if names else None + return _read_text(name_node, source) + + +def extract_ada(path: Path) -> dict: + """Extract packages, subprograms, types, imports, calls, and type references from Ada files. + + Handles: + - Packages (spec and body) + - Subprograms (procedures and functions, declarations and bodies) + - Types (record, derived, interface, array, access, enumeration) + - Subtypes + - Tasks and protected objects (types and bodies) + - Entries (declarations and bodies) + - Generics (packages and instantiations) + - Exceptions + - Object declarations with type references + - With and use clauses (imports) + - Call graphs (procedure and function calls) + - Type reference edges (parameter types, return types, field types) + - Inheritance/derivation edges + """ + try: + import tree_sitter_ada as tsada + from tree_sitter import Language, Parser + except ImportError: + return {"nodes": [], "edges": [], "error": "tree-sitter-ada not installed"} + + try: + language = Language(tsada.language()) + parser = Parser(language) + source = path.read_bytes() + tree = parser.parse(source) + root = tree.root_node + except Exception as e: + return {"nodes": [], "edges": [], "error": str(e)} + + stem = _file_stem(path) + str_path = str(path) + nodes: list[dict] = [] + edges: list[dict] = [] + seen_ids: set[str] = set() + function_bodies: list[tuple[str, object]] = [] + + def add_node(nid: str, label: str, line: int) -> 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}", + }) + + def add_edge(src: str, tgt: str, relation: str, line: int, + confidence: str = "EXTRACTED", weight: float = 1.0, + context: str | None = None) -> None: + edge = { + "source": src, + "target": tgt, + "relation": relation, + "confidence": confidence, + "source_file": str_path, + "source_location": f"L{line}", + "weight": weight, + } + if context: + edge["context"] = context + edges.append(edge) + + file_nid = _make_id(str(path)) + add_node(file_nid, path.name, 1) + + def ensure_named_node(name: str, line: int) -> str: + nid = _make_id(stem, name) + if nid in seen_ids: + return nid + nid = _make_id(name) + if nid not in seen_ids: + seen_ids.add(nid) + nodes.append({ + "id": nid, + "label": name, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + return nid + + def _emit_type_refs(owner_nid: str, type_node, line: int, context: str) -> None: + """Emit references edges for type references in a type expression.""" + refs: list[tuple[str, str]] = [] + _ada_collect_subtype_mark_refs(type_node, source, refs) + for ref_name, _role in refs: + target_nid = ensure_named_node(ref_name, line) + if target_nid != owner_nid: + edges.append(_semantic_reference_edge( + owner_nid, target_nid, context, str_path, line)) + + def walk_calls(body_node, func_nid: str) -> None: + """Walk a body node to find function and procedure calls.""" + if body_node is None: + return + t = body_node.type + if t in ("subprogram_body", "package_body", "task_body", "protected_body"): + return + if t in ("function_call", "procedure_call_statement"): + name = _ada_extract_call_name(body_node, source) + if name: + target_nid = ensure_named_node(name, body_node.start_point[0] + 1) + add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1, + confidence="EXTRACTED", context="call") + elif t == "exception_handler": + # Exception handler references exceptions via exception_choice_list > exception_choice + for child in body_node.children: + if child.type == "exception_choice_list": + for choice in child.children: + if choice.type == "exception_choice": + for cc in choice.children: + if cc.type == "identifier": + exc_name = _read_text(cc, source) + if exc_name: + exc_nid = ensure_named_node(exc_name, body_node.start_point[0] + 1) + add_edge(func_nid, exc_nid, "references", body_node.start_point[0] + 1, + confidence="EXTRACTED", context="exception_handler") + elif cc.type == "selected_component": + names = _ada_extract_selected_component_names(cc, source) + if names: + exc_nid = ensure_named_node(names[-1], body_node.start_point[0] + 1) + add_edge(func_nid, exc_nid, "references", body_node.start_point[0] + 1, + confidence="EXTRACTED", context="exception_handler") + elif t == "raise_statement": + # Raise statement references an exception + for child in body_node.children: + if child.type == "identifier": + exc_name = _read_text(child, source) + if exc_name: + exc_nid = ensure_named_node(exc_name, body_node.start_point[0] + 1) + add_edge(func_nid, exc_nid, "references", body_node.start_point[0] + 1, + confidence="EXTRACTED", context="raise") + elif child.type == "selected_component": + names = _ada_extract_selected_component_names(child, source) + if names: + exc_name = names[-1] + exc_nid = ensure_named_node(exc_name, body_node.start_point[0] + 1) + add_edge(func_nid, exc_nid, "references", body_node.start_point[0] + 1, + confidence="EXTRACTED", context="raise") + for child in body_node.children: + walk_calls(child, func_nid) + + def walk(node, scope_nid: str) -> None: + t = node.type + line = node.start_point[0] + 1 + + # Package declaration or body + if t in ("package_declaration", "package_body"): + name = _ada_get_name_from_node(node, source) + if name: + pkg_nid = _make_id(stem, name) + add_node(pkg_nid, name, line) + add_edge(scope_nid, pkg_nid, "defines", line) + for child in node.children: + walk(child, pkg_nid) + return + + # Generic package declaration + if t == "generic_package_declaration": + # Find the nested package_declaration to get the name + pkg_decl = None + for child in node.children: + if child.type == "package_declaration": + pkg_decl = child + break + + if pkg_decl: + name = _ada_get_name_from_node(pkg_decl, source) + if name: + pkg_nid = _make_id(stem, name) + add_node(pkg_nid, name, line) + add_edge(scope_nid, pkg_nid, "defines", line) + # Walk the package_declaration to extract its contents + for child in pkg_decl.children: + walk(child, pkg_nid) + return + + # Subprogram body (procedure or function with body) + if t == "subprogram_body": + spec_node = None + for child in node.children: + if child.type in ("procedure_specification", "function_specification"): + spec_node = child + break + if spec_node: + func_name = _ada_get_name_from_node(spec_node, source) + if func_name: + func_nid = _make_id(stem, func_name) + add_node(func_nid, f"{func_name}()", line) + add_edge(scope_nid, func_nid, "defines", line) + # Extract parameter type references + for child in spec_node.children: + if child.type == "formal_part": + for param in child.children: + if param.type == "parameter_specification": + _emit_type_refs(func_nid, param, line, "parameter_type") + elif child.type == "result_profile": + _emit_type_refs(func_nid, child, line, "return_type") + function_bodies.append((func_nid, node)) + return + + # Subprogram declaration (procedure or function without body) + if t == "subprogram_declaration": + spec_node = None + for child in node.children: + if child.type in ("procedure_specification", "function_specification"): + spec_node = child + break + if spec_node: + func_name = _ada_get_name_from_node(spec_node, source) + if func_name: + func_nid = _make_id(stem, func_name) + add_node(func_nid, f"{func_name}()", line) + add_edge(scope_nid, func_nid, "defines", line) + # Extract parameter type references + for child in spec_node.children: + if child.type == "formal_part": + for param in child.children: + if param.type == "parameter_specification": + _emit_type_refs(func_nid, param, line, "parameter_type") + elif child.type == "result_profile": + _emit_type_refs(func_nid, child, line, "return_type") + return + + # Expression function declaration (function with expression body) + if t == "expression_function_declaration": + spec_node = None + for child in node.children: + if child.type == "function_specification": + spec_node = child + break + if spec_node: + func_name = _ada_get_name_from_node(spec_node, source) + if func_name: + func_nid = _make_id(stem, func_name) + add_node(func_nid, f"{func_name}()", line) + add_edge(scope_nid, func_nid, "defines", line) + # Extract parameter type references + for child in spec_node.children: + if child.type == "formal_part": + for param in child.children: + if param.type == "parameter_specification": + _emit_type_refs(func_nid, param, line, "parameter_type") + elif child.type == "result_profile": + _emit_type_refs(func_nid, child, line, "return_type") + return + + # Null procedure declaration + if t == "null_procedure_declaration": + spec_node = None + for child in node.children: + if child.type == "procedure_specification": + spec_node = child + break + if spec_node: + func_name = _ada_get_name_from_node(spec_node, source) + if func_name: + func_nid = _make_id(stem, func_name) + add_node(func_nid, f"{func_name}()", line) + add_edge(scope_nid, func_nid, "defines", line) + # Extract parameter type references + for child in spec_node.children: + if child.type == "formal_part": + for param in child.children: + if param.type == "parameter_specification": + _emit_type_refs(func_nid, param, line, "parameter_type") + return + + # Type declarations + if t == "full_type_declaration": + has_task_or_protected = any( + c.type in ("task_type_declaration", "protected_type_declaration") + for c in node.children + ) + if has_task_or_protected: + for child in node.children: + walk(child, scope_nid) + return + name = _ada_get_name_from_node(node, source) + if name: + type_nid = _make_id(stem, name) + add_node(type_nid, name, line) + add_edge(scope_nid, type_nid, "defines", line) + # Check for derived type (inheritance) + for child in node.children: + if child.type == "derived_type_definition": + sm = child.child_by_field_name("subtype_mark") + if sm: + parent_name = _read_text(sm, source) + if parent_name: + parent_nid = ensure_named_node(parent_name, line) + add_edge(type_nid, parent_nid, "inherits", line) + # Check for interface implementations + for sub in child.children: + if sub.type == "identifier": + iface_name = _read_text(sub, source) + if iface_name and iface_name != name: + iface_nid = ensure_named_node(iface_name, line) + add_edge(type_nid, iface_nid, "implements", line) + elif child.type == "interface_type_definition": + # Interface type itself - no special handling needed + pass + elif child.type == "record_type_definition": + # Extract field type references + for rec_def in child.children: + if rec_def.type == "record_definition": + for comp_list in rec_def.children: + if comp_list.type == "component_list": + for comp_decl in comp_list.children: + if comp_decl.type == "component_declaration": + for comp_def in comp_decl.children: + if comp_def.type == "component_definition": + _emit_type_refs(type_nid, comp_def, line, "field") + elif child.type in ("array_type_definition", "access_to_object_definition", + "access_to_subprogram_definition"): + _emit_type_refs(type_nid, child, line, "field") + return + + # Subtype declaration + if t == "subtype_declaration": + name = _ada_get_name_from_node(node, source) + if name: + subtype_nid = _make_id(stem, name) + add_node(subtype_nid, name, line) + add_edge(scope_nid, subtype_nid, "defines", line) + sm = node.child_by_field_name("subtype_mark") + if sm: + parent_name = _read_text(sm, source) + if parent_name: + parent_nid = ensure_named_node(parent_name, line) + add_edge(subtype_nid, parent_nid, "inherits", line) + return + + # Task type declaration + if t == "task_type_declaration": + name = _ada_get_name_from_node(node, source) + if name: + task_nid = _make_id(stem, name) + add_node(task_nid, name, line) + add_edge(scope_nid, task_nid, "defines", line) + for child in node.children: + walk(child, task_nid) + return + + # Task body + if t == "task_body": + name = _ada_get_name_from_node(node, source) + if name: + task_nid = _make_id(stem, name) + add_node(task_nid, name, line) + add_edge(scope_nid, task_nid, "defines", line) + function_bodies.append((task_nid, node)) + for child in node.children: + walk(child, task_nid) + return + + # Protected type declaration + if t == "protected_type_declaration": + name = _ada_get_name_from_node(node, source) + if name: + prot_nid = _make_id(stem, name) + add_node(prot_nid, name, line) + add_edge(scope_nid, prot_nid, "defines", line) + for child in node.children: + walk(child, prot_nid) + return + + # Protected body + if t == "protected_body": + name = _ada_get_name_from_node(node, source) + if name: + prot_nid = _make_id(stem, name) + add_node(prot_nid, name, line) + add_edge(scope_nid, prot_nid, "defines", line) + function_bodies.append((prot_nid, node)) + for child in node.children: + walk(child, prot_nid) + return + + # Entry declaration (within task or protected) + if t == "entry_declaration": + name = _ada_get_name_from_node(node, source) + if name: + entry_nid = _make_id(scope_nid, name) + add_node(entry_nid, f"{name}()", line) + add_edge(scope_nid, entry_nid, "defines", line) + # Extract parameter type references + for child in node.children: + if child.type == "parameter_specification": + _emit_type_refs(entry_nid, child, line, "parameter_type") + return + + # Entry body + if t == "entry_body": + name = _ada_get_name_from_node(node, source) + if name: + entry_nid = _make_id(scope_nid, name) + add_node(entry_nid, f"{name}()", line) + add_edge(scope_nid, entry_nid, "defines", line) + function_bodies.append((entry_nid, node)) + return + + # Single task declaration (task My_Task is ... end My_Task;) + if t == "single_task_declaration": + name = _ada_get_name_from_node(node, source) + if name: + task_nid = _make_id(stem, name) + add_node(task_nid, name, line) + add_edge(scope_nid, task_nid, "defines", line) + for child in node.children: + walk(child, task_nid) + return + + # Single protected object declaration (protected My_Object is ... end My_Object;) + if t == "single_protected_declaration": + name = _ada_get_name_from_node(node, source) + if name: + prot_nid = _make_id(stem, name) + add_node(prot_nid, name, line) + add_edge(scope_nid, prot_nid, "defines", line) + for child in node.children: + walk(child, prot_nid) + return + + # Object renaming declaration (X : Integer renames Y;) + if t == "object_renaming_declaration": + name = _ada_get_name_from_node(node, source) + if name: + ren_nid = _make_id(stem, name) + add_node(ren_nid, name, line) + add_edge(scope_nid, ren_nid, "defines", line) + # Structure: identifier (name) : identifier (type) renames identifier (renamed_obj) + identifiers = [c for c in node.children if c.type == "identifier"] + if len(identifiers) >= 2: + # Second identifier is the type + type_name = _read_text(identifiers[1], source) + if type_name: + type_nid = ensure_named_node(type_name, line) + edges.append(_semantic_reference_edge( + ren_nid, type_nid, "field", str_path, line)) + if len(identifiers) >= 3: + # Third identifier is the renamed object + renamed_obj = _read_text(identifiers[2], source) + if renamed_obj: + renamed_nid = ensure_named_node(renamed_obj, line) + add_edge(ren_nid, renamed_nid, "references", line, + confidence="EXTRACTED", context="renames") + return + + # Exception declaration + if t == "exception_declaration": + name = _ada_get_name_from_node(node, source) + if name: + exc_nid = _make_id(stem, name) + add_node(exc_nid, name, line) + add_edge(scope_nid, exc_nid, "defines", line) + return + + # Object declaration (constants and variables) + if t == "object_declaration": + # Check if this wraps a single_task_declaration or single_protected_declaration + has_task_or_protected = any( + c.type in ("single_task_declaration", "single_protected_declaration") + for c in node.children + ) + if has_task_or_protected: + for child in node.children: + walk(child, scope_nid) + return + name = _ada_get_name_from_node(node, source) + if name: + obj_nid = _make_id(stem, name) + add_node(obj_nid, name, line) + add_edge(scope_nid, obj_nid, "defines", line) + # Extract type reference + sm = node.child_by_field_name("subtype_mark") + if sm: + type_name = _read_text(sm, source) + if type_name: + type_nid = ensure_named_node(type_name, line) + edges.append(_semantic_reference_edge( + obj_nid, type_nid, "field", str_path, line)) + return + + # With clause (import) + if t == "with_clause": + for child in node.children: + if child.type == "selected_component": + names = _ada_extract_selected_component_names(child, source) + if names: + mod_name = ".".join(names) + mod_nid = _make_id(mod_name) + add_node(mod_nid, mod_name, line) + add_edge(scope_nid, mod_nid, "imports", line, context="import") + elif child.type == "identifier": + mod_name = _read_text(child, source) + if mod_name: + mod_nid = _make_id(mod_name) + add_node(mod_nid, mod_name, line) + add_edge(scope_nid, mod_nid, "imports", line, context="import") + return + + # Use clause + if t == "use_clause": + for child in node.children: + if child.type == "selected_component": + names = _ada_extract_selected_component_names(child, source) + if names: + mod_name = ".".join(names) + mod_nid = _make_id(mod_name) + add_node(mod_nid, mod_name, line) + add_edge(scope_nid, mod_nid, "imports", line, context="use") + elif child.type == "identifier": + mod_name = _read_text(child, source) + if mod_name: + mod_nid = _make_id(mod_name) + add_node(mod_nid, mod_name, line) + add_edge(scope_nid, mod_nid, "imports", line, context="use") + return + + # Generic instantiation + if t == "generic_instantiation": + name = _ada_get_name_from_node(node, source) + if name: + inst_nid = _make_id(stem, name) + add_node(inst_nid, name, line) + add_edge(scope_nid, inst_nid, "defines", line) + # Find the function_call child which contains the generic being instantiated + for child in node.children: + if child.type == "function_call": + # First identifier in function_call is the generic name + for fc_child in child.children: + if fc_child.type == "identifier": + generic_name = _read_text(fc_child, source) + if generic_name: + generic_nid = ensure_named_node(generic_name, line) + add_edge(inst_nid, generic_nid, "instantiates", line) + break + elif fc_child.type == "selected_component": + gnames = _ada_extract_selected_component_names(fc_child, source) + if gnames: + generic_name = gnames[-1] + generic_nid = ensure_named_node(generic_name, line) + add_edge(inst_nid, generic_nid, "instantiates", line) + break + break + return + + for child in node.children: + walk(child, scope_nid) + + walk(root, file_nid) + + for func_nid, body_node in function_bodies: + for child in body_node.children: + walk_calls(child, func_nid) + + return {"nodes": nodes, "edges": edges} + + _FORTRAN_CPP_EXTS = {".F", ".F90", ".F95", ".F03", ".F08"} @@ -12821,6 +13490,9 @@ def _body_of(block): ".cshtml": extract_razor, ".cls": extract_apex, ".trigger": extract_apex, + ".ada": extract_ada, + ".adb": extract_ada, + ".ads": extract_ada, } diff --git a/pyproject.toml b/pyproject.toml index 5124207f9..d79f3e649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ dependencies = [ "tree-sitter-fortran>=0.6,<0.8", "tree-sitter-bash>=0.23,<0.27", "tree-sitter-json>=0.23,<0.26", + "tree-sitter-solidity>=1.2,<2.0", + "tree-sitter-ada>=0.1,<0.2", ] [project.urls] diff --git a/tests/fixtures/sample.adb b/tests/fixtures/sample.adb new file mode 100644 index 000000000..00a767e09 --- /dev/null +++ b/tests/fixtures/sample.adb @@ -0,0 +1,76 @@ +with Ada.Text_IO; +with Ada.Integer_Text_IO; + +package body Sample_Package is + + procedure Initialize(C : in out Counter_Type) is + begin + C.Value := 0; + end Initialize; + + function Get_Value(C : Counter_Type) return Integer is + begin + return C.Value; + end Get_Value; + + overriding procedure Speak (D : Dog_Type) is + begin + Ada.Text_IO.Put_Line ("Woof"); + Helper (D.ID); + exception + when Constraint_Error => + Handle_Constraint_Error; + when My_Error => + Handle_My_Error; + end Speak; + + procedure Safe_Operation is + begin + if Counter < 0 then + raise Program_Error with "negative counter"; + end if; + exception + when others => + null; + end Safe_Operation; + + task body Worker_Task is + Local : Integer := Compute (42); + begin + accept Start do + Ada.Text_IO.Put_Line("Worker started"); + Process (Local); + end Start; + + accept Stop do + Ada.Text_IO.Put_Line("Worker stopped"); + end Stop; + end Worker_Task; + + protected body Shared_Counter is + + procedure Increment is + begin + Count := Count + 1; + Notify; + end Increment; + + function Get_Count return Integer is + begin + return Count; + end Get_Count; + + end Shared_Counter; + + package body Generic_Img is + Current : Item; + + function To_String (I : Item) return String is + begin + return Image (I); + end To_String; + end Generic_Img; + + package Integer_Container is new Generic_Img (Integer, Image => Integer_Text_IO.Image); + +end Sample_Package; diff --git a/tests/fixtures/sample.ads b/tests/fixtures/sample.ads new file mode 100644 index 000000000..21a9c1777 --- /dev/null +++ b/tests/fixtures/sample.ads @@ -0,0 +1,93 @@ +with Ada.Text_IO; +with Ada.Integer_Text_IO; +with Ada.Containers.Vectors; + +package Sample_Package is + + type Base_Type is tagged record + ID : Integer; + end record; + + type Derived_Type is new Base_Type with record + Extra : Float; + end record; + + type Animal_Interface is interface; + procedure Speak (A : Animal_Interface) is abstract; + + type Dog_Type is new Base_Type and Animal_Interface with record + Breed : String(1..20); + end record; + + overriding procedure Speak (D : Dog_Type); + + subtype Positive_Integer is Integer range 1 .. Integer'Last; + + type Color_Enum is (Red, Green, Blue); + + type Color_Array is array (Color_Enum) of Integer; + + type Access_To_Integer is access all Integer; + + type Func_Access is access function (X : Integer) return Integer; + + type Record_With_Access is record + Ptr : Access_To_Integer; + end record; + + Max_Size : constant Integer := 100; + Counter : Integer := 0; + + My_Error : exception; + + function Expr_Func (X : Integer) return Integer is (X * 2); + + procedure Null_Proc; + + package Vec_Pkg is new Ada.Containers.Vectors (Integer, Integer); + + generic + type Item is private; + with function Image (I : Item) return String; + package Generic_Img is + function To_String (I : Item) return String; + end Generic_Img; + + type Counter_Type is record + Value : Integer; + Name : String(1..10); + end record; + + procedure Initialize(C : in out Counter_Type); + + function Get_Value(C : Counter_Type) return Integer; + + task type Worker_Task is + entry Start; + entry Stop; + end Worker_Task; + + protected type Shared_Counter is + procedure Increment; + function Get_Count return Integer; + private + Count : Integer := 0; + end Shared_Counter; + + -- Single task declaration + task Monitor_Task is + entry Check; + end Monitor_Task; + + -- Single protected object declaration + protected Shared_Buffer is + procedure Add(Item : Integer); + function Get return Integer; + private + Data : Integer := 0; + end Shared_Buffer; + + -- Object renaming declaration + Renamed_Value : Integer renames Counter; + +end Sample_Package; diff --git a/tests/test_languages.py b/tests/test_languages.py index d2fd43906..5fbebd0f6 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -9,7 +9,7 @@ extract_groovy, extract_sln, extract_csproj, extract_xaml, extract_razor, extract_dm, extract_dmi, extract_dmm, extract_dmf, extract_powershell, extract_apex, extract_verilog, - extract_powershell_manifest, + extract_powershell_manifest, extract_ada, ) FIXTURES = Path(__file__).parent / "fixtures" @@ -2074,3 +2074,274 @@ def test_systemverilog_no_dangling_edges(): for e in r["edges"]: assert e["source"] in node_ids, f"dangling source: {e}" assert e["target"] in node_ids, f"dangling target: {e}" + + +# ── Ada ────────────────────────────────────────────────────────────────────── + + +def test_ada_no_error_spec(): + r = extract_ada(FIXTURES / "sample.ads") + assert "error" not in r + + +def test_ada_no_error_body(): + r = extract_ada(FIXTURES / "sample.adb") + assert "error" not in r + + +def test_ada_finds_package(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Sample_Package" in l for l in _labels(r)) + + +def test_ada_finds_procedure(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Initialize" in l for l in _labels(r)) + + +def test_ada_finds_function(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Get_Value" in l for l in _labels(r)) + + +def test_ada_finds_type(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Counter_Type" in l for l in _labels(r)) + + +def test_ada_finds_task(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Worker_Task" in l for l in _labels(r)) + + +def test_ada_finds_protected(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Shared_Counter" in l for l in _labels(r)) + + +def test_ada_finds_generic_package(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Generic_Img" in l for l in _labels(r)) + + +def test_ada_finds_imports(): + r = extract_ada(FIXTURES / "sample.ads") + assert "imports" in _relations(r) + + +def test_ada_finds_generic_instantiation(): + r = extract_ada(FIXTURES / "sample.adb") + assert any("Integer_Container" in l for l in _labels(r)) + + +def test_ada_import_edges_have_import_context(): + r = extract_ada(FIXTURES / "sample.ads") + import_edges = _edges_with_relation(r, "imports") + assert import_edges + assert all(e.get("context") in ("import", "use") for e in import_edges) + + +def test_ada_no_dangling_edges(): + r = extract_ada(FIXTURES / "sample.ads") + node_ids = {n["id"] for n in r["nodes"]} + for e in r["edges"]: + assert e["source"] in node_ids, f"dangling source: {e}" + assert e["target"] in node_ids, f"dangling target: {e}" + + +def test_ada_body_no_dangling_edges(): + r = extract_ada(FIXTURES / "sample.adb") + node_ids = {n["id"] for n in r["nodes"]} + for e in r["edges"]: + assert e["source"] in node_ids, f"dangling source: {e}" + assert e["target"] in node_ids, f"dangling target: {e}" + + +def test_ada_finds_calls(): + r = extract_ada(FIXTURES / "sample.adb") + calls = _calls(r) + assert any("Put_Line" in target for _, target in calls) + + +def test_ada_finds_derived_type(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Derived_Type" in l for l in _labels(r)) + + +def test_ada_inheritance_edges(): + r = extract_ada(FIXTURES / "sample.ads") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + inherits = _edges_with_relation(r, "inherits") + assert inherits + # Derived_Type inherits from Base_Type + assert any( + "Derived_Type" in node_by_id.get(e["source"], "") + and "Base_Type" in node_by_id.get(e["target"], "") + for e in inherits + ) + + +def test_ada_implements_edges(): + r = extract_ada(FIXTURES / "sample.ads") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + implements = _edges_with_relation(r, "implements") + assert implements + # Dog_Type implements Animal_Interface + assert any( + "Dog_Type" in node_by_id.get(e["source"], "") + and "Animal_Interface" in node_by_id.get(e["target"], "") + for e in implements + ) + + +def test_ada_finds_interface(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Animal_Interface" in l for l in _labels(r)) + + +def test_ada_finds_subtype(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Positive_Integer" in l for l in _labels(r)) + + +def test_ada_subtype_inherits(): + r = extract_ada(FIXTURES / "sample.ads") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + inherits = _edges_with_relation(r, "inherits") + # Positive_Integer inherits from Integer + assert any( + "Positive_Integer" in node_by_id.get(e["source"], "") + and "Integer" in node_by_id.get(e["target"], "") + for e in inherits + ) + + +def test_ada_finds_enumeration_type(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Color_Enum" in l for l in _labels(r)) + + +def test_ada_finds_array_type(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Color_Array" in l for l in _labels(r)) + + +def test_ada_finds_access_type(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Access_To_Integer" in l for l in _labels(r)) + + +def test_ada_finds_exception(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("My_Error" in l for l in _labels(r)) + + +def test_ada_finds_expression_function(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Expr_Func" in l for l in _labels(r)) + + +def test_ada_finds_null_procedure(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Null_Proc" in l for l in _labels(r)) + + +def test_ada_finds_object_declaration(): + r = extract_ada(FIXTURES / "sample.ads") + assert any("Max_Size" in l for l in _labels(r)) + assert any("Counter" in l for l in _labels(r)) + + +def test_ada_object_type_references(): + r = extract_ada(FIXTURES / "sample.ads") + refs = _references(r) + # Max_Size references Integer + assert any("Max_Size" in src and "Integer" in tgt for src, tgt, _ in refs) + + +def test_ada_parameter_type_references(): + r = extract_ada(FIXTURES / "sample.ads") + refs = _references(r) + # Initialize references Counter_Type + assert any("Initialize" in src and "Counter_Type" in tgt for src, tgt, _ in refs) + + +def test_ada_return_type_references(): + r = extract_ada(FIXTURES / "sample.ads") + refs = _references(r) + # Get_Value references Integer + assert any("Get_Value" in src and "Integer" in tgt for src, tgt, _ in refs) + +def test_ada_field_type_references(): + r = extract_ada(FIXTURES / "sample.ads") + refs = _references(r) + # Counter_Type fields reference Integer and String + assert any("Counter_Type" in src and "Integer" in tgt for src, tgt, _ in refs) + + +def test_ada_multiple_calls(): + r = extract_ada(FIXTURES / "sample.adb") + calls = _calls(r) + # Worker_Task calls Put_Line, Process, Compute + assert any("Put_Line" in target for _, target in calls) + assert any("Process" in target for _, target in calls) + assert any("Compute" in target for _, target in calls) + + +def test_ada_nested_package_body(): + r = extract_ada(FIXTURES / "sample.adb") + # Generic_Img body should be found + assert any("Generic_Img" in l for l in _labels(r)) + # To_String function in Generic_Img + assert any("To_String" in l for l in _labels(r)) + + +def test_ada_generic_instantiation_instantiates(): + r = extract_ada(FIXTURES / "sample.adb") + instantiates = _edges_with_relation(r, "instantiates") + assert instantiates + # Integer_Container instantiates Generic_Img + assert any("integer_container" in e["source"] and "generic_img" in e["target"] for e in instantiates) + + +def test_ada_single_task_declaration(): + r = extract_ada(FIXTURES / "sample.ads") + # Monitor_Task should be found + assert any("Monitor_Task" in l for l in _labels(r)) + # Check for entry (Check) + assert any("Check" in l for l in _labels(r)) + + +def test_ada_single_protected_declaration(): + r = extract_ada(FIXTURES / "sample.ads") + # Shared_Buffer should be found + assert any("Shared_Buffer" in l for l in _labels(r)) + # Check for procedures/functions inside + assert any("Add" in l for l in _labels(r)) + assert any("Get" in l for l in _labels(r)) + + +def test_ada_object_renaming_declaration(): + r = extract_ada(FIXTURES / "sample.ads") + # Renamed_Value should be found + assert any("Renamed_Value" in l for l in _labels(r)) + # Should have a reference to Counter (the renamed object) + refs = _edges_with_relation(r, "references") + assert any("renamed_value" in e["source"] and "counter" in e["target"] for e in refs) + + +def test_ada_exception_handler(): + r = extract_ada(FIXTURES / "sample.adb") + # Check that exception handlers create references + refs = _edges_with_relation(r, "references") + # Speak procedure should reference Constraint_Error and My_Error + assert any("constraint_error" in e["target"] for e in refs) + assert any("my_error" in e["target"] for e in refs) + + +def test_ada_raise_statement(): + r = extract_ada(FIXTURES / "sample.adb") + # Check that raise statements create references + refs = _edges_with_relation(r, "references") + # Safe_Operation should reference Program_Error + assert any("program_error" in e["target"] for e in refs)