mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-20 22:31:48 +00:00
Makes .xaml a first-class code input. extract_xaml() uses stdlib XML (no new
parser dependency) behind the same DOCTYPE/ENTITY and size guards as the .csproj
extractor, and captures: the root element, named controls (x:Name/Name) and their
control types, {Binding ...} references, x:Class, and -- the useful part -- a
bridge from the view markup to its .xaml.cs code-behind by resolving event-handler
attributes to the matching methods on the partial class.
Ported from PR #1460 by @MikeKatsoulakis onto current v8.
Maintainer hardening on top of the original PR: event resolution is now gated so
it can't fabricate edges. The original matched any attribute value against
code-behind method names, so Content="Save" next to a business method Save(), or
Tag="<a-handler-name>", produced spurious "event" edges. Resolution now requires
(a) the attribute is not a known free-form/identity property (Content, Text, Tag,
Title, ToolTip, Header, ...), (b) the value is a bare identifier, and (c) the
matched method actually has the .NET event-handler signature
(object sender, <T>EventArgs e) -- read from the code-behind source since the C#
extractor does not record parameter lists on method nodes. Added regression tests
for both false-positive cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
264 lines
9.5 KiB
Python
264 lines
9.5 KiB
Python
"""Tests for .NET project file extraction (.sln, .csproj, .xaml, .razor)."""
|
|
from pathlib import Path
|
|
import tempfile
|
|
import pytest
|
|
from graphify.extract import extract_sln, extract_slnx, extract_csproj, extract_xaml, 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"]}
|
|
|
|
|
|
# ── .sln ─────────────────────────────────────────────────────────────────────
|
|
|
|
def test_sln_extracts_projects():
|
|
r = extract_sln(FIXTURES / "sample.sln")
|
|
assert "error" not in r
|
|
labels = set(_labels(r))
|
|
assert "WebApi" in labels
|
|
assert "Domain" in labels
|
|
assert "Tests" in labels
|
|
|
|
|
|
def test_sln_contains_edges():
|
|
r = extract_sln(FIXTURES / "sample.sln")
|
|
contains = [e for e in r["edges"] if e["relation"] == "contains"]
|
|
assert len(contains) == 3
|
|
|
|
|
|
def test_sln_project_dependency():
|
|
r = extract_sln(FIXTURES / "sample.sln")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
# ── .slnx ────────────────────────────────────────────────────────────────────
|
|
|
|
def test_slnx_extracts_projects():
|
|
r = extract_slnx(FIXTURES / "sample.slnx")
|
|
assert "error" not in r
|
|
labels = set(_labels(r))
|
|
assert "WebApi" in labels
|
|
assert "Domain" in labels
|
|
assert "Tests" in labels
|
|
|
|
|
|
def test_slnx_contains_edges():
|
|
r = extract_slnx(FIXTURES / "sample.slnx")
|
|
contains = [e for e in r["edges"] if e["relation"] == "contains"]
|
|
assert len(contains) == 3
|
|
|
|
|
|
def test_slnx_project_dependency():
|
|
r = extract_slnx(FIXTURES / "sample.slnx")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_slnx_invalid_xml():
|
|
with tempfile.NamedTemporaryFile(suffix=".slnx", mode="w", delete=False) as f:
|
|
f.write("<Solution><Project></Solution>")
|
|
f.flush()
|
|
r = extract_slnx(Path(f.name))
|
|
assert "error" in r
|
|
|
|
|
|
def test_slnx_missing_file():
|
|
r = extract_slnx(Path("/nonexistent/file.slnx"))
|
|
assert "error" in r
|
|
|
|
|
|
# ── .csproj ──────────────────────────────────────────────────────────────────
|
|
|
|
def test_csproj_packages():
|
|
r = extract_csproj(FIXTURES / "sample.csproj")
|
|
assert "error" not in r
|
|
labels = _labels(r)
|
|
assert any("MediatR" in l for l in labels)
|
|
assert any("FluentValidation" in l for l in labels)
|
|
assert any("Swashbuckle" in l for l in labels)
|
|
|
|
|
|
def test_csproj_project_references():
|
|
r = extract_csproj(FIXTURES / "sample.csproj")
|
|
imports = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert len(imports) == 6 # 4 packages + 2 project refs
|
|
|
|
|
|
def test_csproj_target_framework():
|
|
r = extract_csproj(FIXTURES / "sample.csproj")
|
|
assert "net8.0" in _labels(r)
|
|
|
|
|
|
def test_csproj_sdk():
|
|
r = extract_csproj(FIXTURES / "sample.csproj")
|
|
assert "Microsoft.NET.Sdk.Web" in _labels(r)
|
|
|
|
|
|
def test_csproj_invalid_xml():
|
|
with tempfile.NamedTemporaryFile(suffix=".csproj", mode="w", delete=False) as f:
|
|
f.write("<Project><Invalid></Project>")
|
|
f.flush()
|
|
r = extract_csproj(Path(f.name))
|
|
assert "error" in r
|
|
|
|
|
|
# ── .xaml ────────────────────────────────────────────────────────────────────
|
|
|
|
def test_xaml_class_resolves_to_codebehind_partial_class():
|
|
r = extract_xaml(FIXTURES / "sample.xaml")
|
|
assert "error" not in r
|
|
class_nodes = [
|
|
n for n in r["nodes"]
|
|
if n["label"] == "MainWindow" and str(n.get("source_file", "")).endswith("sample.xaml.cs")
|
|
]
|
|
assert class_nodes
|
|
assert any(
|
|
e["relation"] == "references"
|
|
and e.get("context") == "x_class"
|
|
and e["target"] == class_nodes[0]["id"]
|
|
for e in r["edges"]
|
|
)
|
|
|
|
|
|
def test_xaml_named_controls_and_bindings():
|
|
r = extract_xaml(FIXTURES / "sample.xaml")
|
|
labels = set(_labels(r))
|
|
assert {"RootPanel", "UserNameBox", "SaveButton", "UserName"} <= labels
|
|
assert any(e["relation"] == "references" and e.get("context") == "binding" for e in r["edges"])
|
|
|
|
|
|
def test_xaml_events_resolve_to_codebehind_methods():
|
|
r = extract_xaml(FIXTURES / "sample.xaml")
|
|
method_nodes = {
|
|
n["label"].strip("()").lstrip("."): n["id"]
|
|
for n in r["nodes"]
|
|
if str(n.get("source_file", "")).endswith("sample.xaml.cs")
|
|
}
|
|
assert {"Window_Loaded", "UserNameChanged", "Save_Click"} <= set(method_nodes)
|
|
event_targets = {
|
|
e["target"] for e in r["edges"]
|
|
if e["relation"] == "references" and e.get("context") == "event"
|
|
}
|
|
assert method_nodes["Window_Loaded"] in event_targets
|
|
assert method_nodes["UserNameChanged"] in event_targets
|
|
assert method_nodes["Save_Click"] in event_targets
|
|
|
|
|
|
def _event_targets(r):
|
|
return {e["target"] for e in r["edges"]
|
|
if e["relation"] == "references" and e.get("context") == "event"}
|
|
|
|
|
|
def test_xaml_event_match_requires_handler_signature():
|
|
"""A property value that matches an ordinary method's name must not become an
|
|
event edge -- only methods with a (object sender, ...EventArgs e) signature do."""
|
|
xaml = (
|
|
'<Window x:Class="Demo.MainWindow"\n'
|
|
' xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n'
|
|
' xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">\n'
|
|
' <Button Content="Refresh" Click="Refresh"/>\n'
|
|
"</Window>\n"
|
|
)
|
|
cs = (
|
|
"using System.Windows;\n"
|
|
"namespace Demo { public partial class MainWindow : Window {\n"
|
|
" public void Refresh() {}\n" # business method, not a handler signature
|
|
"}}\n"
|
|
)
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p = Path(d) / "view.xaml"
|
|
p.write_text(xaml)
|
|
(Path(d) / "view.xaml.cs").write_text(cs)
|
|
r = extract_xaml(p)
|
|
assert "error" not in r
|
|
assert _event_targets(r) == set()
|
|
|
|
|
|
def test_xaml_non_event_attribute_value_does_not_fabricate_event():
|
|
"""Content=/Tag= holding a string that equals a real handler's name must not
|
|
create an event edge; only the genuine event attribute (Click) should."""
|
|
xaml = (
|
|
'<Window x:Class="Demo.MainWindow"\n'
|
|
' xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n'
|
|
' xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">\n'
|
|
' <Button x:Name="B1" Content="Save_Click" Tag="OnLoaded" Click="Save_Click"/>\n'
|
|
"</Window>\n"
|
|
)
|
|
cs = (
|
|
"using System.Windows;\n"
|
|
"namespace Demo { public partial class MainWindow : Window {\n"
|
|
" private void Save_Click(object sender, RoutedEventArgs e) {}\n"
|
|
" private void OnLoaded(object sender, RoutedEventArgs e) {}\n"
|
|
"}}\n"
|
|
)
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p = Path(d) / "view.xaml"
|
|
p.write_text(xaml)
|
|
(Path(d) / "view.xaml.cs").write_text(cs)
|
|
r = extract_xaml(p)
|
|
handlers = {n["label"].strip("()").lstrip("."): n["id"]
|
|
for n in r["nodes"] if str(n.get("source_file", "")).endswith("view.xaml.cs")}
|
|
targets = _event_targets(r)
|
|
# Click -> Save_Click is the only real event; OnLoaded (referenced only via Tag) is not.
|
|
assert handlers["Save_Click"] in targets
|
|
assert handlers.get("OnLoaded") not in targets
|
|
assert len(targets) == 1
|
|
|
|
|
|
# ── .razor ───────────────────────────────────────────────────────────────────
|
|
|
|
def test_razor_using_and_inject():
|
|
r = extract_razor(FIXTURES / "sample.razor")
|
|
assert "error" not in r
|
|
targets = {e["target"] for e in r["edges"] if e["relation"] == "imports"}
|
|
assert any("microsoft" in t for t in targets)
|
|
assert any("counterservice" in t.lower() for t in targets)
|
|
|
|
|
|
def test_razor_components():
|
|
r = extract_razor(FIXTURES / "sample.razor")
|
|
targets = {e["target"] for e in r["edges"] if e["relation"] == "calls"}
|
|
assert any("weatherdisplay" in t for t in targets)
|
|
assert any("datagrid" in t for t in targets)
|
|
|
|
|
|
def test_razor_page_route():
|
|
r = extract_razor(FIXTURES / "sample.razor")
|
|
assert any("/counter" in l for l in _labels(r))
|
|
|
|
|
|
def test_razor_inherits():
|
|
r = extract_razor(FIXTURES / "sample.razor")
|
|
assert "inherits" in _relations(r)
|
|
|
|
|
|
def test_razor_code_methods():
|
|
r = extract_razor(FIXTURES / "sample.razor")
|
|
labels = _labels(r)
|
|
assert "IncrementCount" in labels
|
|
assert "LoadData" in labels
|
|
|
|
|
|
def test_razor_missing_file():
|
|
r = extract_razor(Path("/nonexistent/file.razor"))
|
|
assert "error" in r
|
|
|
|
|
|
# ── dispatch & detect integration ────────────────────────────────────────────
|
|
|
|
def test_dispatch_table():
|
|
from graphify.extract import _get_extractor
|
|
for ext in (".sln", ".slnx", ".csproj", ".fsproj", ".vbproj", ".xaml", ".razor", ".cshtml"):
|
|
assert _get_extractor(Path(f"foo{ext}")) is not None, f"{ext} not in dispatch"
|
|
|
|
|
|
def test_code_extensions():
|
|
from graphify.detect import CODE_EXTENSIONS
|
|
for ext in (".sln", ".slnx", ".csproj", ".fsproj", ".vbproj", ".xaml", ".razor", ".cshtml"):
|
|
assert ext in CODE_EXTENSIONS, f"{ext} missing"
|