"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia, Fortran, JS/TS, .NET project files, XAML.""" 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_xaml, extract_razor, extract_dm, extract_dmi, extract_dmm, extract_dmf, extract_powershell, extract_apex, extract_verilog, extract_powershell_manifest, ) 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 def test_java_enum_constants_have_case_of_edge(): r = extract_java(FIXTURES / "sample.java") labels = _labels(r) assert "OK" in labels assert "GAME_DONE" in labels assert ("ErrorCode", "OK") in _edge_labels(r, "case_of") assert ("ErrorCode", "GAME_DONE") in _edge_labels(r, "case_of") # ── 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" def test_cpp_generic_parents_include_type_argument_references(): """`class PooledClient : public Connection` must emit the inherits edge to Connection AND a generic_arg reference to the HttpClient type argument, matching the Java base-class behaviour (_emit_java_parent_type).""" r = extract_cpp(FIXTURES / "sample.cpp") assert ("PooledClient", "Connection") in _edge_labels(r, "inherits") assert ("PooledClient", "HttpClient") in _edge_labels(r, "references", "generic_arg") # ── CUDA ────────────────────────────────────────────────────────────────────── # CUDA is a C++ superset, so .cu/.cuh route through the C++ (tree-sitter-cpp) # extractor. These tests guard that __global__/__device__ kernels, host # functions, structs and includes are all extracted. def test_cuda_no_error(): r = extract_cpp(FIXTURES / "sample.cu") assert "error" not in r def test_cuda_finds_kernel_and_device_functions(): r = extract_cpp(FIXTURES / "sample.cu") labels = _labels(r) assert any("saxpy" in l for l in labels) # __global__ kernel assert any("dot" in l for l in labels) # __device__ function def test_cuda_finds_struct(): r = extract_cpp(FIXTURES / "sample.cu") assert any("Vec3" in l for l in _labels(r)) def test_cuda_finds_includes(): r = extract_cpp(FIXTURES / "sample.cu") assert "imports" in _relations(r) def test_cuda_host_call_edges(): r = extract_cpp(FIXTURES / "sample.cu") calls = _calls(r) assert ("host_norm()", "dot()") in calls assert ("main()", "host_norm()") in calls # Metal Shading Language is a C++14-derived language, so .metal files route # through the C++ extractor just like CUDA does. def test_metal_is_code_extension(): from graphify.detect import CODE_EXTENSIONS assert ".metal" in CODE_EXTENSIONS def test_metal_no_error(): r = extract_cpp(FIXTURES / "sample.metal") assert "error" not in r def test_metal_finds_kernel_function_and_struct(): r = extract_cpp(FIXTURES / "sample.metal") labels = _labels(r) assert any("Vec3" in l for l in labels) assert any("dot3" in l for l in labels) assert any("saxpy" in l for l in labels) # ── 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)) def test_ruby_inherits_edge(): """`class Sub < Base` must emit an inherits edge. Ruby exposes the base class in the `superclass` field, but there was no Ruby branch in the inheritance handler, so the edge was silently dropped. """ r = extract_ruby(FIXTURES / "sample.rb") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "TimeoutApiClient" in node_by_id.get(e["source"], "") and node_by_id.get(e["target"], "") == "ApiClient" for e in r["edges"] if e["relation"] == "inherits" ) assert found, "TimeoutApiClient should have inherits edge to ApiClient" # ── 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_generic_parents_include_type_argument_references(tmp_path): source = tmp_path / "GenericParents.java" source.write_text( "class Dependency {}\n" "interface Event {}\n" "class Base {}\n" "interface Handler {}\n" "interface DerivedHandler extends Handler {}\n" "class Service extends Base implements Handler {}\n" ) result = extract_java(source) assert ("Service", "Base") in _edge_labels(result, "inherits") assert ("Service", "Handler") in _edge_labels(result, "implements") refs = _edge_labels(result, "references", "generic_arg") assert ("Service", "Dependency") in refs assert ("Service", "Event") in refs assert ("DerivedHandler", "Handler") in _edge_labels(result, "inherits") assert ("DerivedHandler", "Event") in refs def test_java_type_parameters_do_not_emit_references(tmp_path): source = tmp_path / "TypeParameters.java" source.write_text( "class Payload {}\n" "class Base {}\n" "class Box extends Base {\n" " T value;\n" " List values;\n" " U convert(T input, List mapped, List retained) {\n" " return null;\n" " }\n" " Box(V value) {}\n" "}\n" ) result = extract_java(source) references = _references(result) assert not [edge for _, target, edge in references if target in {"T", "U", "V"}] assert not [ node for node in result["nodes"] if node.get("label") in {"T", "U", "V"} and not node.get("source_file") ] assert ("Box", "Base") in _edge_labels(result, "inherits") assert ("convert", "Payload") in _edge_labels(result, "references", "generic_arg") 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_java_field_type_references_have_field_context(tmp_path): source = tmp_path / "Fields.java" source.write_text( "class PaymentGateway {}\n" "class Handler {}\n" "class CheckoutService {\n" " PaymentGateway gateway;\n" " List handlers;\n" "}\n" ) result = extract_java(source) assert ("CheckoutService", "PaymentGateway") in _edge_labels( result, "references", "field" ) assert ("CheckoutService", "Handler") in _edge_labels( result, "references", "generic_arg" ) def test_java_record_component_type_references(tmp_path): source = tmp_path / "RecordComponents.java" source.write_text( "class Payload {}\n" "class Item {}\n" "class Attachment {}\n" "record Order(Payload payload, List items, int count, " "Attachment... attachments) {}\n" ) result = extract_java(source) assert ("Order", "Payload") in _edge_labels(result, "references", "field") # `List` is a java.util library type: skipped as noise, so only its user-type # generic argument (`Item`) survives, not the container itself. assert ("Order", "List") not in _edge_labels(result, "references") assert ("Order", "Item") in _edge_labels(result, "references", "generic_arg") assert ("Order", "Attachment") in _edge_labels(result, "references", "field") def test_java_record_components_skip_type_parameters(tmp_path): source = tmp_path / "GenericRecord.java" source.write_text( "class Payload {}\n" "class Box {}\n" "record Batch(T value, Box boxed, Box retained) {}\n" ) result = extract_java(source) assert ("Batch", "T") not in _edge_labels(result, "references") assert not [ node for node in result["nodes"] if node.get("label") == "T" and not node.get("source_file") ] assert ("Batch", "Box") in _edge_labels(result, "references", "field") assert ("Batch", "Payload") in _edge_labels(result, "references", "generic_arg") def test_java_type_annotations_have_attribute_context(tmp_path): source = tmp_path / "TypeAnnotations.java" source.write_text( '@Service\n' '@Entity(name = "checkout")\n' 'class CheckoutService {}\n' ) result = extract_java(source) refs = _edge_labels(result, "references", "attribute") assert ("CheckoutService", "Service") in refs assert ("CheckoutService", "Entity") in refs def test_java_enum_and_annotation_declarations_are_type_nodes(tmp_path): source = tmp_path / "TypeDeclarations.java" source.write_text( "enum PaymentStatus { PENDING, PAID }\n" "@interface Audited {}\n" "class Order { PaymentStatus status; }\n" "@Audited class CheckoutService {}\n" ) result = extract_java(source) assert ("TypeDeclarations.java", "PaymentStatus") in _edge_labels( result, "contains" ) assert ("TypeDeclarations.java", "Audited") in _edge_labels(result, "contains") assert ("Order", "PaymentStatus") in _edge_labels( result, "references", "field" ) assert ("CheckoutService", "Audited") in _edge_labels( result, "references", "attribute" ) definitions = { node["label"]: node for node in result["nodes"] if node.get("label") in {"PaymentStatus", "Audited"} } assert definitions["PaymentStatus"].get("source_file") == str(source) assert definitions["Audited"].get("source_file") == str(source) 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_property_type_references_have_field_context(): r = extract_csharp(FIXTURES / "sample.cs") field_refs = _edge_labels(r, "references", "field") # `public Processor Owner { get; set; }` — property type -> field ref. assert ("DataProcessor", "Processor") in field_refs # `public List Workers { get; set; }` — the List container -> field. assert ("DataProcessor", "List") in field_refs # ...and the generic argument -> generic_arg. assert ("DataProcessor", "Processor") in _edge_labels(r, "references", "generic_arg") 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_enum_entries_have_case_of_edge(): # #1700 (Kotlin half): enum entries must be nodes with case_of edges to the enum. r = extract_kotlin(FIXTURES / "sample.kt") labels = _labels(r) assert "NORMAL" in labels and "GROUP" in labels and "SYSTEM" in labels assert ("ChatType", "NORMAL") in _edge_labels(r, "case_of") assert ("ChatType", "SYSTEM") in _edge_labels(r, "case_of") 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_interface_delegation_emits_implements(): """`class Foo : Bar by baz` wraps the delegated interface in an `explicit_delegation` node — it must still emit an implements edge.""" r = extract_kotlin(FIXTURES / "sample.kt") assert ("LoggingList", "MutableList") 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_var_definition_field_context(): r = extract_scala(FIXTURES / "sample.scala") assert ("HttpClient", "BaseClient") 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") def test_php_constructor_property_promotion_contexts(): # PHP 8 constructor property promotion: a promoted param is both a # constructor parameter (parameter_type) and a class field (field). r = extract_php(FIXTURES / "sample.php") assert ("Service", "Result") in _edge_labels(r, "references", "field") assert ("__construct", "Result") in _edge_labels(r, "references", "parameter_type") # A non-promoted param must not leak a field edge onto the class. assert ("Service", "string") not in _edge_labels(r, "references", "field") # ── 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 # #1327: targets must resolve to a node too, else build.py prunes the edge. assert e["target"] in node_ids, f"dangling target {e['target']} ({e['relation']})" def test_swift_imports_survive_build(): # #1327: `import Foundation` / `import UIKit` previously emitted edges to bare # module ids with no backing node, so build.py dropped 100% of Swift imports. from graphify.build import build_from_json r = extract_swift(FIXTURES / "sample.swift") import_edges = [e for e in r["edges"] if e["relation"] == "imports"] assert import_edges, "extractor should emit Swift import edges" node_ids = {n["id"] for n in r["nodes"]} for e in import_edges: assert e["target"] in node_ids # synthesized module node exists # Imported modules are tagged type=module (anchor nodes, #1327/#1330). module_labels = {n["label"] for n in r["nodes"] if n.get("type") == "module"} assert {"Foundation", "UIKit"} <= module_labels # No private bookkeeping key should leak into output edges. assert all("_import_label" not in e for e in r["edges"]) # Edges must survive the build (which prunes edges with unknown endpoints). G = build_from_json(r) surviving = [ (u, v) for u, v, d in G.edges(data=True) if d.get("relation") == "imports" ] assert surviving, "Swift import edges must survive build_from_json (#1327)" 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_enum_associated_value_type_emits_references(): r = extract_swift(FIXTURES / "sample.swift") assert ("NetworkError", "Config") in _edge_labels(r, "references", "type") 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_multi_alias_expands(): """`alias Foo.{Bar, Baz}` must emit one imports edge per expanded module. The brace form is a `dot` node with a trailing `tuple`; the single-alias handler only matched a bare `alias` child, so every multi-alias import was silently dropped. """ r = extract_elixir(FIXTURES / "sample.ex") import_segs = [ e["target"].rsplit("_", 1)[-1] for e in r["edges"] if e["relation"] == "imports" ] # from `alias MyApp.Schemas.{Account, Token}` assert "account" in import_segs, "MyApp.Schemas.Account import missing" assert "token" in import_segs, "MyApp.Schemas.Token import missing" 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_protocol_adopts_protocol(): """`@protocol Derived ` must emit an implements edge Derived->Base. Protocol-on-protocol adoption nests under a protocol_reference_list node (distinct from the parameterized_arguments node used by @interface adoption), so the edge was previously dropped. Protocol nodes are labeled ``, so the edge reads (, ).""" r = extract_objc(FIXTURES / "sample.m") assert ("", "") 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}" def test_objc_resolves_self_method_calls(): """`[self speak]` inside Dog.fetch must produce a calls edge. The method-body second pass was dead code for ObjC because the grammar emits a simple selector as `identifier`, not `selector`/`keyword_argument_list` (#1475).""" r = extract_objc(FIXTURES / "sample.m") nid2label = {n["id"]: n["label"] for n in r["nodes"]} calls = [nid2label.get(e["target"]) for e in r["edges"] if e["relation"] == "calls"] assert any(t and "speak" in t for t in calls), calls def test_objc_class_method_labeled_with_plus(tmp_path): """`+ (…)shared` is a class method and must be labeled +shared, not -shared (#1475).""" p = tmp_path / "S.m" p.write_text("@implementation S\n+ (instancetype)shared { return nil; }\n- (void)go { }\n@end\n") labels = {n["label"] for n in extract_objc(p)["nodes"]} assert "+shared" in labels and "-go" in labels def test_objc_compound_selector_call_resolves(tmp_path): """A compound message `[self a:x b:y]` resolves to the compound method def (#1475).""" p = tmp_path / "V.m" p.write_text( "@implementation V\n" "- (void)tableView:(id)tv numberOfRowsInSection:(int)s { }\n" "- (void)go { [self tableView:nil numberOfRowsInSection:0]; }\n" "@end\n" ) r = extract_objc(p) nid2label = {n["id"]: n["label"] for n in r["nodes"]} calls = [nid2label.get(e["target"]) for e in r["edges"] if e["relation"] == "calls"] assert any(t and "tableViewnumberOfRowsInSection" in t for t in calls), calls def test_objc_generic_property_type_extracted(tmp_path): """`NSArray *` must reference the element type Product (and the container NSArray); the generic wrapper made the type invisible before (#1475).""" p = tmp_path / "M.h" p.write_text("@interface M : NSObject\n@property (strong) NSArray *items;\n@end\n") refs = _edge_labels(extract_objc(p), "references", "field") assert ("M", "Product") in refs assert ("M", "NSArray") in refs def test_objc_module_import_edge(tmp_path): """`@import Foundation;` / `@import UIKit.UIView;` produce imports edges (#1475).""" from graphify.extract import _make_id p = tmp_path / "X.m" p.write_text("@import Foundation;\n@import UIKit.UIView;\n@implementation X\n@end\n") targets = {e["target"] for e in extract_objc(p)["edges"] if e["relation"] == "imports"} assert _make_id("Foundation") in targets and _make_id("UIKit") in targets def test_objc_header_dispatch_routes_objc_not_c(tmp_path): """An ObjC `.h` (has @interface) routes to extract_objc; a plain C `.h` stays on extract_c, so C/C++ headers are never hijacked by the sniff (#1475).""" from graphify.extract import _get_extractor, extract_objc as _eo, extract_c as _ec objc_h = tmp_path / "AppDelegate.h" objc_h.write_text("@interface AppDelegate : NSObject \n@end\n") c_h = tmp_path / "util.h" c_h.write_text("#include \nint add(int a, int b);\nstruct Point { int x; };\n") assert _get_extractor(objc_h) is _eo assert _get_extractor(c_h) is _ec def test_objc_ns_assume_nonnull_macro_does_not_break_parsing(tmp_path): """`NS_ASSUME_NONNULL_BEGIN` before `@interface` made tree-sitter-objc fail to emit a class_interface node, swallowing the whole interface; blanking the argument-less macro restores it (#1475).""" p = tmp_path / "AlertManager.h" p.write_text( "#import \n" "NS_ASSUME_NONNULL_BEGIN\n" "@class Other;\n" "@interface AlertManager : NSObject\n" "- (void)show;\n" "@end\n" "NS_ASSUME_NONNULL_END\n" ) r = extract_objc(p) labels = {n["label"] for n in r["nodes"]} assert "AlertManager" in labels assert ("AlertManager", "NSObject") in _edge_labels(r, "inherits") # `@class Other;` is only a forward declaration; it must not mint a class node. assert "Other" not in labels def test_objc_macro_free_header_unchanged(tmp_path): """A macro-free header still parses exactly as before (regression).""" p = tmp_path / "Plain.h" p.write_text( "@interface Plain : NSObject\n" "- (void)go;\n" "@end\n" ) r = extract_objc(p) labels = {n["label"] for n in r["nodes"]} assert "Plain" in labels assert ("Plain", "NSObject") in _edge_labels(r, "inherits") def test_objc_quoted_import_edges_resolve_to_real_nodes(tmp_path): """Quoted `#import "X.h"` edges must target the real (disambiguated) file node id, not the bare stem, which gets salted away when a `.h`/`.m` pair exists and left the import edge dangling (#1475).""" from graphify.extract import extract (tmp_path / "Product.h").write_text("@interface Product : NSObject\n@end\n") (tmp_path / "Product.m").write_text("#import \"Product.h\"\n@implementation Product\n@end\n") (tmp_path / "Order.h").write_text("@interface Order : NSObject\n@end\n") (tmp_path / "Order.m").write_text("#import \"Order.h\"\n@implementation Order\n@end\n") consumer_a = tmp_path / "ConsumerA.m" consumer_a.write_text("#import \"Product.h\"\n@implementation ConsumerA\n@end\n") consumer_b = tmp_path / "ConsumerB.m" consumer_b.write_text("#import \"Order.h\"\n@implementation ConsumerB\n@end\n") files = [ tmp_path / "Product.h", tmp_path / "Product.m", tmp_path / "Order.h", tmp_path / "Order.m", consumer_a, consumer_b, ] r = extract(files, parallel=False) node_ids = {n["id"] for n in r["nodes"]} id_to_label = {n["id"]: n.get("label", "") for n in r["nodes"]} import_edges = [e for e in r["edges"] if e["relation"] in ("imports", "imports_from")] assert import_edges for e in import_edges: # No dangling targets... assert e["target"] in node_ids, f"dangling import target: {e}" # ...and no self-loops: a `.m` importing its own `.h` must resolve to the # header file node, not get salted back to the importing `.m` (#1475). assert e["source"] != e["target"], f"self-loop import edge: {e}" # every quoted import targets a header (.h) file node assert str(id_to_label.get(e["target"], "")).endswith(".h"), ( f"import target is not a header file node: {e} -> {id_to_label.get(e['target'])}" ) # the self-import (Product.m -> Product.h) specifically lands on the .h variant prod_imports = [e for e in import_edges if id_to_label.get(e["source"], "").endswith("Product.m")] assert prod_imports and all(id_to_label.get(e["target"]) == "Product.h" for e in prod_imports), ( f"Product.m should import the Product.h node, got {[(id_to_label.get(e['source']), id_to_label.get(e['target'])) for e in prod_imports]}" ) def test_objc_alloc_init_emits_type_reference(tmp_path): """`[[Foo alloc] init]` must emit a `references` edge to the project class Foo (#1475).""" from graphify.extract import extract (tmp_path / "Foo.h").write_text("@interface Foo : NSObject\n@end\n") (tmp_path / "Foo.m").write_text("#import \"Foo.h\"\n@implementation Foo\n@end\n") user = tmp_path / "User.m" user.write_text( "#import \"Foo.h\"\n" "@implementation User\n" "- (void)build { Foo *x = [[Foo alloc] init]; }\n" "@end\n" ) r = extract([tmp_path / "Foo.h", tmp_path / "Foo.m", user], parallel=False) assert ("-build", "Foo") in _edge_labels(r, "references") def test_objc_alloc_init_unknown_class_no_resolved_edge(tmp_path): """`[[Unknown alloc] init]` with no such class must not produce a resolved reference edge (the sourceless stub is collapsed only when a real class exists).""" p = tmp_path / "Caller.m" p.write_text( "@implementation Caller\n" "- (void)build { id x = [[Unknown alloc] init]; }\n" "- (void)other { [self build]; [x doStuff]; }\n" "@end\n" ) r = extract_objc(p) # The single-file extractor emits the edge to a sourceless stub; assert there is # no resolved reference to a *real* (sourced) Unknown node and that ordinary # selector sends ([self build] / [x doStuff]) produce no alloc reference. sourced_ids = {n["id"] for n in r["nodes"] if n.get("source_file")} refs = [e for e in r["edges"] if e["relation"] == "references"] for e in refs: assert e["target"] not in sourced_ids, f"unexpected resolved ref: {e}" def test_objc_dot_syntax_property_accesses_edge(tmp_path): """self.name dot-syntax resolves to an accesses edge within the same class.""" p = tmp_path / "Dog.m" p.write_text( "@implementation Dog\n" "- (NSString *)name { return @\"Rex\"; }\n" "- (void)greet { NSLog(@\"%@\", self.name); }\n" "@end\n" ) r = extract_objc(p) accesses = [(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "accesses"] nid2label = {n["id"]: n["label"] for n in r["nodes"]} assert len(accesses) == 1 assert nid2label[accesses[0][1]] == "-name" def test_objc_dot_syntax_no_fanout_two_same_named_properties(tmp_path): """Two classes each declaring -name: self.name in A must NOT fan out to B's -name.""" p = tmp_path / "AB.m" p.write_text( "@implementation A\n" "- (NSString *)name { return @\"A\"; }\n" "- (void)show { NSLog(@\"%@\", self.name); }\n" "@end\n" "@implementation B\n" "- (NSString *)name { return @\"B\"; }\n" "- (void)show { NSLog(@\"%@\", self.name); }\n" "@end\n" ) r = extract_objc(p) accesses = [e for e in r["edges"] if e["relation"] == "accesses"] assert len(accesses) == 2, f"expected 2 scoped accesses, got {len(accesses)}: {accesses}" nid2label = {n["id"]: n["label"] for n in r["nodes"]} for e in accesses: src_label = nid2label[e["source"]] tgt_label = nid2label[e["target"]] assert src_label == "-show" and tgt_label == "-name" def test_objc_dot_syntax_unresolvable_property_zero_edges(tmp_path): """Accessing a property not defined in the current class produces zero accesses edges.""" p = tmp_path / "X.m" p.write_text( "@implementation X\n" "- (void)run { NSLog(@\"%@\", self.missing); }\n" "@end\n" ) r = extract_objc(p) accesses = [e for e in r["edges"] if e["relation"] == "accesses"] assert len(accesses) == 0 def test_objc_selector_expression_calls_edge(tmp_path): """@selector(uniqueMethod) with exactly one match produces a calls edge.""" p = tmp_path / "Sched.m" p.write_text( "@implementation Sched\n" "- (void)fetch { }\n" "- (void)schedule { [self performSelector:@selector(fetch)]; }\n" "@end\n" ) r = extract_objc(p) nid2label = {n["id"]: n["label"] for n in r["nodes"]} sel_calls = [(nid2label.get(e["source"]), nid2label.get(e["target"])) for e in r["edges"] if e["relation"] == "calls" and e.get("context") == "call"] assert ("-schedule", "-fetch") in sel_calls def test_objc_selector_no_fanout_two_same_named_methods(tmp_path): """@selector(doThing) with two doThing methods must emit zero calls edges.""" p = tmp_path / "Dual.m" p.write_text( "@implementation A\n" "- (void)doThing { }\n" "- (void)run { [self performSelector:@selector(doThing)]; }\n" "@end\n" "@implementation B\n" "- (void)doThing { }\n" "@end\n" ) r = extract_objc(p) nid2label = {n["id"]: n["label"] for n in r["nodes"]} sel_edges = [e for e in r["edges"] if e["relation"] == "calls" and nid2label.get(e["target"], "").endswith("doThing")] assert len(sel_edges) == 0, f"expected 0 selector edges with ambiguous name, got {sel_edges}" def test_objc_dot_syntax_substring_sibling_exact_match(tmp_path): """A substring-colliding sibling must neither be falsely matched nor suppress the real match: `self.name` with both `-name` and `-surname` present resolves to `-name` ONLY (exact id, not a `endswith` suffix) (#1475).""" p = tmp_path / "Person.m" p.write_text( "@implementation Person\n" "- (NSString *)name { return @\"n\"; }\n" "- (NSString *)surname { return @\"s\"; }\n" "- (void)show { NSLog(@\"%@\", self.name); }\n" "@end\n" ) r = extract_objc(p) nid2label = {n["id"]: n["label"] for n in r["nodes"]} accesses = [(nid2label.get(e["source"]), nid2label.get(e["target"])) for e in r["edges"] if e["relation"] == "accesses"] assert ("-show", "-name") in accesses, f"self.name must resolve to -name: {accesses}" assert ("-show", "-surname") not in accesses, f"self.name must NOT match -surname: {accesses}" def test_objc_selector_substring_method_exact_match(tmp_path): """@selector(doThing) must resolve to `-doThing` exactly, not be suppressed by a substring-colliding `-reallyDoThing` (exact match, not suffix) (#1475).""" p = tmp_path / "Worker.m" p.write_text( "@implementation Worker\n" "- (void)doThing { }\n" "- (void)reallyDoThing { }\n" "- (void)run { [self performSelector:@selector(doThing)]; }\n" "@end\n" ) r = extract_objc(p) nid2label = {n["id"]: n["label"] for n in r["nodes"]} sel_calls = [(nid2label.get(e["source"]), nid2label.get(e["target"])) for e in r["edges"] if e["relation"] == "calls" and e.get("context") == "call"] assert ("-run", "-doThing") in sel_calls, f"@selector(doThing) must resolve to -doThing: {sel_calls}" assert ("-run", "-reallyDoThing") not in sel_calls # --------------------------------------------------------------------------- # 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_qualified_and_relative_imports(): """Qualified (`using Base.Threads`) and relative (`using ..Mod`) imports must emit edges. The handler only matched bare identifiers, so scoped_identifier and import_path forms — and the scoped package of a selected_import — were silently dropped. """ r = extract_julia(FIXTURES / "sample.jl") targets = [e["target"] for e in r["edges"] if e["relation"] == "imports"] assert any("base_threads" in t for t in targets), "qualified import Base.Threads missing" assert any("parentmodule" in t for t in targets), "relative import ParentModule missing" 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_finds_function_call(): """`y = f(x)` function invocations must emit a calls edge. Function calls are `call_expression` (not `subroutine_call`); that node was never handled, so every function-to-function call was dropped. The callee is resolved against defined procedures so array indexing (`arr(i)`) can't fabricate a spurious edge. """ r = extract_fortran(FIXTURES / "sample.f90") labels = {n["id"]: n["label"] for n in r["nodes"]} found = any( "report" in labels.get(e["source"], "") and "double_val" in labels.get(e["target"], "") for e in r["edges"] if e["relation"] == "calls" ) assert found, "report() should have a calls edge to double_val()" 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_psm1_dispatched_and_extracted(tmp_path): # #1315: .psm1 modules were never indexed — no dispatch entry, no CODE_EXTENSIONS. from graphify.extract import _get_extractor mod = tmp_path / "Utils.psm1" mod.write_text( "function Get-Greeting { param([string]$Name) return \"Hi $Name\" }\n", encoding="utf-8", ) assert _get_extractor(mod) is extract_powershell r = extract_powershell(mod) assert "error" not in r assert any("Get-Greeting" in n["label"] for n in r["nodes"]) 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_class_base_type_emits_inherits_edge(): # `class Circle : Shape` — the base type after ':' was previously dropped # because the handler only read the first simple_name (the class name). r = extract_powershell(FIXTURES / "sample.ps1") assert ("Circle", "Shape") in _edge_labels(r, "inherits") 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") # ── PowerShell: Import-Module + dot-source (#1331) ─────────────────────────── def test_powershell_import_module_emits_edge(): """Import-Module Foo at top level emits an imports_from edge.""" r = extract_powershell(FIXTURES / "sample_import.ps1") assert "error" not in r targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("foo" in t for t in targets), f"Missing Import-Module Foo edge; targets={targets}" def test_powershell_import_module_with_name_param(): """Import-Module -Name Bar.psm1 resolves to module stem 'bar'.""" r = extract_powershell(FIXTURES / "sample_import.ps1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("bar" in t for t in targets), f"Missing Import-Module -Name Bar edge; targets={targets}" def test_powershell_dot_source_forward_slash_emits_edge(): """Dot-source `. ./Shared.psm1` emits an imports_from edge.""" r = extract_powershell(FIXTURES / "sample_import.ps1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("shared" in t for t in targets), f"Missing dot-source Shared edge; targets={targets}" def test_powershell_dot_source_backslash_emits_edge(): """Dot-source `. .\\Utils.ps1` (backslash path) emits an imports_from edge.""" r = extract_powershell(FIXTURES / "sample_import.ps1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("utils" in t for t in targets), f"Missing dot-source Utils edge; targets={targets}" def test_powershell_import_module_inside_function_emits_edge(): """Import-Module inside a function body still produces an imports_from edge.""" r = extract_powershell(FIXTURES / "sample_import.ps1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("innermod" in t for t in targets), ( f"Missing Import-Module InnerMod edge from function body; targets={targets}" ) def test_powershell_import_module_not_a_raw_call(): """Import-Module must not appear in raw_calls (it is an import, not a function call).""" r = extract_powershell(FIXTURES / "sample_import.ps1") import_module_calls = [ rc for rc in r.get("raw_calls", []) if rc.get("callee", "").lower() == "import-module" ] assert not import_module_calls, ( f"Import-Module appeared in raw_calls but should be emitted as import edge: {import_module_calls}" ) def test_powershell_dot_source_inside_function_emits_edge(): """Dot-source inside a function body still produces an imports_from edge.""" r = extract_powershell(FIXTURES / "sample_import.ps1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("innershared" in t for t in targets), ( f"Missing dot-source InnerShared edge from function body; targets={targets}" ) # ── PowerShell manifest (.psd1) (#1331) ────────────────────────────────────── def test_powershell_psd1_dispatched(): """_get_extractor should route .psd1 to extract_powershell_manifest.""" from graphify.extract import _get_extractor import tempfile, os with tempfile.NamedTemporaryFile(suffix=".psd1", delete=False) as f: f.write(b"@{ RootModule = 'X.psm1' }") path = f.name try: assert _get_extractor(Path(path)) is extract_powershell_manifest finally: os.unlink(path) def test_powershell_psd1_no_error(): r = extract_powershell_manifest(FIXTURES / "sample.psd1") assert "error" not in r def test_powershell_psd1_has_file_node(): r = extract_powershell_manifest(FIXTURES / "sample.psd1") assert any("sample.psd1" in n["label"] for n in r["nodes"]), ( f"Missing file node for sample.psd1; nodes={[n['label'] for n in r['nodes']]}" ) def test_powershell_psd1_root_module(): """RootModule = 'MyModule.psm1' produces an imports_from edge to 'mymodule'.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("mymodule" in t for t in targets), ( f"Missing RootModule edge for MyModule; targets={targets}" ) def test_powershell_psd1_nested_modules(): """NestedModules = @('Helpers.psm1', 'Logger.psm1') produces edges for both.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("helpers" in t for t in targets), f"Missing NestedModules Helpers edge; targets={targets}" assert any("logger" in t for t in targets), f"Missing NestedModules Logger edge; targets={targets}" def test_powershell_psd1_required_modules_string(): """RequiredModules string form 'PSReadLine' produces an imports_from edge.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("psreadline" in t for t in targets), ( f"Missing RequiredModules PSReadLine edge; targets={targets}" ) def test_powershell_psd1_required_modules_hashtable(): """RequiredModules hashtable form @{{ ModuleName='Pester' }} produces an imports_from edge.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert any("pester" in t for t in targets), ( f"Missing RequiredModules Pester (hashtable form) edge; targets={targets}" ) def test_powershell_psd1_no_moduleversion_as_edge(): """ModuleVersion values ('5.0', '1.0.0') must NOT appear as import targets.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"} assert not any(t in targets for t in ("5_0", "1_0_0", "5.0", "1.0.0")), ( f"ModuleVersion string leaked into import targets: {targets}" ) def test_powershell_psd1_no_dangling_edges(): """All imports_from edge sources must exist in the node set.""" r = extract_powershell_manifest(FIXTURES / "sample.psd1") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"Dangling source in edge: {e}" # ── 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}" def test_ts_constructor_injection_calls_edge(tmp_path): """this.repo.findById() in a class with constructor(private repo: IUserRepository) must produce a calls edge from getUser() to findById() (#1316).""" from graphify.extract import extract repo_ts = tmp_path / "repo.ts" repo_ts.write_text( "export interface IUserRepository {\n" " findById(id: string): Promise;\n" " save(user: any): Promise;\n" "}\n" ) svc_ts = tmp_path / "service.ts" svc_ts.write_text( "import { IUserRepository } from './repo';\n" "\n" "export class UserService {\n" " constructor(private repo: IUserRepository) {}\n" "\n" " getUser(id: string) {\n" " return this.repo.findById(id);\n" " }\n" "}\n" ) r = extract([repo_ts, svc_ts], cache_root=tmp_path / "cache") edge_triples = { (e["source"], e["relation"], e["target"]) for e in r["edges"] } labels_by_id = {n["id"]: n["label"] for n in r["nodes"]} label_triples = { (labels_by_id.get(s, s), rel, labels_by_id.get(t, t)) for s, rel, t in edge_triples } calls_from_get_user = [ (s, rel, t) for s, rel, t in label_triples if "getUser" in s and rel == "calls" ] assert any("findById" in t for _, _, t in calls_from_get_user), ( f"expected getUser()->findById() calls edge, got: {calls_from_get_user}" ) def test_ts_this_field_receiver_not_same_file_collision(tmp_path): """this.db.query() should NOT match an unrelated query() in the same file (#1316).""" f = tmp_path / "collision.ts" f.write_text( "function query() { return 'global'; }\n" "\n" "export class Service {\n" " constructor(private db: Database) {}\n" "\n" " run() {\n" " return this.db.query();\n" " }\n" "}\n" ) r = extract_js(f) calls_edges = [ e for e in r["edges"] if e["relation"] == "calls" ] caller_labels = {n["id"]: n["label"] for n in r["nodes"]} run_to_query = [ e for e in calls_edges if "run" in caller_labels.get(e["source"], "") and "query" in caller_labels.get(e["target"], "") ] assert len(run_to_query) == 0, ( f"this.db.query() should NOT resolve to bare query() in same file: {run_to_query}" ) def _ts_label_calls(r, src_sub): labels = {n["id"]: n["label"] for n in r["nodes"]} return [ labels.get(e["target"], e["target"]) for e in r["edges"] if e["relation"] == "calls" and src_sub in labels.get(e["source"], e["source"]) ] def test_ts_injected_field_resolves_to_typed_class_not_same_named_collision(tmp_path): """The decisive #1316 guardrail: two classes each define `query`, but the injected field is typed `Database`, so `this.db.query()` must resolve to Database.query ONLY — never HttpClient.query (no global name-match fan-out).""" from graphify.extract import extract (tmp_path / "database.ts").write_text( "export class Database {\n query(sql: string) { return sql; }\n}\n" ) (tmp_path / "http.ts").write_text( "export class HttpClient {\n query(url: string) { return url; }\n}\n" ) (tmp_path / "service.ts").write_text( "import { Database } from './database';\n" "export class Service {\n" " constructor(private db: Database) {}\n" " run() { return this.db.query('x'); }\n" "}\n" ) r = extract( [tmp_path / "database.ts", tmp_path / "http.ts", tmp_path / "service.ts"], cache_root=tmp_path / "cache", ) labels = {n["id"]: n["label"] for n in r["nodes"]} # Find the run()->query calls edge and confirm its target is owned by Database. method_owner = { e["target"]: e["source"] for e in r["edges"] if e["relation"] == "method" } run_query_targets = [ e["target"] for e in r["edges"] if e["relation"] == "calls" and "run" in labels.get(e["source"], "") and "query" in labels.get(e["target"], "") ] assert run_query_targets, "expected this.db.query() to resolve to a query method" for tgt in run_query_targets: owner = method_owner.get(tgt) assert owner is not None and labels.get(owner) == "Database", ( f"this.db.query() must resolve to Database.query, got owner {labels.get(owner)}" ) def test_ts_injected_field_ambiguous_type_emits_no_edge(tmp_path): """If the injected field's type name is ambiguous (two classes named Database), the god-node guard bails — no calls edge rather than a guess (#1316).""" from graphify.extract import extract (tmp_path / "a" ).mkdir() (tmp_path / "b").mkdir() (tmp_path / "a" / "database.ts").write_text( "export class Database {\n query(sql: string) { return sql; }\n}\n" ) (tmp_path / "b" / "database.ts").write_text( "export class Database {\n query(sql: string) { return sql; }\n}\n" ) (tmp_path / "service.ts").write_text( "export class Service {\n" " constructor(private db: Database) {}\n" " run() { return this.db.query('x'); }\n" "}\n" ) r = extract(sorted(tmp_path.rglob("*.ts")), cache_root=tmp_path / "cache") # `query` resolution must bail (2 Database defs) -> no run()->query calls edge. assert not [t for t in _ts_label_calls(r, "run") if "query" in t], ( "ambiguous Database type must not produce a this.db.query() edge" ) # ── 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}" def _md_link_fixture(tmp_path): """A hub doc linking to sibling docs, plus those docs (#1376).""" pkg = tmp_path / "packages" / "coding-standards-csharp" pkg.mkdir(parents=True) (pkg / "index.md").write_text( "# C# Coding Standards\n\n" "| Topic | Doc |\n| --- | --- |\n" "| Repository | [C# Repository Standards](./repository.md) |\n" "| HTTP Client | [C# HTTP Client Standards](http-client.md) |\n" "| Unit Tests | [C# Unit Test Standards](unit-tests.md) |\n\n" "See also [external](https://example.com/x) and ![logo](./logo.png).\n" "Anchor: [section](./repository.md#setup).\n" "Wikilink: [[http-client]].\n" ) (pkg / "repository.md").write_text("# C# Repository Standards\nContent.\n") (pkg / "http-client.md").write_text("# C# HTTP Client Standards\nContent.\n") (pkg / "unit-tests.md").write_text("# C# Unit Test Standards\nContent.\n") return pkg def test_markdown_link_edges_emitted(tmp_path): """Inline/wikilink markdown links to sibling docs become references edges (#1376).""" pkg = _md_link_fixture(tmp_path) r = extract_markdown(pkg / "index.md") refs = [e for e in r["edges"] if e["relation"] == "references"] targets = {e["target"] for e in refs} # repository, http-client, unit-tests — each exactly once (deduped despite # the anchor link and wikilink pointing at repository/http-client again). assert len(refs) == 3, f"expected 3 reference edges, got {refs}" assert any("repository" in t for t in targets) assert any("http_client" in t for t in targets) assert any("unit_tests" in t for t in targets) def test_markdown_link_skips_external_and_images(tmp_path): """External URLs, in-page anchors and images must not produce edges (#1376).""" pkg = _md_link_fixture(tmp_path) r = extract_markdown(pkg / "index.md") refs = [e for e in r["edges"] if e["relation"] == "references"] for e in refs: assert "example.com" not in e["target"] assert "logo" not in e["target"] def test_markdown_link_edges_resolve_to_real_nodes(tmp_path): """End-to-end: after extract()'s ID remap, link targets are real doc nodes, so the hub doc gains edges into existing nodes instead of ghost nodes (#1376).""" from graphify.extract import extract pkg = _md_link_fixture(tmp_path) paths = sorted(pkg.glob("*.md")) res = extract(paths, cache_root=tmp_path, parallel=False) node_ids = {n["id"] for n in res["nodes"]} refs = [e for e in res["edges"] if e["relation"] == "references"] assert refs, "expected reference edges after full extract" for e in refs: assert e["target"] in node_ids, f"link target is a ghost node: {e}" # index.md must connect to all three sibling docs. index_id = next(n["id"] for n in res["nodes"] if n["label"] == "index.md") index_refs = {e["target"] for e in refs if e["source"] == index_id} assert len(index_refs) == 3, f"hub doc under-connected: {index_refs}" # ── 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_extends_edge(): """`class X extends Base` must emit an inherits edge. tree-sitter-groovy exposes inheritance via the same `superclass` field as tree-sitter-java, but the inheritance handler was gated to Java only, so Groovy extends/implements were silently dropped. """ r = extract_groovy(FIXTURES / "sample.groovy") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "ExtendedService" in node_by_id.get(e["source"], "") and "SampleService" in node_by_id.get(e["target"], "") for e in r["edges"] if e["relation"] == "inherits" ) assert found, "ExtendedService should have inherits edge to SampleService" def test_groovy_implements_edge(): """`class X implements Iface` must emit an implements edge.""" r = extract_groovy(FIXTURES / "sample.groovy") node_by_id = {n["id"]: n["label"] for n in r["nodes"]} found = any( "ExtendedService" in node_by_id.get(e["source"], "") and "Resettable" in node_by_id.get(e["target"], "") for e in r["edges"] if e["relation"] == "implements" ) assert found, "ExtendedService should have implements edge to Resettable" 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, .xaml, .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_xaml_finds_class_and_event_references(): r = extract_xaml(FIXTURES / "sample.xaml") assert "error" not in r assert "MainWindow" in _labels(r) assert any(e["relation"] == "references" and e.get("context") == "event" for e in r["edges"]) 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_interface_extends(tmp_path): source = tmp_path / "PaymentProcessor.cls" source.write_text( "public interface PaymentProcessor extends Processor, Auditable { void process(); }\n" ) result = extract_apex(source) inheritance = _edge_labels(result, "extends") | _edge_labels(result, "implements") assert ("PaymentProcessor", "Processor") in inheritance assert ("PaymentProcessor", "Auditable") in inheritance 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}" # -- SystemVerilog ------------------------------------------------------------- def test_systemverilog_no_error(): r = extract_verilog(FIXTURES / "sample.sv") assert "error" not in r def test_systemverilog_splits_inherits_and_implements(): r = extract_verilog(FIXTURES / "sample.sv") assert ("DataProcessor", "BaseProcessor") in _edge_labels(r, "inherits") assert ("DataProcessor", "Processor") in _edge_labels(r, "implements") def test_systemverilog_field_parameter_return_and_generic_contexts(): r = extract_verilog(FIXTURES / "sample.sv") assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") assert ("DataProcessor", "Payload") in _edge_labels(r, "references", "generic_arg") assert ("build", "Payload") in _edge_labels(r, "references", "parameter_type") assert ("build", "Result") in _edge_labels(r, "references", "return_type") assert ("build", "Payload") in _edge_labels(r, "references", "generic_arg") def test_systemverilog_qualified_field_references(): """Class properties with leading qualifiers (rand/local/protected/etc.) must still emit `references` field edges. The field regex only matched unqualified ` ;` declarations, so `rand Config x;` (three tokens) failed to match and its type reference was silently dropped. """ r = extract_verilog(FIXTURES / "sample.sv") field_refs = _edge_labels(r, "references", "field") assert ("DataProcessor", "Config") in field_refs, "rand-qualified field dropped" assert ("DataProcessor", "BaseProcessor") in field_refs, "protected-qualified field dropped" def test_systemverilog_does_not_emit_type_parameter_refs(): r = extract_verilog(FIXTURES / "sample.sv") assert ("Result", "T") not in _edge_labels(r, "references", "field") def test_systemverilog_preserves_existing_module_extraction(): r = extract_verilog(FIXTURES / "sample.sv") labels = set(_labels(r)) assert {"top", "leaf", "add()", "tick"}.issubset(labels) assert "imports_from" in _relations(r) assert "instantiates" in _relations(r) def test_systemverilog_missing_file_returns_empty(): r = extract_verilog(Path("nonexistent.sv")) assert r["nodes"] == [] assert r["edges"] == [] def test_systemverilog_no_dangling_edges(): r = extract_verilog(FIXTURES / "sample.sv") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"dangling source: {e}" assert e["target"] in node_ids, f"dangling target: {e}" # ── Header/impl class merge + .h routing (#1547 C++, #1556 ObjC/Swift) ───────── from graphify.extract import ( extract as _extract_corpus, _get_extractor, _is_cpp_header, _is_objc_header, ) def _corpus(*relpaths): """Run the full extract() pipeline on fixture files (absolute, resolved paths so the per-file id-remap behaves like real usage), no shared cache.""" import tempfile paths = [(FIXTURES / rp).resolve() for rp in relpaths] with tempfile.TemporaryDirectory() as td: return _extract_corpus(paths, cache_root=Path(td)) def _nodes_with_label(r, label): return [n for n in r["nodes"] if n["label"] == label] def _assert_no_dangling(r): ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in ids, f"dangling source: {e}" assert e["target"] in ids, f"dangling target: {e}" # --- #1547: C++ paired header/impl -------------------------------------------- def test_cpp_header_routes_to_cpp_extractor(): """A `.h` with a C++ class must route to extract_cpp, not extract_c (which has no class_specifier and would drop the class entirely).""" p = (FIXTURES / "cpp_paired" / "Foo.h").resolve() assert _get_extractor(p).__name__ == "extract_cpp" assert _is_cpp_header(p) def test_plain_c_header_stays_on_c_extractor(): """A plain C header (no C++ signal) must keep its extract_c routing.""" p = (FIXTURES / "cpp_samedir" / "plain.h").resolve() assert not _is_cpp_header(p) assert _get_extractor(p).__name__ == "extract_c" def test_cpp_paired_single_class_node(): """Foo.h (class) + Foo.cpp (Foo::bar def) + Main.cpp must yield exactly ONE Foo class node — not a foo_h + foo_cpp pair, and no junk `class` stub.""" r = _corpus("cpp_paired/Foo.h", "cpp_paired/Foo.cpp", "cpp_paired/Main.cpp") foos = _nodes_with_label(r, "Foo") assert len(foos) == 1, f"expected one Foo, got {[n['id'] for n in foos]}" assert not _nodes_with_label(r, "class"), "no sourceless `class` stub should exist" assert not _nodes_with_label(r, "foo_foo") def test_cpp_paired_method_decl_and_def_are_one_node(): """`void bar();` in Foo.h and `void Foo::bar() {}` in Foo.cpp must collapse to ONE method node owned by the single Foo class.""" r = _corpus("cpp_paired/Foo.h", "cpp_paired/Foo.cpp", "cpp_paired/Main.cpp") foo = _nodes_with_label(r, "Foo")[0]["id"] method_targets = { e["target"] for e in r["edges"] if e["source"] == foo and e["relation"] in ("method", "defines", "contains") } bar_nodes = [n for n in r["nodes"] if n["id"] in method_targets and n["label"] in ("bar", "Foo::bar()")] # There must be exactly one node representing bar (decl and def merged). bar_ids = {n["id"] for n in r["nodes"] if n["label"] in ("bar", "Foo::bar()")} assert len(bar_ids) == 1, f"bar decl/def should be one node, got {bar_ids}" assert bar_nodes, "the merged bar node should be a member of Foo" def test_cpp_paired_includes_resolve_to_real_header(): """Foo.cpp and Main.cpp `#include "Foo.h"` must resolve to the real Foo.h file node (no dangling import).""" r = _corpus("cpp_paired/Foo.h", "cpp_paired/Foo.cpp", "cpp_paired/Main.cpp") ids = {n["id"] for n in r["nodes"]} foo_h = _nodes_with_label(r, "Foo.h")[0]["id"] imports = [e for e in r["edges"] if e["relation"] == "imports"] assert len(imports) >= 2 for e in imports: assert e["target"] in ids, f"dangling import target: {e}" assert any(e["target"] == foo_h for e in imports), "includes should target Foo.h" def test_cpp_paired_no_dangling_edges(): r = _corpus("cpp_paired/Foo.h", "cpp_paired/Foo.cpp", "cpp_paired/Main.cpp") _assert_no_dangling(r) # --- #1556: ObjC paired header/impl + bridging header ------------------------- def test_objc_header_with_import_routes_to_objc(): """A bridging header that is only `#import "X.h"` (no @interface) must route to extract_objc; extract_c parses `#import` as preproc_call and drops the edge.""" p = (FIXTURES / "objc_mixed" / "Bridging-Header.h").resolve() assert _is_objc_header(p) assert _get_extractor(p).__name__ == "extract_objc" def test_objc_paired_single_class_methods_not_duplicated(): """Widget.h (@interface) + Widget.m (@implementation) -> ONE Widget class node with its methods present once each.""" r = _corpus("objc_mixed/Widget.h", "objc_mixed/Widget.m") widgets = _nodes_with_label(r, "Widget") assert len(widgets) == 1, f"expected one Widget, got {[n['id'] for n in widgets]}" render = _nodes_with_label(r, "-render") refresh = _nodes_with_label(r, "-refresh") assert len(render) == 1, f"-render duplicated: {render}" assert len(refresh) == 1, f"-refresh duplicated: {refresh}" def test_objc_bridging_header_not_isolated(): """A bridging header of only `#import "Widget.h"` must produce an imports edge to the real Widget.h node (not be an isolated node).""" r = _corpus("objc_mixed/Widget.h", "objc_mixed/Widget.m", "objc_mixed/Bridging-Header.h") bridge = _nodes_with_label(r, "Bridging-Header.h")[0]["id"] widget_h = _nodes_with_label(r, "Widget.h")[0]["id"] out = [e for e in r["edges"] if e["source"] == bridge and e["relation"] == "imports"] assert out, "bridging header should emit an imports edge" assert any(e["target"] == widget_h for e in out), "bridging import should target Widget.h" def test_objc_paired_no_dangling_edges(): r = _corpus("objc_mixed/Widget.h", "objc_mixed/Widget.m", "objc_mixed/Bridging-Header.h") _assert_no_dangling(r) # --- #1556: Swift extension folds onto canonical ObjC class ------------------- def test_swift_extension_folds_onto_objc_class(): """`extension Widget` in Swift over an ObjC `Widget` must fold onto the single canonical Widget node, with its members anchored there.""" r = _corpus("objc_mixed/Widget.h", "objc_mixed/Widget.m", "objc_mixed/WidgetExtras.swift") widgets = _nodes_with_label(r, "Widget") assert len(widgets) == 1, f"expected one Widget, got {[n['id'] for n in widgets]}" wid = widgets[0]["id"] method_targets = {e["target"] for e in r["edges"] if e["relation"] == "method" and e["source"] == wid} labels = {n["label"] for n in r["nodes"] if n["id"] in method_targets} assert any("describe" in l for l in labels), f"Swift extension method should anchor on Widget, got {labels}" _assert_no_dangling(r) # --- god-node guard negatives ------------------------------------------------- def test_decldef_merge_does_not_merge_across_directories(): """Two unrelated `class Logger` in DIFFERENT directories (each its own .h/.cpp) must NOT merge — assert TWO distinct Logger nodes.""" r = _corpus( "cpp_logger/a/Logger.h", "cpp_logger/a/Logger.cpp", "cpp_logger/b/Logger.h", "cpp_logger/b/Logger.cpp", ) loggers = _nodes_with_label(r, "Logger") assert len(loggers) == 2, f"cross-dir Loggers must stay distinct, got {[n['id'] for n in loggers]}" assert len({n["id"] for n in loggers}) == 2 def test_decldef_merge_does_not_merge_same_name_same_dir_distinct_files(): """Two same-named `class Dup` in the SAME dir but different base stems (Alpha.h, Beta.h) must stay distinct (no unique header/impl sibling pair).""" r = _corpus("cpp_samedir/Alpha.h", "cpp_samedir/Beta.h") dups = _nodes_with_label(r, "Dup") assert len(dups) == 2, f"same-dir distinct Dups must stay distinct, got {[n['id'] for n in dups]}"