feat(extract): WPF/XAML structural extraction with code-behind bridge (#1460)

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>
This commit is contained in:
Michael Katsoulakis
2026-06-25 13:00:47 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 68dba89a99
commit 7dc5d968a3
8 changed files with 410 additions and 9 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- Feat: extract WPF/XAML structure from `.xaml` files (#1460, thanks @MikeKatsoulakis). No new parser dependency (stdlib XML, with the same DOCTYPE/ENTITY and size guards as the `.csproj` extractor). Captures the root element, named controls (`x:Name`/`Name`) and their control types, `{Binding ...}` references, and `x:Class`, and bridges the view to its `.xaml.cs` code-behind by resolving event-handler attributes to the matching methods on the partial class. Event resolution is gated on the .NET handler signature `(object sender, …EventArgs e)` and skips free-form attributes (`Content`, `Text`, `Tag`, …), so a property value that merely matches a method name (e.g. `Content="Save"` next to a business method `Save()`) can't fabricate a spurious event edge.
- Fix: `to_canvas` (Obsidian Canvas export) now lays out each community's node cards in the same `ceil(sqrt(n))`-column grid the group box is sized for. The box width assumed a roughly-square `sqrt(n)`-column layout, but the placement loop hardcoded 3 columns, so any community larger than ~9 members rendered as a cramped 3-wide strip in an over-wide, mostly-empty box. The column count is now computed once per community and reused for the box width, box height, and card placement, so the cards fill the box. Cosmetic, no data change (#1452, thanks @TPAteeq).
- Fix: `to_obsidian` / `to_canvas` / `to_wiki` no longer silently overwrite notes whose labels differ only by case (e.g. a class `References` and a prose heading `references`). The filename dedup was keyed on the exact-case name, so two such labels counted as non-colliding and the second write clobbered the first on case-insensitive filesystems (macOS/APFS, Windows/NTFS) — no suffix, no warning. Dedup now folds case (keyed on the lowercased name) while still emitting the original-case filename, so any pair that would collide on disk gets a numeric suffix. The obsidian/canvas dedup is shared in one helper so they can't drift, `wiki`'s slug dedup gets the matching fix, the `_COMMUNITY_*.md` overview notes (which had no dedup) are covered, and a generated `base_1` is itself re-checked so it can't overwrite a node literally labelled `base_1` (#1453, thanks @TPAteeq).
- Feat: the `kimi`, `gemini`, and `deepseek` semantic-extraction backends now honor `KIMI_BASE_URL`, `GEMINI_BASE_URL`, and `DEEPSEEK_BASE_URL` to point at any OpenAI-compatible endpoint (a proxy, gateway, or self-hosted relay), matching the existing `OLLAMA_BASE_URL` / `OPENAI_BASE_URL` overrides. Each falls back to its hardcoded official default when the variable is unset, so behavior is unchanged for everyone who doesn't set it (#1458, thanks @jc2shile).
+1 -1
View File
@@ -242,7 +242,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg
| Type | Extensions |
|------|-----------|
| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` reuse the C++ grammar) |
| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` reuse the C++ grammar) |
| Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) |
| Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) |
| MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements |
+1 -1
View File
@@ -27,7 +27,7 @@ class FileType(str, Enum):
_MANIFEST_PATH = str(out_path("manifest.json"))
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.razor', '.cshtml', '.cls', '.trigger'}
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
+262
View File
@@ -11036,6 +11036,267 @@ def extract_csproj(path: Path) -> dict:
return {"nodes": nodes, "edges": edges}
def _xml_local_name(name: str) -> str:
return name.rsplit("}", 1)[-1] if name.startswith("{") else name
# A .NET event handler has the signature `(object sender, <T>EventArgs e)`. Used
# to tell a real event handler in the code-behind apart from an ordinary method
# whose name a XAML attribute value happens to match. Tolerates `object?`, a
# namespace-qualified args type, and a generic `EventArgs<T>`.
_EVENT_HANDLER_SIGNATURE_RE = re.compile(
r"\(\s*object\??\s+\w+\s*,\s*[\w.]*EventArgs(?:<[^>]*>)?\s+\w+\s*\)"
)
# XAML attribute names that carry free-form strings or identifiers and never name
# an event handler. They are skipped when matching attribute values to code-behind
# methods so e.g. Content="Save" or Tag="Refresh" can't fabricate an event edge.
_XAML_NON_EVENT_ATTRS = frozenset({
"Name", "Content", "Text", "Title", "Tag", "ToolTip", "Header",
"Class", "Key", "Uid", "DataContext", "Style", "Source",
})
# A handler attribute value is a bare method name (e.g. Click="Save_Click"), not
# markup, a path, or a sentence. Used to skip values like "{Binding ...}" or
# free-form content before looking them up as code-behind methods.
_XAML_IDENT_RE = re.compile(r"[A-Za-z_]\w*")
def _xaml_codebehind_path(path: Path) -> Path | None:
expected = path.with_suffix(path.suffix + ".cs")
if expected.exists():
return expected
try:
for sibling in path.parent.iterdir():
if sibling.name.casefold() == expected.name.casefold():
return sibling
except OSError:
return None
return None
def _xaml_codebehind_symbols(
path: Path,
class_name: str | None,
) -> tuple[dict | None, dict[str, dict], list[dict]]:
codebehind = _xaml_codebehind_path(path)
if not codebehind:
return None, {}, []
result = extract_csharp(codebehind)
if result.get("error"):
return None, {}, []
class_simple = class_name.rsplit(".", 1)[-1] if class_name else None
class_node = None
if class_simple:
for node in result.get("nodes", []):
if node.get("label") == class_simple:
class_node = node
break
class_method_edges: list[dict] = []
if class_node:
class_id = class_node.get("id")
for edge in result.get("edges", []):
if edge.get("source") == class_id and edge.get("relation") == "method":
class_method_edges.append(edge)
method_ids = {edge.get("target") for edge in class_method_edges} if class_node else None
# Only methods with a .NET event-handler signature -- (object sender,
# <T>EventArgs e) -- are eligible to be wired to a XAML attribute as an
# event. Without this gate, any attribute whose value happens to match a
# method name (e.g. Content="Save" next to a business method Save()) would
# produce a spurious "event" edge. The C# extractor does not record the
# parameter list on method nodes, so we read it from the code-behind source
# at the method's recorded line.
try:
cb_lines = codebehind.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
cb_lines = []
def _has_event_handler_signature(node: dict) -> bool:
loc = str(node.get("source_location") or "")
m = re.match(r"L(\d+)", loc)
if not m or not cb_lines:
return False
start = int(m.group(1)) - 1
# Join a few lines so a signature split across lines still matches.
snippet = " ".join(cb_lines[start:start + 3])
return _EVENT_HANDLER_SIGNATURE_RE.search(snippet) is not None
methods: dict[str, dict] = {}
for node in result.get("nodes", []):
if method_ids is not None and node.get("id") not in method_ids:
continue
label = str(node.get("label", ""))
if label.startswith(".") and label.endswith("()") and _has_event_handler_signature(node):
methods[label.strip("()").lstrip(".")] = node
return class_node, methods, class_method_edges
def extract_xaml(path: Path) -> dict:
"""Extract WPF/XAML structure, bindings, x:Class, and event handler references."""
import xml.etree.ElementTree as ET
try:
src = path.read_bytes()
except OSError:
return {"nodes": [], "edges": [], "error": f"cannot read {path}"}
if len(src) > _PROJECT_XML_MAX_BYTES:
return {"nodes": [], "edges": [], "error": "xaml file too large"}
if not _project_xml_is_safe(src):
return {"nodes": [], "edges": [],
"error": "refusing XML with DOCTYPE/ENTITY declaration"}
try:
tree = ET.fromstring(src)
except ET.ParseError as e:
return {"nodes": [], "edges": [], "error": f"XML parse error: {e}"}
text = src.decode("utf-8", errors="replace")
lines = text.splitlines()
str_path = str(path)
stem = _file_stem(path)
file_nid = _make_id(str(path))
root_type = _xml_local_name(tree.tag)
root_nid = _make_id(stem, root_type)
nodes: list[dict] = []
edges: list[dict] = []
seen_ids: set[str] = set()
seen_edges: set[tuple[str, str, str, str | None]] = set()
def line_for(value: str | None) -> int:
if value:
for idx, line in enumerate(lines, 1):
if value in line:
return idx
return 1
def add_node(
nid: str,
label: str,
line: int | None,
*,
file_type: str = "code",
source_file: str = str_path,
) -> None:
if nid in seen_ids:
return
seen_ids.add(nid)
nodes.append({
"id": nid, "label": label, "file_type": file_type,
"source_file": source_file,
"source_location": f"L{line}" if line else None,
})
def add_existing_node(node: dict | None) -> None:
if not node:
return
nid = node.get("id")
if not nid or nid in seen_ids:
return
seen_ids.add(nid)
nodes.append(dict(node))
def add_edge(
src_nid: str,
tgt_nid: str,
relation: str,
line: int,
*,
context: str | None = None,
source_file: str = str_path,
) -> None:
key = (src_nid, tgt_nid, relation, context)
if key in seen_edges:
return
seen_edges.add(key)
edge = {
"source": src_nid, "target": tgt_nid, "relation": relation,
"confidence": "EXTRACTED", "source_file": source_file,
"source_location": f"L{line}", "weight": 1.0,
}
if context:
edge["context"] = context
edges.append(edge)
def add_existing_edge(edge: dict) -> None:
key = (edge.get("source"), edge.get("target"), edge.get("relation"), edge.get("context"))
if key in seen_edges:
return
seen_edges.add(key)
edges.append(dict(edge))
add_node(file_nid, path.name, 1)
add_node(root_nid, root_type, 1)
add_edge(file_nid, root_nid, "contains", 1)
class_name = None
for key, value in tree.attrib.items():
if _xml_local_name(key) == "Class" and value:
class_name = value.strip()
break
class_node, codebehind_methods, class_method_edges = _xaml_codebehind_symbols(path, class_name)
if class_name:
if class_node:
class_nid = class_node["id"]
add_existing_node(class_node)
else:
class_label = class_name.rsplit(".", 1)[-1]
class_nid = _make_id(stem, class_label)
add_node(class_nid, class_label, line_for(class_name))
add_edge(root_nid, class_nid, "references", line_for(class_name), context="x_class")
binding_re = re.compile(r"\{Binding\s+([^,}\s]+)")
for elem in tree.iter():
elem_type = _xml_local_name(elem.tag)
elem_name = None
for key, value in elem.attrib.items():
if _xml_local_name(key) == "Name" and value:
elem_name = value.strip()
break
owner_nid = root_nid
if elem_name:
owner_nid = _make_id(stem, elem_name)
add_node(owner_nid, elem_name, line_for(elem_name))
add_edge(root_nid, owner_nid, "contains", line_for(elem_name))
type_nid = _make_id("xaml", elem_type)
add_node(type_nid, elem_type, line_for(elem_name), file_type="concept")
add_edge(owner_nid, type_nid, "references", line_for(elem_name), context="type")
for key, value in elem.attrib.items():
value = value or ""
# Event wiring: an attribute references a handler only when its local
# name isn't a known free-form/identity property, its value is a bare
# identifier (a method name, not markup or a sentence), and the matched
# code-behind method actually has an event-handler signature (the gate
# in _xaml_codebehind_symbols). This stops Content="Save" / Tag="..."
# from fabricating event edges against same-named ordinary methods.
attr_local = _xml_local_name(key)
if attr_local not in _XAML_NON_EVENT_ATTRS and _XAML_IDENT_RE.fullmatch(value):
method = codebehind_methods.get(value)
if method:
add_existing_node(method)
add_edge(owner_nid, method["id"], "references", line_for(value), context="event")
for method_edge in class_method_edges:
if method_edge.get("target") == method["id"]:
add_existing_node(class_node)
add_existing_edge(method_edge)
break
for match in binding_re.finditer(value):
binding = match.group(1).strip()
if not binding:
continue
bind_nid = _make_id("binding", binding)
add_node(bind_nid, binding, line_for(value), file_type="concept")
add_edge(owner_nid, bind_nid, "references", line_for(value), context="binding")
return {"nodes": nodes, "edges": edges}
# Config/manifest JSON filenames the structural extractor understands. Anything
@@ -12018,6 +12279,7 @@ _DISPATCH: dict[str, Any] = {
".csproj": extract_csproj,
".fsproj": extract_csproj,
".vbproj": extract_csproj,
".xaml": extract_xaml,
".razor": extract_razor,
".cshtml": extract_razor,
".cls": extract_apex,
+10
View File
@@ -0,0 +1,10 @@
<Window x:Class="GraphifyDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Dashboard"
Loaded="Window_Loaded">
<StackPanel x:Name="RootPanel">
<TextBox Name="UserNameBox" Text="{Binding UserName}" TextChanged="UserNameChanged" />
<Button x:Name="SaveButton" Content="Save" Click="Save_Click" />
</StackPanel>
</Window>
+19
View File
@@ -0,0 +1,19 @@
using System.Windows;
namespace GraphifyDemo
{
public partial class MainWindow : Window
{
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
private void UserNameChanged(object sender, RoutedEventArgs e)
{
}
private void Save_Click(object sender, RoutedEventArgs e)
{
}
}
}
+107 -4
View File
@@ -1,8 +1,8 @@
"""Tests for .NET project file extraction (.sln, .csproj, .razor)."""
"""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_razor
from graphify.extract import extract_sln, extract_slnx, extract_csproj, extract_xaml, extract_razor
FIXTURES = Path(__file__).parent / "fixtures"
@@ -107,6 +107,109 @@ def test_csproj_invalid_xml():
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():
@@ -150,11 +253,11 @@ def test_razor_missing_file():
def test_dispatch_table():
from graphify.extract import _get_extractor
for ext in (".sln", ".slnx", ".csproj", ".fsproj", ".vbproj", ".razor", ".cshtml"):
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", ".razor", ".cshtml"):
for ext in (".sln", ".slnx", ".csproj", ".fsproj", ".vbproj", ".xaml", ".razor", ".cshtml"):
assert ext in CODE_EXTENSIONS, f"{ext} missing"
+9 -3
View File
@@ -1,4 +1,4 @@
"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia, Fortran, JS/TS, .NET project files."""
"""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
@@ -6,7 +6,7 @@ from graphify.extract import (
extract_java, extract_c, extract_cpp, extract_ruby,
extract_csharp, extract_kotlin, extract_scala, extract_php,
extract_swift, extract_go, extract_julia, extract_js, extract_fortran,
extract_groovy, extract_sln, extract_csproj, extract_razor,
extract_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,
@@ -1755,7 +1755,7 @@ def test_dmf_no_dangling_edges():
assert e["target"] in node_ids
# -- .NET project files (.sln, .csproj, .razor) -------------------------------
# -- .NET project files (.sln, .csproj, .xaml, .razor) ------------------------
def test_sln_no_error():
r = extract_sln(FIXTURES / "sample.sln")
@@ -1798,6 +1798,12 @@ 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