"""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, extract_powershell, extract_apex, ) FIXTURES = Path(__file__).parent / "fixtures" # tree-sitter-dm is an optional extra (#1104) - it ships no Linux/Mac wheel, so it # is not installed by a default `uv sync`. Skip the .dm/.dme grammar tests when the # grammar is absent (.dmi/.dmm/.dmf use no tree-sitter and are always tested). import importlib.util as _ilu _needs_dm = pytest.mark.skipif( _ilu.find_spec("tree_sitter_dm") is None, reason="tree-sitter-dm not installed (optional [dm] extra)", ) 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_parameter_and_return_type_contexts(): r = extract_c(FIXTURES / "sample.c") assert ("make_rect", "Rectangle") in _edge_labels(r, "references", "parameter_type") assert ("make_rect", "Rectangle") in _edge_labels(r, "references", "return_type") 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_method_parameter_and_return_type_contexts(): r = extract_cpp(FIXTURES / "sample.cpp") assert ("get", "string") in _edge_labels(r, "references", "parameter_type") assert ("get", "string") in _edge_labels(r, "references", "return_type") def test_cpp_field_and_template_argument_contexts(): r = extract_cpp(FIXTURES / "sample.cpp") assert ("HttpClient", "string") in _edge_labels(r, "references", "field") assert ("HttpClient", "vector") in _edge_labels(r, "references", "field") assert ("HttpClient", "string") in _edge_labels(r, "references", "generic_arg") 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 def test_kotlin_splits_inherits_and_implements(): r = extract_kotlin(FIXTURES / "sample.kt") assert ("DataProcessor", "BaseProcessor") in _edge_labels(r, "inherits") assert ("DataProcessor", "Loggable") in _edge_labels(r, "implements") def test_kotlin_parameter_return_generic_and_field_contexts(): r = extract_kotlin(FIXTURES / "sample.kt") assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type") assert ("run", "Result") in _edge_labels(r, "references", "return_type") assert ("run", "DataProcessor") in _edge_labels(r, "references", "generic_arg") assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") # ── 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_splits_inherits_and_mixes_in(): r = extract_scala(FIXTURES / "sample.scala") assert ("HttpClient", "BaseClient") in _edge_labels(r, "inherits") assert ("HttpClient", "Loggable") in _edge_labels(r, "mixes_in") def test_scala_constructor_parameter_field_context(): r = extract_scala(FIXTURES / "sample.scala") assert ("HttpClient", "Config") in _edge_labels(r, "references", "field") def test_scala_val_definition_field_context(): r = extract_scala(FIXTURES / "sample.scala") assert ("HttpClient", "Config") in _edge_labels(r, "references", "field") def test_scala_method_return_type_context(): r = extract_scala(FIXTURES / "sample.scala") assert ("create", "HttpClient") in _edge_labels(r, "references", "return_type") 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) def test_php_splits_inherits_implements_mixes_in(): r = extract_php(FIXTURES / "sample.php") assert ("DataProcessor", "BaseProcessor") in _edge_labels(r, "inherits") assert ("DataProcessor", "Loggable") in _edge_labels(r, "implements") assert ("DataProcessor", "HasName") in _edge_labels(r, "mixes_in") def test_php_property_parameter_and_return_contexts(): r = extract_php(FIXTURES / "sample.php") assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type") assert ("run", "Result") in _edge_labels(r, "references", "return_type") # ── 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_protocol_conformance_emits_implements(): r = extract_swift(FIXTURES / "sample.swift") assert ("DataProcessor", "Processor") in _edge_labels(r, "implements") def test_swift_extension_conformance_emits_implements(): r = extract_swift(FIXTURES / "sample.swift") assert ("DataProcessor", "Loggable") in _edge_labels(r, "implements") def test_swift_splits_inherits_and_implements(): r = extract_swift(FIXTURES / "sample.swift") assert ("DataProcessor", "BaseProcessor") in _edge_labels(r, "inherits") assert ("DataProcessor", "Processor") in _edge_labels(r, "implements") def test_swift_parameter_return_generic_and_field_contexts(): r = extract_swift(FIXTURES / "sample.swift") assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type") assert ("run", "Result") in _edge_labels(r, "references", "return_type") assert ("run", "DataProcessor") in _edge_labels(r, "references", "generic_arg") assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") 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_splits_inherits_and_implements(): r = extract_objc(FIXTURES / "sample.m") assert ("Animal", "NSObject") in _edge_labels(r, "inherits") assert ("Dog", "Animal") in _edge_labels(r, "inherits") assert ("Animal", "SampleDelegate") in _edge_labels(r, "implements") def test_objc_property_type_context(): r = extract_objc(FIXTURES / "sample.m") assert ("Animal", "NSString") in _edge_labels(r, "references", "field") 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_abstract_concrete_hierarchy_inherits(): r = extract_julia(FIXTURES / "sample.jl") assert ("Point", "Shape") in _edge_labels(r, "inherits") assert ("Circle", "Shape") in _edge_labels(r, "inherits") def test_julia_struct_field_type_context(): r = extract_julia(FIXTURES / "sample.jl") assert ("Point", "Float64") in _edge_labels(r, "references", "field") assert ("Circle", "Point") in _edge_labels(r, "references", "field") assert ("Circle", "Float64") in _edge_labels(r, "references", "field") 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_finds_derived_type(): r = extract_fortran(FIXTURES / "sample.f90") labels = [n["label"] for n in r["nodes"]] assert "point" in labels def test_fortran_parameter_and_return_type_contexts(): r = extract_fortran(FIXTURES / "sample.f90") assert ("translate", "point") in _edge_labels(r, "references", "parameter_type") assert ("origin", "point") in _edge_labels(r, "references", "return_type") 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) # ── PowerShell ─────────────────────────────────────────────────────────────── def test_powershell_no_error(): r = extract_powershell(FIXTURES / "sample.ps1") assert "error" not in r def test_powershell_finds_class_and_method(): r = extract_powershell(FIXTURES / "sample.ps1") labels = [n["label"] for n in r["nodes"]] assert "DataProcessor" in labels assert any("Transform" in l for l in labels) def test_powershell_property_field_type_context(): r = extract_powershell(FIXTURES / "sample.ps1") assert ("DataProcessor", "string") in _edge_labels(r, "references", "field") def test_powershell_method_parameter_and_return_type_contexts(): r = extract_powershell(FIXTURES / "sample.ps1") assert ("Transform", "string") in _edge_labels(r, "references", "parameter_type") assert ("Transform", "string") in _edge_labels(r, "references", "return_type") assert ("Save", "void") in _edge_labels(r, "references", "return_type") # ── 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}" def test_js_local_const_does_not_emit_phantom_node(tmp_path): """Local const/let/var inside an arrow callback must NOT emit a node (#1077). Previously `_js_extra_walk` recursed into arrow_function bodies and emitted a node for every `const x = ...` inside e.g. `describe(() => {})`, so bare names like `set`, `sorted` collided across unrelated test files. """ src = ( "describe('suite', () => {\n" " const inner = new Set([1, 2, 3]);\n" " let other = [1, 2];\n" "});\n" "\n" "const moduleConst = new Set([4, 5]);\n" "export const exportedConst = { a: 1 };\n" ) f = tmp_path / "scope_guard.js" f.write_text(src) r = extract_js(f) labels = _labels(r) # Locals inside the arrow callback must not produce nodes. assert "inner" not in labels, f"phantom node for arrow-body local 'inner': {labels}" assert "other" not in labels, f"phantom node for arrow-body local 'other': {labels}" # Module-level consts should still produce nodes. assert "moduleConst" in labels, f"module-level const 'moduleConst' missing: {labels}" assert "exportedConst" in labels, f"exported const 'exportedConst' missing: {labels}" def test_js_module_level_arrow_produces_node_and_call_edges(tmp_path): """Module-level arrow functions must still emit a node and capture their calls (#1077). The scope guard must not accidentally suppress top-level arrow functions. """ src = ( "function helper() { return 1; }\n" "const handler = () => {\n" " helper();\n" "};\n" ) f = tmp_path / "arrows.js" f.write_text(src) r = extract_js(f) labels = _labels(r) relations = _relations(r) assert any("handler" in l for l in labels), f"module-level arrow 'handler' missing: {labels}" assert "calls" in relations, f"expected 'calls' edge from handler->helper: {relations}" def test_ts_local_const_does_not_emit_phantom_node(tmp_path): """Scope guard applies to TypeScript files too (shared _js_extra_walk path).""" src = ( "describe('suite', () => {\n" " const inner: Set = new Set([1, 2]);\n" "});\n" "\n" "export const topLevel = { a: 1 };\n" ) f = tmp_path / "scope_guard.ts" f.write_text(src) r = extract_js(f) labels = _labels(r) assert "inner" not in labels, f"phantom TS node for arrow-body local 'inner': {labels}" assert "topLevel" in labels, f"module-level TS const 'topLevel' missing: {labels}" # ── 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_skips_fenced_code_blocks(): """Fenced code blocks should NOT emit nodes (#1077). They were always orphans (single contains edge to parent doc) and inflated the disconnected-component count. We still skip over their *contents* when parsing so the inside of a fence is not misread as a heading. """ r = extract_markdown(FIXTURES / "deploy_guide.md") labels = _labels(r) assert not any(l.startswith("code:") for l in labels), \ f"Expected no code:* nodes after #1077 fix, got: {[l for l in labels if l.startswith('code:')]}" def test_markdown_contains_edges(): """Headings should be connected via 'contains' edges (file->h, h->h).""" r = extract_markdown(FIXTURES / "deploy_guide.md") assert "contains" in _relations(r) contains_edges = [e for e in r["edges"] if e["relation"] == "contains"] # deploy_guide.md has: file->h1, h1->h2(Prerequisites), h1->h2(Full Deploy), # h2(Full Deploy)->h3(Database Migration), h1->h2(Rollback) = 5 edges assert len(contains_edges) >= 5, f"expected >= 5 contains edges, got {len(contains_edges)}" def test_markdown_fenced_heading_not_parsed(): """A '## heading' inside a fenced block must not produce a heading node (#1077). The fence-toggle skips over fenced contents so interior markdown syntax is not misread as document structure. """ import tempfile, os src = ( "# Real Heading\n" "\n" "```bash\n" "## Not A Heading\n" "echo hello\n" "```\n" "\n" "## Another Real Heading\n" ) with tempfile.NamedTemporaryFile(suffix=".md", mode="w", delete=False) as fh: fh.write(src) fpath = fh.name try: r = extract_markdown(Path(fpath)) labels = _labels(r) finally: os.unlink(fpath) assert any("Real Heading" in l for l in labels), f"'Real Heading' missing: {labels}" assert any("Another Real Heading" in l for l in labels), f"'Another Real Heading' missing: {labels}" assert not any("Not A Heading" in l for l in labels), \ f"fenced '## Not A Heading' was incorrectly parsed as a node: {labels}" 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) ──────────────────────────────────────────────────── @_needs_dm def test_dm_no_error(): r = extract_dm(FIXTURES / "sample.dm") assert "error" not in r @_needs_dm 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) @_needs_dm 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 @_needs_dm 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 @_needs_dm def test_dm_finds_path_form_proc_definition(): r = extract_dm(FIXTURES / "sample.dm") assert "/datum/weapon/sword/sharpen()" in _labels(r) @_needs_dm 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) @_needs_dm 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) @_needs_dm 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 @_needs_dm 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", [])) @_needs_dm 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 @_needs_dm 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) @_needs_dm 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 @_needs_dm 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 # ---------------Salesforce Apex (.cls / .trigger)---------------------- def test_apex_class_extraction(): r = extract_apex(FIXTURES / "sample.cls") labels = _labels(r) assert "AccountService" in labels def test_apex_enum_extraction(): r = extract_apex(FIXTURES / "sample.cls") labels = _labels(r) assert "AccountStatus" in labels def test_apex_interface_extraction(): r = extract_apex(FIXTURES / "sample.cls") labels = _labels(r) assert "Notifiable" in labels def test_apex_method_extraction(): r = extract_apex(FIXTURES / "sample.cls") labels = _labels(r) assert any("getAccounts" in l for l in labels) assert any("updateAccountsAsync" in l for l in labels) assert any("createAccounts" in l for l in labels) assert any("deleteOldAccounts" in l for l in labels) def test_apex_contains_and_method_relations(): r = extract_apex(FIXTURES / "sample.cls") relations = _relations(r) assert "contains" in relations assert "method" in relations def test_apex_soql_uses_edge(): r = extract_apex(FIXTURES / "sample.cls") relations = _relations(r) assert "uses" in relations labels = _labels(r) assert "Account" in labels def test_apex_dml_uses_edge(): r = extract_apex(FIXTURES / "sample.cls") dml_labels = {n["label"] for n in r["nodes"] if n["label"] in ("insert", "update", "delete", "upsert")} assert len(dml_labels) > 0 def test_apex_file_node_present(): r = extract_apex(FIXTURES / "sample.cls") labels = _labels(r) assert "sample.cls" in labels def test_apex_trigger_extraction(): r = extract_apex(FIXTURES / "sample.trigger") labels = _labels(r) assert "sample.trigger" in labels assert "AccountTrigger" in labels def test_apex_trigger_uses_sobject(): r = extract_apex(FIXTURES / "sample.trigger") relations = _relations(r) assert "uses" in relations labels = _labels(r) assert "Account" in labels def test_apex_missing_file_returns_empty(): r = extract_apex(Path("nonexistent.cls")) assert r["nodes"] == [] assert r["edges"] == [] def test_apex_no_dangling_edges(): for fixture in ("sample.cls", "sample.trigger"): r = extract_apex(FIXTURES / fixture) node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"dangling source in {fixture}: {e}" assert e["target"] in node_ids, f"dangling target in {fixture}: {e}"