"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia, Fortran, JS/TS, .NET project files.""" from __future__ import annotations from pathlib import Path import pytest from graphify.extract import ( extract_java, extract_c, extract_cpp, extract_ruby, extract_csharp, extract_kotlin, extract_scala, extract_php, extract_swift, extract_go, extract_julia, extract_js, extract_fortran, extract_groovy, extract_sln, extract_csproj, extract_razor, extract_dm, extract_dmi, extract_dmm, extract_dmf, ) FIXTURES = Path(__file__).parent / "fixtures" def _labels(r): return [n["label"] for n in r["nodes"]] def _relations(r): return {e["relation"] for e in r["edges"]} def _calls(r): node_by_id = {n["id"]: n["label"] for n in r["nodes"]} return { (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in r["edges"] if e["relation"] == "calls" } def _references(r): node_by_id = {n["id"]: n["label"] for n in r["nodes"]} return [ ( node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]), e, ) for e in r["edges"] if e["relation"] == "references" ] def _edges_with_relation(r, *relations): return [e for e in r["edges"] if e["relation"] in relations] def _normalize_symbol_label(label: str) -> str: return label.strip("()").lstrip(".") def _node_by_label(result: dict, label: str) -> dict: for node in result["nodes"]: if node.get("label") == label or _normalize_symbol_label(node.get("label", "")) == label: return node raise AssertionError(f"missing node label {label!r}") def _edge_labels(result: dict, relation: str, context: str | None = None) -> set[tuple[str, str]]: labels = {node["id"]: _normalize_symbol_label(node["label"]) for node in result["nodes"]} pairs = set() for edge in result["edges"]: if edge.get("relation") != relation: continue if context is not None and edge.get("context") != context: continue pairs.add((labels.get(edge["source"], edge["source"]), labels.get(edge["target"], edge["target"]))) return pairs # ── Java ────────────────────────────────────────────────────────────────────── def test_java_no_error(): r = extract_java(FIXTURES / "sample.java") assert "error" not in r def test_java_finds_class(): r = extract_java(FIXTURES / "sample.java") assert any("DataProcessor" in l for l in _labels(r)) def test_java_finds_interface(): r = extract_java(FIXTURES / "sample.java") assert any("Processor" in l for l in _labels(r)) def test_java_finds_methods(): r = extract_java(FIXTURES / "sample.java") labels = _labels(r) assert any("addItem" in l for l in labels) assert any("process" in l for l in labels) def test_java_finds_imports(): r = extract_java(FIXTURES / "sample.java") assert "imports" in _relations(r) def test_java_import_edges_have_import_context(): r = extract_java(FIXTURES / "sample.java") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_java_no_dangling_edges(): r = extract_java(FIXTURES / "sample.java") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids # ── C ──────────────────────────────────────────────────────────────────────── def test_c_no_error(): r = extract_c(FIXTURES / "sample.c") assert "error" not in r def test_c_finds_functions(): r = extract_c(FIXTURES / "sample.c") labels = _labels(r) assert any("process" in l for l in labels) assert any("main" in l for l in labels) def test_c_finds_includes(): r = extract_c(FIXTURES / "sample.c") assert "imports" in _relations(r) def test_c_emits_calls(): r = extract_c(FIXTURES / "sample.c") assert any(e["relation"] == "calls" for e in r["edges"]) def test_c_calls_are_extracted(): r = extract_c(FIXTURES / "sample.c") for e in r["edges"]: if e["relation"] == "calls": assert e["confidence"] == "EXTRACTED" def test_c_import_edges_have_import_context(): r = extract_c(FIXTURES / "sample.c") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_c_call_edges_have_call_context(): r = extract_c(FIXTURES / "sample.c") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) # ── C++ ─────────────────────────────────────────────────────────────────────── def test_cpp_no_error(): r = extract_cpp(FIXTURES / "sample.cpp") assert "error" not in r def test_cpp_finds_class(): r = extract_cpp(FIXTURES / "sample.cpp") assert any("HttpClient" in l for l in _labels(r)) def test_cpp_finds_methods(): r = extract_cpp(FIXTURES / "sample.cpp") labels = _labels(r) # C++ extractor captures the constructor and public-visible methods assert any("HttpClient" in l for l in labels) def test_cpp_finds_includes(): r = extract_cpp(FIXTURES / "sample.cpp") assert "imports" in _relations(r) def test_cpp_import_edges_have_import_context(): r = extract_cpp(FIXTURES / "sample.cpp") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_cpp_class_inherits_edge(): """Regression for #915: `class Derived : public Base {}` should emit an inherits edge.""" r = extract_cpp(FIXTURES / "sample.cpp") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "AuthedHttpClient" in node_by_id.get(e["source"], "") and "HttpClient" in node_by_id.get(e["target"], "") for e in r["edges"] if e["relation"] == "inherits" ) assert found, "AuthedHttpClient should have inherits edge to HttpClient" def test_cpp_struct_inherits_edge(): """Structs use the same `: Base` syntax as classes and must also emit inherits.""" r = extract_cpp(FIXTURES / "sample.cpp") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "RetryingHttpClient" in node_by_id.get(e["source"], "") and "HttpClient" in node_by_id.get(e["target"], "") for e in r["edges"] if e["relation"] == "inherits" ) assert found, "RetryingHttpClient (struct) should have inherits edge to HttpClient" # ── Ruby ───────────────────────────────────────────────────────────────────── def test_ruby_no_error(): r = extract_ruby(FIXTURES / "sample.rb") assert "error" not in r def test_ruby_finds_class(): r = extract_ruby(FIXTURES / "sample.rb") assert any("ApiClient" in l for l in _labels(r)) def test_ruby_finds_methods(): r = extract_ruby(FIXTURES / "sample.rb") labels = _labels(r) assert any("get" in l for l in labels) assert any("post" in l for l in labels) def test_ruby_finds_function(): r = extract_ruby(FIXTURES / "sample.rb") assert any("parse_response" in l for l in _labels(r)) # ── C# ─────────────────────────────────────────────────────────────────────── def test_csharp_no_error(): r = extract_csharp(FIXTURES / "sample.cs") assert "error" not in r def test_csharp_finds_class(): r = extract_csharp(FIXTURES / "sample.cs") assert any("DataProcessor" in l for l in _labels(r)) def test_csharp_finds_interface(): r = extract_csharp(FIXTURES / "sample.cs") assert any("IProcessor" in l for l in _labels(r)) def test_csharp_finds_methods(): r = extract_csharp(FIXTURES / "sample.cs") labels = _labels(r) assert any("Process" in l for l in labels) def test_csharp_finds_usings(): r = extract_csharp(FIXTURES / "sample.cs") assert "imports" in _relations(r) def test_csharp_inherits_edge(): r = extract_csharp(FIXTURES / "sample.cs") inherits = [e for e in r["edges"] if e["relation"] == "inherits"] assert len(inherits) >= 1 def test_csharp_implements_iprocessor(): r = extract_csharp(FIXTURES / "sample.cs") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "DataProcessor" in node_by_id.get(e["source"], "") and "IProcessor" in node_by_id.get(e["target"], "") for e in r["edges"] if e["relation"] == "implements" ) assert found, "DataProcessor should have implements edge to IProcessor" def test_csharp_splits_inherits_and_implements_edges(): result = extract_csharp(FIXTURES / "sample.cs") assert ("DataProcessor", "Processor") in _edge_labels(result, "inherits") assert ("DataProcessor", "IProcessor") in _edge_labels(result, "implements") def test_csharp_parameter_return_and_generic_contexts(): result = extract_csharp(FIXTURES / "sample.cs") assert ("Build", "HttpClient") in _edge_labels(result, "references", "parameter_type") assert ("Build", "Result") in _edge_labels(result, "references", "return_type") assert ("Build", "DataProcessor") in _edge_labels(result, "references", "generic_arg") def test_java_normalizes_inherits_and_implements(): result = extract_java(FIXTURES / "sample.java") assert ("DataProcessor", "BaseProcessor") in _edge_labels(result, "inherits") assert ("DataProcessor", "Processor") in _edge_labels(result, "implements") def test_java_parameter_return_generic_and_attribute_contexts(): result = extract_java(FIXTURES / "sample.java") assert ("build", "HttpClient") in _edge_labels(result, "references", "parameter_type") assert ("build", "Result") in _edge_labels(result, "references", "return_type") assert ("build", "DataProcessor") in _edge_labels(result, "references", "generic_arg") assert ("build", "Override") in _edge_labels(result, "references", "attribute") def test_csharp_field_type_references_have_field_context(): r = extract_csharp(FIXTURES / "sample.cs") refs = _references(r) assert any( "DataProcessor" in src and "HttpClient" in tgt and edge.get("context") == "field" for src, tgt, edge in refs ), "DataProcessor field declarations should reference HttpClient with field context" def test_csharp_call_edges_have_call_context(): r = extract_csharp(FIXTURES / "sample.cs") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} assert any( "Process" in node_by_id.get(e["source"], "") and "Validate" in node_by_id.get(e["target"], "") and e.get("context") == "call" for e in r["edges"] if e["relation"] == "calls" ), "C# call edges should retain call context" def test_csharp_import_edges_have_import_context(): r = extract_csharp(FIXTURES / "sample.cs") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert import_edges assert all(e.get("context") == "import" for e in import_edges) # ── Kotlin ─────────────────────────────────────────────────────────────────── def test_kotlin_no_error(): r = extract_kotlin(FIXTURES / "sample.kt") assert "error" not in r def test_kotlin_finds_class(): r = extract_kotlin(FIXTURES / "sample.kt") assert any("HttpClient" in l for l in _labels(r)) def test_kotlin_finds_data_class(): r = extract_kotlin(FIXTURES / "sample.kt") assert any("Config" in l for l in _labels(r)) def test_kotlin_finds_methods(): r = extract_kotlin(FIXTURES / "sample.kt") labels = _labels(r) assert any("get" in l for l in labels) assert any("post" in l for l in labels) def test_kotlin_finds_function(): r = extract_kotlin(FIXTURES / "sample.kt") assert any("createClient" in l for l in _labels(r)) def test_kotlin_emits_in_file_calls(): """Regression test for the call-walker `simple_identifier` / `identifier` rename — see graphify-kmp's PythonParityTest.""" r = extract_kotlin(FIXTURES / "sample.kt") calls = _calls(r) # In sample.kt: get() and post() both call buildRequest(), and # createClient() invokes Config and HttpClient (constructor calls). assert (".get()", ".buildRequest()") in calls assert (".post()", ".buildRequest()") in calls assert ("createClient()", "Config") in calls assert ("createClient()", "HttpClient") in calls # ── Scala ───────────────────────────────────────────────────────────────────── def test_scala_no_error(): r = extract_scala(FIXTURES / "sample.scala") assert "error" not in r def test_scala_finds_class(): r = extract_scala(FIXTURES / "sample.scala") assert any("HttpClient" in l for l in _labels(r)) def test_scala_finds_object(): r = extract_scala(FIXTURES / "sample.scala") assert any("HttpClientFactory" in l for l in _labels(r)) def test_scala_finds_methods(): r = extract_scala(FIXTURES / "sample.scala") labels = _labels(r) assert any("get" in l for l in labels) assert any("post" in l for l in labels) def test_scala_import_edges_have_import_context(): r = extract_scala(FIXTURES / "sample.scala") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_scala_call_edges_have_call_context(): r = extract_scala(FIXTURES / "sample.scala") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) # ── PHP ─────────────────────────────────────────────────────────────────────── def test_php_no_error(): r = extract_php(FIXTURES / "sample.php") assert "error" not in r def test_php_finds_class(): r = extract_php(FIXTURES / "sample.php") assert any("ApiClient" in l for l in _labels(r)) def test_php_finds_methods(): r = extract_php(FIXTURES / "sample.php") labels = _labels(r) assert any("get" in l for l in labels) assert any("post" in l for l in labels) def test_php_finds_function(): r = extract_php(FIXTURES / "sample.php") assert any("parseResponse" in l for l in _labels(r)) def test_php_finds_imports(): r = extract_php(FIXTURES / "sample.php") assert "imports" in _relations(r) def test_php_import_edges_have_import_context(): r = extract_php(FIXTURES / "sample.php") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_php_call_edges_have_call_context(): r = extract_php(FIXTURES / "sample.php") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) def test_php_finds_static_property_access(): r = extract_php(FIXTURES / "sample_php_static_prop.php") assert "uses_static_prop" in _relations(r) def test_php_static_prop_target_is_holding_class(): r = extract_php(FIXTURES / "sample_php_static_prop.php") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} uses_prop = [ (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in r["edges"] if e["relation"] == "uses_static_prop" ] assert any("DefaultPalette" in tgt for _, tgt in uses_prop) def test_php_finds_config_helper_call(): r = extract_php(FIXTURES / "sample_php_config.php") assert "uses_config" in _relations(r) def test_php_config_helper_target_matches_first_segment(): r = extract_php(FIXTURES / "sample_php_config.php") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} uses_cfg = [ (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in r["edges"] if e["relation"] == "uses_config" ] assert any("Throttle" in tgt for _, tgt in uses_cfg) def test_php_finds_container_bind(): r = extract_php(FIXTURES / "sample_php_container.php") assert "bound_to" in _relations(r) def test_php_container_bind_links_contract_to_implementation(): r = extract_php(FIXTURES / "sample_php_container.php") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} bound = [ (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in r["edges"] if e["relation"] == "bound_to" ] assert any("PaymentGateway" in src and "StripeGateway" in tgt for src, tgt in bound) def test_php_finds_event_listeners(): r = extract_php(FIXTURES / "sample_php_listen.php") assert "listened_by" in _relations(r) def test_php_event_listener_links_event_to_listener(): r = extract_php(FIXTURES / "sample_php_listen.php") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} listened = [ (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in r["edges"] if e["relation"] == "listened_by" ] assert any("UserRegistered" in src and "SendWelcomeEmail" in tgt for src, tgt in listened) # ── Swift ──────────────────────────────────────────────────────────────────── def test_swift_no_error(): r = extract_swift(FIXTURES / "sample.swift") assert "error" not in r def test_swift_finds_class(): r = extract_swift(FIXTURES / "sample.swift") assert any("DataProcessor" in l for l in _labels(r)) def test_swift_finds_protocol(): r = extract_swift(FIXTURES / "sample.swift") assert any("Processor" in l for l in _labels(r)) def test_swift_finds_struct(): r = extract_swift(FIXTURES / "sample.swift") assert any("Config" in l for l in _labels(r)) def test_swift_finds_methods(): r = extract_swift(FIXTURES / "sample.swift") labels = _labels(r) assert any("addItem" in l for l in labels) assert any("process" in l for l in labels) def test_swift_finds_function(): r = extract_swift(FIXTURES / "sample.swift") assert any("createProcessor" in l for l in _labels(r)) def test_swift_finds_imports(): r = extract_swift(FIXTURES / "sample.swift") assert "imports" in _relations(r) def test_swift_import_edges_have_import_context(): r = extract_swift(FIXTURES / "sample.swift") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_swift_no_dangling_edges(): r = extract_swift(FIXTURES / "sample.swift") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids def test_swift_finds_actor(): r = extract_swift(FIXTURES / "sample.swift") assert any("CacheManager" in l for l in _labels(r)) def test_swift_finds_enum(): r = extract_swift(FIXTURES / "sample.swift") assert any("NetworkError" in l for l in _labels(r)) def test_swift_finds_enum_methods(): r = extract_swift(FIXTURES / "sample.swift") assert any("describe" in l for l in _labels(r)) def test_swift_finds_enum_cases(): r = extract_swift(FIXTURES / "sample.swift") labels = _labels(r) assert any("timeout" in l for l in labels) assert any("connectionFailed" in l for l in labels) def test_swift_enum_cases_have_case_of_edge(): r = extract_swift(FIXTURES / "sample.swift") case_edges = [e for e in r["edges"] if e["relation"] == "case_of"] assert len(case_edges) >= 2 def test_swift_finds_deinit(): r = extract_swift(FIXTURES / "sample.swift") assert any("deinit" in l for l in _labels(r)) def test_swift_finds_subscript(): r = extract_swift(FIXTURES / "sample.swift") assert any("subscript" in l for l in _labels(r)) def test_swift_extension_methods_attach_to_type(): r = extract_swift(FIXTURES / "sample.swift") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} method_edges = [e for e in r["edges"] if e["relation"] == "method"] found = False for e in method_edges: src_label = node_by_id.get(e["source"], "") tgt_label = node_by_id.get(e["target"], "") if "Config" in src_label and "isValid" in tgt_label: found = True break assert found, "extension method isValid should attach to Config" def test_swift_extension_does_not_duplicate_type_node(): r = extract_swift(FIXTURES / "sample.swift") config_nodes = [n for n in r["nodes"] if n["label"] == "Config"] assert len(config_nodes) == 1, f"Config should appear once, got {len(config_nodes)}" def test_swift_conformance_edge(): r = extract_swift(FIXTURES / "sample.swift") inherits_edges = [e for e in r["edges"] if e["relation"] == "inherits"] node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = False for e in inherits_edges: src_label = node_by_id.get(e["source"], "") tgt_label = node_by_id.get(e["target"], "") if "DataProcessor" in src_label and "Processor" in tgt_label: found = True break assert found, "DataProcessor should have inherits edge to Processor" def test_swift_extension_conformance_edge(): r = extract_swift(FIXTURES / "sample.swift") inherits_edges = [e for e in r["edges"] if e["relation"] == "inherits"] node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = False for e in inherits_edges: src_label = node_by_id.get(e["source"], "") tgt_label = node_by_id.get(e["target"], "") if "DataProcessor" in src_label and "Loggable" in tgt_label: found = True break assert found, "extension should add conformance edge DataProcessor -> Loggable" def test_swift_emits_calls(): r = extract_swift(FIXTURES / "sample.swift") calls = _calls(r) assert any("process" in src and "validate" in tgt for src, tgt in calls) def test_swift_call_edges_have_call_context(): r = extract_swift(FIXTURES / "sample.swift") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) def test_swift_extension_across_files_merges_into_canonical_type(): """`extension Foo` in a separate file from `class Foo` must resolve to a single Foo node. tree-sitter-swift parses both as `class_declaration` and node ids carry the file stem, so without a corpus-level merge each file would emit its own Foo.""" from graphify.extract import extract paths = sorted((FIXTURES / "swift_cross_file").glob("*.swift")) r = extract(paths, cache_root=Path("/tmp/graphify-test-no-cache")) foo_nodes = [n for n in r["nodes"] if n["label"] == "Foo"] assert len(foo_nodes) == 1, f"Foo should appear once, got {len(foo_nodes)}: {[n['id'] for n in foo_nodes]}" foo_id = foo_nodes[0]["id"] method_targets = { e["target"] for e in r["edges"] if e["relation"] == "method" and e["source"] == foo_id } method_labels = {n["label"] for n in r["nodes"] if n["id"] in method_targets} assert any("one" in l for l in method_labels), f"one() should attach to Foo, got {method_labels}" assert any("two" in l for l in method_labels), f"extension method two() should attach to Foo, got {method_labels}" # ── Elixir ──────────────────────────────────────────────────────────────────── from graphify.extract import extract_elixir def test_elixir_finds_module(): r = extract_elixir(FIXTURES / "sample.ex") assert "error" not in r labels = [n["label"] for n in r["nodes"]] assert any("MyApp.Accounts.User" in l for l in labels) def test_elixir_finds_functions(): r = extract_elixir(FIXTURES / "sample.ex") labels = [n["label"] for n in r["nodes"]] assert any("create" in l for l in labels) assert any("find" in l for l in labels) assert any("validate" in l for l in labels) def test_elixir_finds_imports(): r = extract_elixir(FIXTURES / "sample.ex") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert len(import_edges) >= 2 def test_elixir_import_edges_have_import_context(): r = extract_elixir(FIXTURES / "sample.ex") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_elixir_finds_calls(): r = extract_elixir(FIXTURES / "sample.ex") calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"} labels = {n["id"]: n["label"] for n in r["nodes"]} assert any("create" in labels.get(src, "") and "validate" in labels.get(tgt, "") for src, tgt in calls) def test_elixir_call_edges_have_call_context(): r = extract_elixir(FIXTURES / "sample.ex") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) def test_elixir_method_edges(): r = extract_elixir(FIXTURES / "sample.ex") methods = [e for e in r["edges"] if e["relation"] == "method"] assert len(methods) >= 3 # ── Objective-C ────────────────────────────────────────────────────────────── from graphify.extract import extract_objc def test_objc_finds_interface(): r = extract_objc(FIXTURES / "sample.m") labels = [n["label"] for n in r["nodes"]] assert "Animal" in labels def test_objc_finds_subclass(): r = extract_objc(FIXTURES / "sample.m") labels = [n["label"] for n in r["nodes"]] assert "Dog" in labels def test_objc_finds_methods(): r = extract_objc(FIXTURES / "sample.m") labels = [n["label"] for n in r["nodes"]] assert any("speak" in l or "fetch" in l or "initWithName" in l for l in labels) def test_objc_finds_imports(): r = extract_objc(FIXTURES / "sample.m") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert len(import_edges) >= 1 def test_objc_import_edges_have_import_context(): r = extract_objc(FIXTURES / "sample.m") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_objc_inherits_edge(): r = extract_objc(FIXTURES / "sample.m") inherits = [e for e in r["edges"] if e["relation"] == "inherits"] assert len(inherits) >= 1 def test_objc_no_dangling_edges(): r = extract_objc(FIXTURES / "sample.m") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"Dangling source: {e}" # --------------------------------------------------------------------------- # Go # --------------------------------------------------------------------------- def test_go_receiver_methods_share_type_node(): """Methods on the same receiver type must share one canonical type node.""" r = extract_go(FIXTURES / "sample.go") server_nodes = [n for n in r["nodes"] if n["label"] == "Server"] # Both Start() and Stop() are on *Server — should produce exactly one Server node assert len(server_nodes) == 1 def test_go_receiver_uses_pkg_scope(): """Type node id should be scoped to directory, not file stem.""" r = extract_go(FIXTURES / "sample.go") server_nodes = [n for n in r["nodes"] if n["label"] == "Server"] assert server_nodes # Should NOT contain the file stem "sample" in the type node id assert "sample" not in server_nodes[0]["id"].split(":")[0] # --------------------------------------------------------------------------- # Julia # --------------------------------------------------------------------------- def test_julia_finds_module(): r = extract_julia(FIXTURES / "sample.jl") labels = [n["label"] for n in r["nodes"]] assert "Geometry" in labels def test_julia_finds_structs(): r = extract_julia(FIXTURES / "sample.jl") labels = [n["label"] for n in r["nodes"]] assert "Point" in labels assert "Circle" in labels def test_julia_finds_abstract_type(): r = extract_julia(FIXTURES / "sample.jl") labels = [n["label"] for n in r["nodes"]] assert "Shape" in labels def test_julia_finds_functions(): r = extract_julia(FIXTURES / "sample.jl") labels = [n["label"] for n in r["nodes"]] assert any("area" in l for l in labels) assert any("distance" in l for l in labels) def test_julia_finds_short_function(): r = extract_julia(FIXTURES / "sample.jl") labels = [n["label"] for n in r["nodes"]] assert any("perimeter" in l for l in labels) def test_julia_finds_imports(): r = extract_julia(FIXTURES / "sample.jl") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert len(import_edges) >= 1 def test_julia_import_edges_have_import_context(): r = extract_julia(FIXTURES / "sample.jl") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_julia_finds_inherits(): r = extract_julia(FIXTURES / "sample.jl") inherits = [e for e in r["edges"] if e["relation"] == "inherits"] assert len(inherits) >= 1 def test_julia_finds_calls(): r = extract_julia(FIXTURES / "sample.jl") call_edges = [e for e in r["edges"] if e["relation"] == "calls"] assert len(call_edges) >= 1 def test_julia_call_edges_have_call_context(): r = extract_julia(FIXTURES / "sample.jl") call_edges = _edges_with_relation(r, "calls") assert call_edges assert all(e.get("context") == "call" for e in call_edges) def test_julia_no_dangling_edges(): r = extract_julia(FIXTURES / "sample.jl") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"Dangling source: {e}" # ── Fortran extractor ──────────────────────────────────────────────────────── def test_fortran_finds_module(): r = extract_fortran(FIXTURES / "sample.f90") assert "error" not in r labels = [n["label"] for n in r["nodes"]] assert "geometry" in labels def test_fortran_finds_subroutines(): r = extract_fortran(FIXTURES / "sample.f90") labels = [n["label"] for n in r["nodes"]] assert any("circle_area" in l for l in labels) assert any("print_area" in l for l in labels) def test_fortran_finds_function(): r = extract_fortran(FIXTURES / "sample.f90") labels = [n["label"] for n in r["nodes"]] assert any("distance" in l for l in labels) def test_fortran_finds_program(): r = extract_fortran(FIXTURES / "sample.f90") labels = [n["label"] for n in r["nodes"]] assert "main" in labels def test_fortran_finds_use_imports(): r = extract_fortran(FIXTURES / "sample.f90") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert len(import_edges) >= 2 def test_fortran_use_edges_have_use_context(): r = extract_fortran(FIXTURES / "sample.f90") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert all(e.get("context") == "use" for e in import_edges) def test_fortran_finds_calls(): r = extract_fortran(FIXTURES / "sample.f90") call_edges = [e for e in r["edges"] if e["relation"] == "calls"] assert len(call_edges) >= 1 def test_fortran_case_insensitive_names(): r = extract_fortran(FIXTURES / "sample.f90") labels = [n["label"] for n in r["nodes"]] assert all(l == l.lower() or "(" in l for l in labels if l.endswith(("()", "")) and not "." in l) assert "geometry" in labels assert "main" in labels def test_fortran_no_dangling_edges(): r = extract_fortran(FIXTURES / "sample.f90") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"Dangling source: {e}" def test_fortran_capital_F_parses_preprocessed(): r = extract_fortran(FIXTURES / "sample_preprocessed.F90") assert "error" not in r labels = [n["label"] for n in r["nodes"]] assert "shapes" in labels assert any("compute_volume" in l for l in labels) # ── TypeScript dynamic imports ─────────────────────────────────────────────── def test_ts_dynamic_import_no_error(): r = extract_js(FIXTURES / "dynamic_import.ts") assert "error" not in r def test_ts_dynamic_import_extracts_edges(): """Dynamic import() calls inside functions should produce imports_from edges.""" r = extract_js(FIXTURES / "dynamic_import.ts") dyn_edges = [e for e in r["edges"] if e["relation"] == "imports_from"] targets = {e["target"] for e in dyn_edges} # Should find: static ./logger, dynamic ./mayaEngine.js, dynamic ./queue.js assert any("logger" in t for t in targets), f"Missing static import of logger: {targets}" assert any("mayaengine" in t.lower() for t in targets), f"Missing dynamic import of mayaEngine: {targets}" assert any("queue" in t.lower() for t in targets), f"Missing dynamic import of queue: {targets}" def test_ts_dynamic_import_confidence(): """Dynamic imports should have EXTRACTED confidence (they are deterministic string literals).""" r = extract_js(FIXTURES / "dynamic_import.ts") dyn_edges = [e for e in r["edges"] if e["relation"] == "imports_from" and "mayaengine" in e["target"].lower()] assert len(dyn_edges) >= 1 assert dyn_edges[0]["confidence"] == "EXTRACTED" def test_ts_dynamic_import_source_is_function(): """Dynamic import edge source should be the enclosing function, not the file.""" r = extract_js(FIXTURES / "dynamic_import.ts") node_labels = {n["id"]: n["label"] for n in r["nodes"]} dyn_edges = [e for e in r["edges"] if e["relation"] == "imports_from" and "mayaengine" in e["target"].lower()] assert len(dyn_edges) >= 1 src_label = node_labels.get(dyn_edges[0]["source"], "") assert "processInbound" in src_label, f"Expected processInbound as source, got {src_label}" def test_ts_no_dynamic_import_in_sync_fn(): """Functions without dynamic imports should not get spurious imports_from edges.""" r = extract_js(FIXTURES / "dynamic_import.ts") node_ids = {n["label"]: n["id"] for n in r["nodes"]} sync_nid = node_ids.get("syncOnly()") if sync_nid: sync_imports = [e for e in r["edges"] if e["source"] == sync_nid and e["relation"] == "imports_from"] assert len(sync_imports) == 0 def test_ts_dynamic_template_literal_skipped(): """Dynamic template literals (with ${}) must not produce an imports_from edge.""" r = extract_js(FIXTURES / "dynamic_import.ts") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} # loadHandler uses `./handlers/${handlerName}` — no static path, must be absent assert not any("handler" in t.lower() and "$" in t for t in targets), \ f"Garbage edge from dynamic template literal found: {targets}" # More robust: no target should contain a brace character assert not any("{" in t or "}" in t for t in targets), \ f"Target contains unresolved template expression: {targets}" def test_ts_static_template_literal_resolved(): """Static template literals (no ${}) should resolve the same as a plain string.""" r = extract_js(FIXTURES / "dynamic_import.ts") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("statichelper" in t.lower() for t in targets), \ f"Static template literal import not resolved: {targets}" # ── Markdown ───────────────────────────────────────────────────────────────── from graphify.extract import extract_markdown def test_markdown_no_error(): r = extract_markdown(FIXTURES / "deploy_guide.md") assert "error" not in r def test_markdown_finds_headings(): r = extract_markdown(FIXTURES / "deploy_guide.md") labels = _labels(r) assert any("Deploy Guide" in l for l in labels) assert any("Prerequisites" in l for l in labels) assert any("Full Deploy" in l for l in labels) assert any("Rollback" in l for l in labels) def test_markdown_finds_nested_heading(): """### Database Migration is nested under ## Full Deploy.""" r = extract_markdown(FIXTURES / "deploy_guide.md") labels = _labels(r) assert any("Database Migration" in l for l in labels) def test_markdown_finds_code_blocks(): r = extract_markdown(FIXTURES / "deploy_guide.md") labels = _labels(r) assert any("code:bash" in l for l in labels) assert any("code:sql" in l for l in labels) assert any("code:python" in l for l in labels) def test_markdown_contains_edges(): """Headings and code blocks should be connected via 'contains' edges.""" r = extract_markdown(FIXTURES / "deploy_guide.md") assert "contains" in _relations(r) contains_edges = [e for e in r["edges"] if e["relation"] == "contains"] assert len(contains_edges) >= 5 # file->h1, h1->h2s, h2->h3, h2->codeblocks def test_markdown_no_dangling_edges(): r = extract_markdown(FIXTURES / "deploy_guide.md") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"Dangling source: {e}" # ── Groovy ─────────────────────────────────────────────────────────────────── def test_groovy_no_error(): r = extract_groovy(FIXTURES / "sample.groovy") assert "error" not in r def test_groovy_finds_class(): r = extract_groovy(FIXTURES / "sample.groovy") assert any("SampleService" in l for l in _labels(r)) def test_groovy_finds_methods(): r = extract_groovy(FIXTURES / "sample.groovy") labels = _labels(r) assert any("process" in l for l in labels) assert any("reset" in l for l in labels) def test_groovy_finds_imports(): r = extract_groovy(FIXTURES / "sample.groovy") assert "imports" in _relations(r) def test_groovy_import_edges_have_import_context(): r = extract_groovy(FIXTURES / "sample.groovy") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_groovy_no_dangling_edges(): r = extract_groovy(FIXTURES / "sample.groovy") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids def test_groovy_spock_finds_class(): r = extract_groovy(FIXTURES / "sample_spock.groovy") assert any("SampleSpec" in l for l in _labels(r)) def test_groovy_spock_finds_feature_methods(): r = extract_groovy(FIXTURES / "sample_spock.groovy") feature_labels = [l for l in _labels(r) if l.startswith('"')] assert len(feature_labels) >= 2 def test_groovy_spock_finds_method_with_apostrophe(): r = extract_groovy(FIXTURES / "sample_spock.groovy") assert any("it's" in l for l in _labels(r)) def test_groovy_spock_preserves_import_edges(): r = extract_groovy(FIXTURES / "sample_spock.groovy") assert "imports" in _relations(r) def test_groovy_spock_no_dangling_edges(): r = extract_groovy(FIXTURES / "sample_spock.groovy") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids # ── DM (BYOND DreamMaker) ──────────────────────────────────────────────────── def test_dm_no_error(): r = extract_dm(FIXTURES / "sample.dm") assert "error" not in r def test_dm_finds_global_proc(): r = extract_dm(FIXTURES / "sample.dm") labels = _labels(r) assert any(l == "log_event()" for l in labels) assert any(l == "RunTest()" for l in labels) def test_dm_finds_type_definition(): r = extract_dm(FIXTURES / "sample.dm") labels = _labels(r) assert "/datum/weapon" in labels assert "/datum/weapon/sword" in labels def test_dm_qualifies_proc_with_type_path(): r = extract_dm(FIXTURES / "sample.dm") labels = _labels(r) assert "/datum/weapon/attack()" in labels assert "/datum/weapon/sword/attack()" in labels def test_dm_finds_path_form_proc_definition(): r = extract_dm(FIXTURES / "sample.dm") assert "/datum/weapon/sword/sharpen()" in _labels(r) def test_dm_emits_include_edge(): r = extract_dm(FIXTURES / "sample.dm") import_edges = _edges_with_relation(r, "imports", "imports_from") assert import_edges assert all(e.get("context") == "import" for e in import_edges) def test_dm_unresolved_include_flagged_external(): r = extract_dm(FIXTURES / "sample.dm") import_edges = _edges_with_relation(r, "imports", "imports_from") helpers = [e for e in import_edges if "helpers" in e["target"]] assert helpers assert all(e.get("external") is True for e in helpers) def test_dm_resolves_in_file_calls(): r = extract_dm(FIXTURES / "sample.dm") calls = _calls(r) assert any(callee == "log_event()" for _, callee in calls) assert ("/datum/weapon/sword/attack()", "/datum/weapon/sword/sharpen()") in calls def test_dm_ambiguous_member_call_left_unresolved(): r = extract_dm(FIXTURES / "sample.dm") calls = _calls(r) runtest_to_attack = [c for s, c in calls if s == "RunTest()" and "attack" in c] assert not runtest_to_attack assert any(rc["callee"] == "attack" for rc in r.get("raw_calls", [])) def test_dm_emits_new_as_instantiates(): r = extract_dm(FIXTURES / "sample.dm") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} inst = [(node_by_id.get(e["source"]), node_by_id.get(e["target"])) for e in r["edges"] if e["relation"] == "instantiates"] assert ("RunTest()", "/datum/weapon/sword") in inst def test_dm_call_edges_have_call_context(): r = extract_dm(FIXTURES / "sample.dm") call_edges = _edges_with_relation(r, "calls", "instantiates") assert call_edges assert all(e.get("context") == "call" for e in call_edges) def test_dm_no_dangling_edges(): r = extract_dm(FIXTURES / "sample.dm") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids def test_dm_super_call_not_emitted(): r = extract_dm(FIXTURES / "sample.dm") calls = _calls(r) assert not any(callee.strip("()") == ".." for _, callee in calls) assert not any(rc["callee"] == ".." for rc in r.get("raw_calls", [])) # ── DMI (BYOND icon sheets) ────────────────────────────────────────────────── def test_dmi_no_error(): r = extract_dmi(FIXTURES / "sample.dmi") assert "error" not in r def test_dmi_emits_state_nodes(): r = extract_dmi(FIXTURES / "sample.dmi") labels = _labels(r) assert any(l == '"mob"' for l in labels) def test_dmi_state_contained_by_file(): r = extract_dmi(FIXTURES / "sample.dmi") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} contains = [(node_by_id.get(e["source"]), node_by_id.get(e["target"])) for e in r["edges"] if e["relation"] == "contains"] assert ("sample.dmi", '"mob"') in contains # ── DMM (BYOND map files) ──────────────────────────────────────────────────── def test_dmm_no_error(): r = extract_dmm(FIXTURES / "sample.dmm") assert "error" not in r def test_dmm_extracts_type_paths_as_uses_edges(): r = extract_dmm(FIXTURES / "sample.dmm") targets = {e["target"] for e in r["edges"] if e["relation"] == "uses"} assert "turf_closed_wall" in targets assert "obj_structure_table" in targets assert "obj_item_weapon_sword" in targets def test_dmm_strips_var_overrides(): r = extract_dmm(FIXTURES / "sample.dmm") targets = {e["target"] for e in r["edges"] if e["relation"] == "uses"} assert not any("{" in t for t in targets) assert "obj_item_weapon_sword" in targets def test_dmm_handles_multiline_tile_definition(): r = extract_dmm(FIXTURES / "sample.dmm") targets = {e["target"] for e in r["edges"] if e["relation"] == "uses"} assert "area_station_maintenance" in targets def test_dmm_skips_grid_section(): r = extract_dmm(FIXTURES / "sample.dmm") targets = {e["target"] for e in r["edges"] if e["relation"] == "uses"} assert len(targets) == 5 # ── DMF (BYOND interface forms) ────────────────────────────────────────────── def test_dmf_no_error(): r = extract_dmf(FIXTURES / "sample.dmf") assert "error" not in r def test_dmf_extracts_windows(): r = extract_dmf(FIXTURES / "sample.dmf") labels = _labels(r) assert 'window "mapwindow"' in labels assert 'window "infowindow"' in labels def test_dmf_elem_labels_carry_control_type(): r = extract_dmf(FIXTURES / "sample.dmf") labels = _labels(r) assert 'elem "map" [MAP]' in labels def test_dmf_elem_under_window(): r = extract_dmf(FIXTURES / "sample.dmf") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} contains = [(node_by_id.get(e["source"]), node_by_id.get(e["target"])) for e in r["edges"] if e["relation"] == "contains"] assert ('window "mapwindow"', 'elem "map" [MAP]') in contains def test_dmf_no_dangling_edges(): r = extract_dmf(FIXTURES / "sample.dmf") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids assert e["target"] in node_ids # -- .NET project files (.sln, .csproj, .razor) ------------------------------- def test_sln_no_error(): r = extract_sln(FIXTURES / "sample.sln") assert "error" not in r def test_sln_finds_projects(): r = extract_sln(FIXTURES / "sample.sln") labels = _labels(r) assert any("WebApi" in l for l in labels) assert any("Domain" in l for l in labels) def test_sln_contains_edges(): r = extract_sln(FIXTURES / "sample.sln") assert "contains" in _relations(r) def test_sln_project_dependency_edges(): r = extract_sln(FIXTURES / "sample.sln") assert "imports" in _relations(r) def test_csproj_no_error(): r = extract_csproj(FIXTURES / "sample.csproj") assert "error" not in r def test_csproj_finds_packages(): r = extract_csproj(FIXTURES / "sample.csproj") labels = _labels(r) assert any("MediatR" in l for l in labels) assert any("FluentValidation" in l for l in labels) def test_csproj_finds_project_references(): r = extract_csproj(FIXTURES / "sample.csproj") labels = _labels(r) assert any("Domain.csproj" in l for l in labels) def test_csproj_finds_target_framework(): r = extract_csproj(FIXTURES / "sample.csproj") assert any("net8.0" in l for l in _labels(r)) def test_csproj_finds_sdk(): r = extract_csproj(FIXTURES / "sample.csproj") assert any("Microsoft.NET.Sdk.Web" in l for l in _labels(r)) def test_razor_no_error(): r = extract_razor(FIXTURES / "sample.razor") assert "error" not in r def test_razor_finds_using_directives(): r = extract_razor(FIXTURES / "sample.razor") assert "imports" in _relations(r) def test_razor_finds_component_references(): r = extract_razor(FIXTURES / "sample.razor") assert "calls" in _relations(r) def test_razor_finds_inherits(): r = extract_razor(FIXTURES / "sample.razor") assert "inherits" in _relations(r) def test_razor_finds_code_block_methods(): r = extract_razor(FIXTURES / "sample.razor") labels = _labels(r) assert any("IncrementCount" in l for l in labels) assert any("LoadData" in l for l in labels) def test_razor_no_dangling_edges(): r = extract_razor(FIXTURES / "sample.razor") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids