feat: add VB.NET language extractor

(cherry picked from commit 304c82fe10)
This commit is contained in:
Abdullah authored and safishamsi committed 2026-09-21 19:06:50 +01:00
1 parent ab568c8f42
commit ebe1f32c81
9 files changed
+593 -5

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ _LANG_FAMILY: dict[str, str] = {
**{e: "c" for e in (".c", ".h", ".cpp", ".cc", ".cxx", ".hpp")},
**{e: "ruby" for e in (".rb", ".rake")},
**{e: "swift" for e in (".swift",)},
**{e: "dotnet" for e in (".cs",)},
**{e: "dotnet" for e in (".cs", ".vb")},
**{e: "php" for e in (".php",)},
**{e: "r" for e in (".r",)},
**{e: "cobol" for e in (".cbl", ".cob", ".cobol", ".cpy")},
+1 -1
View File
@@ -41,7 +41,7 @@ _MANIFEST_PATH = str(out_path("manifest.json"))
_MTIME_COARSE_S = 2.0
_MTIME_SUBSECOND_S = 0.05
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.cbl', '.cob', '.cobol', '.cpy', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.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', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource'}
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.vb', '.cbl', '.cob', '.cobol', '.cpy', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.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', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
+8 -1
View File
@@ -61,6 +61,7 @@ from graphify.extractors.sln import extract_sln # noqa: F401
from graphify.extractors.sql import extract_sql # noqa: F401
from graphify.extractors.terraform import extract_terraform, prepare_terraform, resolve_terraform_modules # noqa: F401
from graphify.extractors.verilog import extract_verilog # noqa: F401
from graphify.extractors.vbnet import extract_vbnet, resolve_vbnet_partial_calls # noqa: F401
from graphify.extractors.zig import extract_zig # noqa: F401
from graphify.security import sanitize_metadata
from graphify.paths import disambiguate_ambiguous_candidates
@@ -2730,6 +2731,7 @@ _CASE_INSENSITIVE_EXTS = frozenset({
".sql", # SQL identifiers
".nim", ".nims", ".nimble", # Nim (style-insensitive)
".cbl", ".cob", ".cobol", ".cpy",
".vb",
})
@@ -2769,7 +2771,7 @@ _LANG_FAMILY_BY_EXT: dict[str, str] = {
".rb": "ruby", ".rake": "ruby",
".php": "php", ".phtml": "php", ".php3": "php", ".php4": "php",
".php5": "php", ".php7": "php", ".phps": "php",
".cs": "dotnet", ".razor": "dotnet", ".cshtml": "dotnet", ".xaml": "dotnet",
".cs": "dotnet", ".vb": "dotnet", ".razor": "dotnet", ".cshtml": "dotnet", ".xaml": "dotnet",
".lua": "lua", ".luau": "lua",
".zig": "zig",
".ex": "elixir", ".exs": "elixir",
@@ -5197,6 +5199,9 @@ register_language_resolver(
register_language_resolver(
LanguageResolver("rust_self_member_calls", frozenset({".rs"}), _resolve_rust_self_member_calls)
)
register_language_resolver(
LanguageResolver("vbnet_partial_calls", frozenset({".vb"}), resolve_vbnet_partial_calls)
)
register_language_resolver(
LanguageResolver(
"elixir_import_targets",
@@ -6300,6 +6305,7 @@ _DISPATCH: dict[str, Any] = {
".cob": extract_cobol,
".cobol": extract_cobol,
".cpy": extract_cobol,
".vb": extract_vbnet,
".kt": extract_kotlin,
".kts": extract_kotlin,
".scala": extract_scala,
@@ -6385,6 +6391,7 @@ _DISPATCH: dict[str, Any] = {
# rather than falling back like Pascal does. Used by the #1745 warning in
# extract() to tell the user which extra restores the language.
_EXTRA_FOR_EXTENSION = {
".vb": "vbnet",
".sql": "sql",
".tf": "terraform",
".tfvars": "terraform",
+2
View File
@@ -33,6 +33,7 @@ from graphify.extractors.sln import extract_sln
from graphify.extractors.sql import extract_sql
from graphify.extractors.terraform import extract_terraform
from graphify.extractors.verilog import extract_verilog
from graphify.extractors.vbnet import extract_vbnet
from graphify.extractors.zig import extract_zig
LANGUAGE_EXTRACTORS: dict[str, Callable[[Path], dict]] = {
@@ -64,5 +65,6 @@ LANGUAGE_EXTRACTORS: dict[str, Callable[[Path], dict]] = {
"sql": extract_sql,
"terraform": extract_terraform,
"verilog": extract_verilog,
"vbnet": extract_vbnet,
"zig": extract_zig,
}
+409
View File
@@ -0,0 +1,409 @@
"""Structural extraction for Visual Basic .NET source files."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from tree_sitter import Node
from graphify.extractors.base import _file_stem, _make_id, _read_text
_TYPE_BLOCKS = {
"class_block": "class",
"module_block": "module",
"interface_block": "interface",
"structure_block": "structure",
"enum_block": "enum",
}
def resolve_vbnet_partial_calls(
per_file: list[dict], all_nodes: list[dict], all_edges: list[dict]
) -> None:
"""Resolve calls across files that declare the same partial VB type."""
methods: dict[tuple[str, str, int], list[str]] = {}
for node in all_nodes:
metadata = node.get("metadata")
if not isinstance(metadata, dict) or metadata.get("language") != "vbnet":
continue
if metadata.get("kind") not in {"method", "constructor"}:
continue
owner = metadata.get("owner")
name = metadata.get("name")
accepted_arities = metadata.get("accepted_arities")
if (
isinstance(owner, str)
and isinstance(name, str)
and isinstance(accepted_arities, list)
):
for arity in accepted_arities:
if isinstance(arity, int):
methods.setdefault(
(owner.casefold(), name.casefold(), arity), []
).append(node["id"])
existing = {
(edge.get("source"), edge.get("target"))
for edge in all_edges
if edge.get("relation") == "calls"
}
for result in per_file:
for call in result.get("raw_calls", []):
if call.get("language") != "vbnet" or not call.get("owner"):
continue
key = (
str(call["owner"]).casefold(),
str(call.get("callee", "")).casefold(),
int(call.get("arity", -1)),
)
candidates = methods.get(key, [])
caller = call.get("caller_nid")
if len(candidates) != 1 or candidates[0] == caller:
continue
pair = (caller, candidates[0])
if pair in existing:
continue
existing.add(pair)
all_edges.append({
"source": caller,
"target": candidates[0],
"relation": "calls",
"context": "partial_type_call",
"confidence": "EXTRACTED",
"confidence_score": 1.0,
"source_file": call.get("source_file", ""),
"source_location": call.get("source_location"),
"weight": 1.0,
})
def extract_vbnet(path: Path) -> dict:
try:
import tree_sitter_vb_dotnet
from tree_sitter import Language, Parser
except ImportError:
return {"nodes": [], "edges": [], "error": "tree-sitter-vb-dotnet not installed"}
try:
source = path.read_bytes()
root = Parser(Language(tree_sitter_vb_dotnet.language())).parse(source).root_node
except Exception as exc:
return {"nodes": [], "edges": [], "error": f"VB.NET grammar failed to load: {exc}"}
source_file = str(path)
stem = _file_stem(path)
file_id = _make_id(source_file)
nodes: list[dict[str, Any]] = []
edges: list[dict[str, Any]] = []
raw_calls: list[dict[str, Any]] = []
seen_ids: set[str] = set()
seen_edges: set[tuple[str, str, str]] = set()
types_by_name: dict[str, str] = {}
methods: dict[tuple[str, str, int], list[str]] = {}
events: dict[tuple[str, str], str] = {}
bodies: list[tuple[Node, str, str, str]] = []
pending_handles: list[tuple[str, str, str, Node]] = []
def add_node(
nid: str,
label: str,
node: Node,
*,
kind: str,
source_backed: bool = True,
callable_node: bool = False,
metadata: dict[str, Any] | None = None,
) -> str:
if nid not in seen_ids:
seen_ids.add(nid)
details: dict[str, Any] = {"language": "vbnet", "kind": kind}
if metadata:
details.update(metadata)
item: dict[str, Any] = {
"id": nid,
"label": label,
"file_type": "code",
"source_location": f"L{node.start_point[0] + 1}",
"metadata": details,
}
if source_backed:
item["source_file"] = source_file
if callable_node:
item["_callable"] = True
nodes.append(item)
return nid
def add_edge(source_id: str, target_id: str, relation: str, node: Node) -> None:
key = (source_id, target_id, relation)
if not source_id or not target_id or source_id == target_id or key in seen_edges:
return
seen_edges.add(key)
edges.append({
"source": source_id,
"target": target_id,
"relation": relation,
"confidence": "EXTRACTED",
"source_file": source_file,
"source_location": f"L{node.start_point[0] + 1}",
"weight": 1.0,
})
add_node(file_id, path.name, root, kind="file")
for statement in (node for node in root.named_children if node.type == "imports_statement"):
namespace_node = next(
(child for child in statement.named_children if child.type == "namespace_name"),
None,
)
if namespace_node is None:
continue
namespace = _read_text(namespace_node, source)
target = add_node(
_make_id("vbnet", "namespace", namespace.casefold()),
namespace,
namespace_node,
kind="external_namespace",
source_backed=False,
)
add_edge(file_id, target, "imports", statement)
def type_reference(name: str, node: Node) -> str:
short_name = name.split(".")[-1]
target = types_by_name.get(short_name.casefold())
if target is not None:
return target
return add_node(
_make_id("vbnet", "type", name.casefold()),
short_name,
node,
kind="external_type",
source_backed=False,
)
def add_data_member(type_id: str, member: Node, name: str, kind: str) -> str:
member_id = add_node(
_make_id(type_id, kind, name.casefold(), str(member.start_point[0])),
name,
member,
kind=kind,
)
add_edge(type_id, member_id, "contains", member)
return member_id
def process_type(block: Node, parent_id: str, namespace: str) -> None:
kind = _TYPE_BLOCKS[block.type]
name_node = block.child_by_field_name("name")
if name_node is None:
return
name = _read_text(name_node, source)
full_name = f"{namespace}.{name}" if namespace else name
owner_key = full_name.casefold()
type_id = add_node(
_make_id(stem, "type", owner_key),
name,
block,
kind=kind,
callable_node=kind in {"class", "structure"},
metadata={"name": name, "full_name": full_name},
)
add_edge(parent_id, type_id, "contains", block)
types_by_name[name.casefold()] = type_id
for clause in block.named_children:
if clause.type not in {"inherits_clause", "implements_clause"}:
continue
relation = "inherits" if clause.type == "inherits_clause" else "implements"
for type_node in (child for child in clause.named_children if child.type == "type"):
referenced = _read_text(type_node, source)
add_edge(type_id, type_reference(referenced, type_node), relation, clause)
for member in block.named_children:
if member.type == "enum_member":
member_name = member.child_by_field_name("name")
if member_name is not None:
add_data_member(
type_id, member, _read_text(member_name, source), "enum_member"
)
continue
if member.type == "field_declaration":
for declarator in (
child for child in member.named_children
if child.type == "variable_declarator"
):
member_name = declarator.child_by_field_name("name")
if member_name is not None:
add_data_member(
type_id,
declarator,
_read_text(member_name, source),
"field",
)
continue
if member.type in {"property_declaration", "event_declaration"}:
member_name = member.child_by_field_name("name")
if member_name is not None:
data_name = _read_text(member_name, source)
member_id = add_data_member(
type_id,
member,
data_name,
"property" if member.type == "property_declaration" else "event",
)
if member.type == "event_declaration":
events[(owner_key, data_name.casefold())] = member_id
continue
if member.type not in {"method_declaration", "constructor_declaration"}:
continue
if member.type == "constructor_declaration":
method_name = "New"
method_kind = "constructor"
else:
method_name_node = member.child_by_field_name("name")
if method_name_node is None:
continue
method_name = _read_text(method_name_node, source)
method_kind = "method"
parameters = member.child_by_field_name("parameters")
parameter_nodes = (
[child for child in parameters.named_children if child.type == "parameter"]
if parameters is not None
else []
)
arity = len(parameter_nodes)
required_arity = sum(
not _read_text(parameter, source).lstrip().casefold().startswith("optional ")
for parameter in parameter_nodes
)
accepted_arities = list(range(required_arity, arity + 1))
method_id = add_node(
_make_id(
type_id,
method_kind,
method_name.casefold(),
str(arity),
str(member.start_point[0]),
),
f"{method_name}()",
member,
kind=method_kind,
callable_node=True,
metadata={
"owner": owner_key,
"name": method_name,
"arity": arity,
"accepted_arities": accepted_arities,
},
)
add_edge(type_id, method_id, "method", member)
for accepted_arity in accepted_arities:
methods.setdefault(
(owner_key, method_name.casefold(), accepted_arity), []
).append(method_id)
for clause in (
child for child in member.named_children if child.type == "handles_clause"
):
for handled in (
child for child in clause.named_children if child.type == "namespace_name"
):
pending_handles.append((
method_id,
owner_key,
_read_text(handled, source),
handled,
))
bodies.append((member, method_id, owner_key, name))
def scan(node: Node, parent_id: str, namespace: str) -> None:
if node.type == "imports_statement":
return
if node.type == "namespace_block":
name_node = node.child_by_field_name("name")
if name_node is None:
return
local_name = _read_text(name_node, source)
full_namespace = f"{namespace}.{local_name}" if namespace else local_name
namespace_id = add_node(
_make_id(stem, "namespace", full_namespace.casefold()),
local_name,
node,
kind="namespace",
metadata={"full_name": full_namespace},
)
add_edge(parent_id, namespace_id, "contains", node)
for child in node.named_children:
if child != name_node:
scan(child, namespace_id, full_namespace)
return
if node.type in _TYPE_BLOCKS:
process_type(node, parent_id, namespace)
# Nested type declarations are the only child containers that still
# need recursion after processing this type's own members.
for child in node.named_children:
if child.type == "type_declaration":
scan(child, parent_id, namespace)
return
for child in node.named_children:
scan(child, parent_id, namespace)
scan(root, file_id, "")
for method_id, owner_key, written, handled_node in pending_handles:
event_name = written.split(".")[-1]
target = events.get((owner_key, event_name.casefold()))
if target is None:
target = add_node(
_make_id("vbnet", "event", written.casefold()),
written,
handled_node,
kind="external_event",
source_backed=False,
)
add_edge(method_id, target, "handles", handled_node)
for body, caller_id, owner_key, type_name in bodies:
stack = [body]
while stack:
node = stack.pop()
if node != body and node.type in {
"method_declaration", "constructor_declaration", "property_declaration"
}:
continue
if node.type == "invocation":
target_node = node.child_by_field_name("target")
arguments = node.child_by_field_name("arguments")
if target_node is not None:
written = _read_text(target_node, source)
parts = written.split(".")
receiver = ".".join(parts[:-1]).casefold()
callee = parts[-1]
known_receiver = (
not receiver
or receiver in {"me", "myclass", type_name.casefold()}
)
if known_receiver:
arity = len(arguments.named_children) if arguments is not None else 0
candidates = methods.get(
(owner_key, callee.casefold(), arity), []
)
if len(candidates) == 1:
add_edge(caller_id, candidates[0], "calls", node)
else:
raw_calls.append({
"caller_nid": caller_id,
"callee": callee,
"arity": arity,
"owner": owner_key,
"is_member_call": True,
"language": "vbnet",
"source_file": source_file,
"source_location": f"L{node.start_point[0] + 1}",
})
stack.extend(reversed(node.named_children))
clean_edges = [
edge for edge in edges
if edge["source"] in seen_ids and edge["target"] in seen_ids
]
return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
+2 -1
View File
@@ -105,7 +105,8 @@ commonlisp = ["tree-sitter-commonlisp"]
# every platform, no C toolchain; optional because Robot Framework corpora are
# QA-automation specific.
robot = ["robotframework>=4.0"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.16.1", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "graspologic-native>=1.3.1,<2; python_version >= '3.13'", "python-docx", "openpyxl", "psycopg[binary]", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.7.4", "matplotlib", "pillow>=12.3.0", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba; python_version < '3.12'", "jieba-py>=0.46.12,<1; python_version >= '3.12'", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0"]
vbnet = ["tree-sitter-vb-dotnet==0.3.0"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.16.1", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "graspologic-native>=1.3.1,<2; python_version >= '3.13'", "python-docx", "openpyxl", "psycopg[binary]", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.7.4", "matplotlib", "pillow>=12.3.0", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba; python_version < '3.12'", "jieba-py>=0.46.12,<1; python_version >= '3.12'", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0", "tree-sitter-vb-dotnet==0.3.0"]
[project.scripts]
graphify = "graphify.__main__:main"
+10
View File
@@ -0,0 +1,10 @@
Namespace Fixtures
Public Class Sample
Public Sub Run()
Helper()
End Sub
Private Sub Helper()
End Sub
End Class
End Namespace
+142
View File
@@ -0,0 +1,142 @@
"""Extraction coverage for vbnet."""
from __future__ import annotations
import sys
from pathlib import Path
from graphify.extract import extract
FIXTURE = Path(__file__).parent / "fixtures" / "new_languages" / "sample.vb"
def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
labels = {node["id"]: node["label"] for node in result["nodes"]}
return {
(labels.get(edge["source"], edge["source"]), labels.get(edge["target"], edge["target"]))
for edge in result["edges"]
if edge["relation"] == relation
}
def test_vbnet_class_methods_and_case_insensitive_calls(tmp_path):
source = tmp_path / "Counter.vb"
source.write_text(
"Namespace Demo\n"
" Public Class Counter\n"
" Public Sub Run()\n"
" helper()\n"
" End Sub\n"
" Private Sub Helper()\n"
" End Sub\n"
" End Class\n"
"End Namespace\n",
encoding="utf-8",
)
result = extract([source], cache_root=tmp_path)
labels = {node["label"] for node in result["nodes"]}
assert {"Demo", "Counter", "Run()", "Helper()"} <= labels
assert ("Run()", "Helper()") in _edge_labels(result, "calls")
def test_vbnet_types_members_relationships_and_partial_calls(tmp_path):
first = tmp_path / "Counter.vb"
first.write_text(
"Imports System.Text\n"
"Namespace Demo\n"
" Public Interface IWorker\n Sub Run()\n End Interface\n"
" Public Class BaseCounter\n End Class\n"
" Public Partial Class Counter\n"
" Inherits BaseCounter\n Implements IWorker\n"
" Private count As Integer\n"
" Public Event Changed As EventHandler\n"
" Public Property Value As Integer\n"
" Public Sub New()\n End Sub\n"
" Public Sub Run() Implements IWorker.Run Handles Me.Changed\n"
" hElPeR()\n End Sub\n"
" End Class\n"
" Public Structure Pair\n End Structure\n"
" Public Enum State\n OnValue\n OffValue\n End Enum\n"
"End Namespace\n",
encoding="utf-8",
)
second = tmp_path / "Counter.Designer.vb"
second.write_text(
"Namespace Demo\n"
" Partial Public Class Counter\n"
" Private Sub Helper(Of T)(\n"
" Optional value As T = Nothing)\n"
" End Sub\n"
" End Class\n"
"End Namespace\n",
encoding="utf-8",
)
project = tmp_path / "Demo.vbproj"
project.write_text(
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>'
'<TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>',
encoding="utf-8",
)
result = extract([first, second, project], cache_root=tmp_path)
labels = {node["label"] for node in result["nodes"]}
assert {
"Demo", "IWorker", "BaseCounter", "Counter", "Pair", "State",
"OnValue", "OffValue", "count", "Changed", "Value", "New()",
"Run()", "Helper()", "System.Text", "net8.0",
} <= labels
assert ("Counter", "BaseCounter") in _edge_labels(result, "inherits")
assert ("Counter", "IWorker") in _edge_labels(result, "implements")
assert ("Run()", "Helper()") in _edge_labels(result, "calls")
assert ("Run()", "Changed") in _edge_labels(result, "handles")
def test_vbnet_fixture_uses_normal_extract_path(tmp_path):
result = extract([FIXTURE], cache_root=tmp_path)
labels = {node["label"] for node in result["nodes"]}
assert {'Helper()', 'Fixtures', 'Run()', 'Sample'} <= labels
assert ('Run()', 'Helper()') in _edge_labels(result, "calls")
def test_vbnet_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
source = tmp_path / 'Broken.vb'
source.write_text("Class Broken\n Sub Valid()\n End Sub\nEnd Class\n???\n' Sub Ghost()\n", encoding="utf-8")
result = extract([source], cache_root=tmp_path)
labels = {node["label"].casefold() for node in result["nodes"]}
assert 'valid()' in labels
assert labels.isdisjoint({'ghost()'})
def test_vbnet_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys):
source = tmp_path / "missing.vb"
source.write_text('Class Missing\nEnd Class\n', encoding="utf-8")
monkeypatch.setitem(sys.modules, 'tree_sitter_vb_dotnet', None)
result = extract([source], cache_root=tmp_path)
assert result["nodes"] == []
assert 'pip install "graphifyy[vbnet]"' in capsys.readouterr().err
Generated
+18 -1
View File
@@ -1163,6 +1163,7 @@ all = [
{ name = "tree-sitter-ocaml" },
{ name = "tree-sitter-pascal" },
{ name = "tree-sitter-sql" },
{ name = "tree-sitter-vb-dotnet" },
{ name = "watchdog" },
{ name = "yt-dlp" },
]
@@ -1246,6 +1247,9 @@ svg = [
terraform = [
{ name = "tree-sitter-hcl" },
]
vbnet = [
{ name = "tree-sitter-vb-dotnet" },
]
video = [
{ name = "faster-whisper", marker = "python_full_version >= '3.11'" },
{ name = "yt-dlp" },
@@ -1368,6 +1372,8 @@ requires-dist = [
{ name = "tree-sitter-sql", marker = "extra == 'sql'" },
{ name = "tree-sitter-swift", specifier = ">=0.7,<0.9" },
{ name = "tree-sitter-typescript", specifier = ">=0.23,<0.25" },
{ name = "tree-sitter-vb-dotnet", marker = "extra == 'all'", specifier = "==0.3.0" },
{ name = "tree-sitter-vb-dotnet", marker = "extra == 'vbnet'", specifier = "==0.3.0" },
{ name = "tree-sitter-verilog", specifier = ">=1.0,<2.0" },
{ name = "tree-sitter-zig", specifier = ">=1.0,<2.0" },
{ name = "watchdog", marker = "extra == 'all'" },
@@ -1375,7 +1381,7 @@ requires-dist = [
{ name = "yt-dlp", marker = "extra == 'all'", specifier = ">=2026.7.4" },
{ name = "yt-dlp", marker = "extra == 'video'", specifier = ">=2026.7.4" },
]
provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "ocaml", "commonlisp", "robot", "all"]
provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "ocaml", "commonlisp", "robot", "vbnet", "all"]
[package.metadata.requires-dev]
dev = [
@@ -5042,6 +5048,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" },
]
[[package]]
name = "tree-sitter-vb-dotnet"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/a1/95f99d291163cab128f86e6303268f50c37856bcf7dc361435f752549a48/tree_sitter_vb_dotnet-0.3.0.tar.gz", hash = "sha256:6e7178a8cf7c34b8ba07aa3914bef5475f4eafd348cdd393ff3ebfc7cb41ae61", size = 1049419, upload-time = "2026-09-04T10:33:34.645Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/22/d209d8bce9148d50e17190b8c3fb7909fa51d65ed41a83efb7d99f4aa180/tree_sitter_vb_dotnet-0.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ad7dcf05170b429a07f05f78a2f0f5d3d46fc6174b8a7c90eec7cbfb980fca76", size = 326738, upload-time = "2026-09-04T10:33:30.602Z" },
{ url = "https://files.pythonhosted.org/packages/cd/69/d089c1f8e12ce743d41546650ca9a479bf99ea0eb74652ccd8939b198a4b/tree_sitter_vb_dotnet-0.3.0-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d2a2c6670c9f5917b3606e8a44045d1b508fc3258ede9f31a9b3a75f704dc99", size = 325760, upload-time = "2026-09-04T10:33:31.896Z" },
{ url = "https://files.pythonhosted.org/packages/c2/19/4edabceed5aaa9f0390bae115bd7a68f8e3b4fbe3c893c1e79bf060ec0a1/tree_sitter_vb_dotnet-0.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:f5615797aa1c5d98f1906eeda5ad3a78667069bd51b78ddf143118aaff57801e", size = 307980, upload-time = "2026-09-04T10:33:33.195Z" },
]
[[package]]
name = "tree-sitter-verilog"
version = "1.0.3"