Files
graphify/tests/test_languages.py
T
Safi 8bcfffdf62 add .NET project file support (.sln, .csproj, .fsproj, .vbproj, .razor, .cshtml)
Adds extract_sln, extract_csproj, and extract_razor extractors. Captures NuGet
package refs, project-to-project dependencies, target frameworks, SDK attribute,
@using/@inject/@inherits/@model directives, Blazor component refs, and @code
methods. Resolves relative project paths to absolute paths so sln/csproj nodes
link correctly when the graph is assembled. Closes #515.

Co-Authored-By: aksrathore <aksrathore@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:27:28 +01:00

1133 lines
42 KiB
Python

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