Files
graphify/tests/test_languages.py
T
Rangarajan Ramaswamy 5dbbcf7dad feat: add VB.NET (.vb) language support via tree-sitter
This commit adds full VB.NET language support to graphify, raising the
supported language count from 25 to 26. The implementation follows the
established LanguageConfig pattern used by all other tree-sitter-backed
extractors.

New dependency:
- Adds optional extra [vbnet] backed by tree-sitter-vbnet (published
  to PyPI at https://pypi.org/project/tree-sitter-vbnet/0.1.0/).
  Install with: pip install graphifyy[vbnet]

graphify/detect.py:
- Added .vb to CODE_EXTENSIONS so VB.NET files are discovered during
  corpus ingestion and file-system watching.

graphify/extract.py:
- _import_vbnet(): import handler for imports_statement nodes; emits
  imports edges using the namespace_name child text.
- _vbnet_extra_walk(): extra-walk hook that intercepts namespace_block
  nodes, emits a namespace node, and recurses.
- _VBNET_CONFIG: full LanguageConfig covering class_block / module_block /
  structure_block / interface_block as class types; method_declaration /
  constructor_declaration / property_declaration as function types;
  invocation call nodes with target/member_access fields.
- VB.NET-specific branches in _extract_generic:
  * Class body: VB.NET has no wrapper body node; inherits and implements
    are named fields directly on the class_block. Emits separate inherits
    and implements edges for each base type, stripping generic arguments.
  * Constructor name: constructor_declaration carries no name field in
    the grammar; always resolves to New.
  * Function body: uses the declaration node itself as body sentinel so
    the call-graph pass can find invocations inside methods.
- extract_vbnet(path): public wrapper that delegates to _extract_generic.
- _DISPATCH['.vb']: routes .vb files to extract_vbnet.

pyproject.toml:
- Added vbnet = ['tree-sitter-vbnet'] optional dependency group.
- Added 'tree-sitter-vbnet' to the all extra.

tests/fixtures/sample.vb:
- New fixture file exercising: Imports statements, Namespace block,
  Interface, Class with Inherits + Implements, Module, Structure,
  Sub/Function/Property methods, and method calls.

tests/test_languages.py:
- Added 13 tests covering: no-error, class/interface/module/structure
  detection, method detection, imports relation, inherits edge,
  implements edge, and no-dangling-edges invariant.

README.md:
- Updated language count 25 to 26.
- Added VB.NET to language list and file-extension table.
2026-05-01 21:20:36 +03:00

634 lines
23 KiB
Python

"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia, VB.NET."""
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_vbnet,
)
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"
}
# ── 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_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"
# ── 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)
# ── 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_inherits_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"] == "inherits"
)
assert found, "DataProcessor should have inherits edge to IProcessor"
# ── 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))
# ── 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)
# ── 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_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_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)
# ── VB.NET ─────────────────────────────────────────────────────────────────────────
def test_vbnet_no_error():
r = extract_vbnet(FIXTURES / "sample.vb")
assert "error" not in r
def test_vbnet_finds_class():
r = extract_vbnet(FIXTURES / "sample.vb")
assert any("DataProcessor" in l for l in _labels(r))
def test_vbnet_finds_interface():
r = extract_vbnet(FIXTURES / "sample.vb")
assert any("IProcessor" in l for l in _labels(r))
def test_vbnet_finds_module():
r = extract_vbnet(FIXTURES / "sample.vb")
assert any("AppHelper" in l for l in _labels(r))
def test_vbnet_finds_structure():
r = extract_vbnet(FIXTURES / "sample.vb")
assert any("Point" in l for l in _labels(r))
def test_vbnet_finds_methods():
r = extract_vbnet(FIXTURES / "sample.vb")
labels = _labels(r)
assert any("Process" in l for l in labels)
assert any("Validate" in l for l in labels)
def test_vbnet_finds_sub():
r = extract_vbnet(FIXTURES / "sample.vb")
assert any("Run" in l for l in _labels(r))
def test_vbnet_finds_imports():
r = extract_vbnet(FIXTURES / "sample.vb")
assert "imports" in _relations(r)
def test_vbnet_inherits_edge():
r = extract_vbnet(FIXTURES / "sample.vb")
inherits = [e for e in r["edges"] if e["relation"] == "inherits"]
assert len(inherits) >= 1
def test_vbnet_inherits_baseprovessor():
r = extract_vbnet(FIXTURES / "sample.vb")
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
found = any(
"DataProcessor" in node_by_id.get(e["source"], "") and
"BaseProcessor" in node_by_id.get(e["target"], "")
for e in r["edges"] if e["relation"] == "inherits"
)
assert found, "DataProcessor should have inherits edge to BaseProcessor"
def test_vbnet_implements_edge():
r = extract_vbnet(FIXTURES / "sample.vb")
implements = [e for e in r["edges"] if e["relation"] == "implements"]
assert len(implements) >= 1
def test_vbnet_implements_iprocessor():
r = extract_vbnet(FIXTURES / "sample.vb")
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_vbnet_no_dangling_edges():
r = extract_vbnet(FIXTURES / "sample.vb")
node_ids = {n["id"] for n in r["nodes"]}
for e in r["edges"]:
assert e["source"] in node_ids
# ── 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_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_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_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_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_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}"