From 5dbbcf7dadbee0482d34fd4d1d1419a06eee55b8 Mon Sep 17 00:00:00 2001 From: Rangarajan Ramaswamy Date: Fri, 1 May 2026 21:20:36 +0300 Subject: [PATCH] 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. --- README.md | 4 +- graphify/detect.py | 2 +- graphify/extract.py | 102 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- tests/fixtures/sample.vb | 59 ++++++++++++++++++++++ tests/test_languages.py | 75 +++++++++++++++++++++++++++- 6 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/sample.vb diff --git a/README.md b/README.md index c721dd39..811e0048 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ **An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, or Google Antigravity - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions. -Fully multimodal. Drop in code, PDFs, markdown, screenshots, diagrams, whiteboard photos, images in other languages, or video and audio files - graphify extracts concepts and relationships from all of it and connects them into one graph. Videos are transcribed with Whisper using a domain-aware prompt derived from your corpus. YAML/YML files (Kubernetes, Kustomize, Helm, config) are indexed for semantic extraction. SQL files are AST-extracted deterministically — tables, views, functions, foreign keys, and FROM/JOIN relationships map directly into the graph with no LLM needed. 25 languages supported via tree-sitter AST (Python, JS, TS, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir, Objective-C, Julia, Verilog, SystemVerilog, Vue, Svelte, Dart). +Fully multimodal. Drop in code, PDFs, markdown, screenshots, diagrams, whiteboard photos, images in other languages, or video and audio files - graphify extracts concepts and relationships from all of it and connects them into one graph. Videos are transcribed with Whisper using a domain-aware prompt derived from your corpus. YAML/YML files (Kubernetes, Kustomize, Helm, config) are indexed for semantic extraction. SQL files are AST-extracted deterministically — tables, views, functions, foreign keys, and FROM/JOIN relationships map directly into the graph with no LLM needed. 26 languages supported via tree-sitter AST (Python, JS, TS, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir, Objective-C, Julia, Verilog, SystemVerilog, Vue, Svelte, Dart, VB.NET). > Andrej Karpathy keeps a `/raw` folder where he drops papers, tweets, screenshots, and notes. graphify is the answer to that problem - 71.5x fewer tokens per query vs reading the raw files, persistent across sessions, honest about what it found vs guessed. @@ -353,7 +353,7 @@ Works with any mix of file types: | Type | Extensions | Extraction | |------|-----------|------------| -| Code | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1 .ex .exs .m .mm .jl .vue .svelte .sql` | AST via tree-sitter + call-graph (cross-file for all languages) + Java extends/implements + docstring/comment rationale. SQL: tables, views, functions, foreign keys, FROM/JOIN edges (requires `pip install graphifyy[sql]`) | +| Code | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1 .ex .exs .m .mm .jl .vue .svelte .sql .vb` | AST via tree-sitter + call-graph (cross-file for all languages) + Java extends/implements + docstring/comment rationale. SQL: tables, views, functions, foreign keys, FROM/JOIN edges (requires `pip install graphifyy[sql]`). VB.NET: classes, modules, structures, interfaces, inherits/implements edges (requires `pip install graphifyy[vbnet]`) | | Docs | `.md .mdx .html .txt .rst .yaml .yml` | Concepts + relationships + design rationale via Claude | | Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) | | Papers | `.pdf` | Citation mining + concept extraction | diff --git a/graphify/detect.py b/graphify/detect.py index 2fe85401..52717818 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -18,7 +18,7 @@ class FileType(str, Enum): _MANIFEST_PATH = "graphify-out/manifest.json" -CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv', '.sql'} +CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv', '.sql', '.vb'} DOC_EXTENSIONS = {'.md', '.mdx', '.txt', '.rst', '.html', '.yaml', '.yml'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} diff --git a/graphify/extract.py b/graphify/extract.py index a6951644..1bfaf8f8 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -472,6 +472,25 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s return False +def _vbnet_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, + nodes: list, edges: list, seen_ids: set, function_bodies: list, + parent_class_nid: str | None, add_node_fn, add_edge_fn, + walk_fn) -> bool: + """Handle namespace_block for VB.NET. Returns True if handled.""" + if node.type == "namespace_block": + name_node = node.child_by_field_name("name") + if name_node: + ns_name = _read_text(name_node, source) + ns_nid = _make_id(stem, ns_name) + line = node.start_point[0] + 1 + add_node_fn(ns_nid, ns_name, line) + add_edge_fn(file_nid, ns_nid, "contains", line) + for child in node.children: + walk_fn(child, parent_class_nid) + return True + return False + + # ── Language configs ────────────────────────────────────────────────────────── _PYTHON_CONFIG = LanguageConfig( @@ -686,6 +705,24 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st break +def _import_vbnet(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: + """Handle VB.NET 'Imports System.Collections.Generic' statements.""" + for child in node.children: + if child.type == "namespace_name": + raw = _read_text(child, source).strip() + if raw: + tgt_nid = _make_id(raw) + edges.append({ + "source": file_nid, + "target": tgt_nid, + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + }) + + _SWIFT_CONFIG = LanguageConfig( ts_module="tree_sitter_swift", class_types=frozenset({"class_declaration", "protocol_declaration"}), @@ -701,6 +738,20 @@ _SWIFT_CONFIG = LanguageConfig( import_handler=_import_swift, ) +_VBNET_CONFIG = LanguageConfig( + ts_module="tree_sitter_vbnet", + ts_language_fn="language", + class_types=frozenset({"class_block", "module_block", "structure_block", "interface_block"}), + function_types=frozenset({"method_declaration", "constructor_declaration", "property_declaration"}), + import_types=frozenset({"imports_statement"}), + call_types=frozenset({"invocation"}), + call_function_field="target", + call_accessor_node_types=frozenset({"member_access"}), + call_accessor_field="member", + function_boundary_types=frozenset({"method_declaration", "constructor_declaration", "property_declaration"}), + import_handler=_import_vbnet, +) + # ── Generic extractor ───────────────────────────────────────────────────────── @@ -904,6 +955,43 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict: if body: for child in body.children: walk(child, parent_class_nid=class_nid) + elif config.ts_module == "tree_sitter_vbnet": + # VB.NET class/module/structure/interface have no separate body node — + # inherits/implements appear as named fields, members are inline children. + def _emit_vbnet_parent(clause_node, rel: str) -> None: + for child in clause_node.children: + if not child.is_named: + continue + raw = _read_text(child, source).strip() + if not raw: + continue + # Strip generic args e.g. IList(Of T) → IList + base_name = raw.split("(")[0].strip().split(".")[-1] + if not base_name: + continue + base_nid = _make_id(stem, base_name) + if base_nid not in seen_ids: + base_nid = _make_id(base_name) + if base_nid not in seen_ids: + nodes.append({ + "id": base_nid, + "label": base_name, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) + add_edge(class_nid, base_nid, rel, line) + + inherits_node = node.child_by_field_name("inherits") + if inherits_node: + _emit_vbnet_parent(inherits_node, "inherits") + implements_node = node.child_by_field_name("implements") + if implements_node: + _emit_vbnet_parent(implements_node, "implements") + + for child in node.children: + walk(child, parent_class_nid=class_nid) return # Event listener property arrays: $listen = [Event::class => [Listener::class]] @@ -964,6 +1052,9 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict: func_name: str | None = "deinit" elif t == "subscript_declaration": func_name = "subscript" + elif config.ts_module == "tree_sitter_vbnet" and t == "constructor_declaration": + # VB.NET Sub New has no 'name' field — always named "New" + func_name = "New" elif config.resolve_function_name_fn is not None: # C/C++ style: use declarator declarator = node.child_by_field_name("declarator") @@ -995,6 +1086,10 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict: body = _find_body(node, config) if body: function_bodies.append((func_nid, body)) + elif config.ts_module == "tree_sitter_vbnet": + # VB.NET method/property/constructor bodies have no wrapper node — + # use the declaration node itself so walk_calls can find invocations. + function_bodies.append((func_nid, node)) return # JS/TS arrow functions and C# namespaces — language-specific extra handling @@ -1016,6 +1111,12 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict: parent_class_nid, add_node, add_edge): return + if config.ts_module == "tree_sitter_vbnet": + if _vbnet_extra_walk(node, source, file_nid, stem, str_path, + nodes, edges, seen_ids, function_bodies, + parent_class_nid, add_node, add_edge, walk): + return + # Default: recurse for child in node.children: walk(child, parent_class_nid=None) @@ -3491,6 +3592,7 @@ def extract(paths: list[Path], cache_root: Path | None = None) -> dict: ".v": extract_verilog, ".sv": extract_verilog, ".sql": extract_sql, + ".vb": extract_vbnet, } total = len(paths) diff --git a/pyproject.toml b/pyproject.toml index 649d9ce6..55211282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,8 @@ office = ["python-docx", "openpyxl"] video = ["faster-whisper", "yt-dlp"] kimi = ["openai"] sql = ["tree-sitter-sql"] -all = ["mcp", "neo4j", "pypdf", "html2text", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper", "yt-dlp", "matplotlib", "openai", "tree-sitter-sql"] +vbnet = ["tree-sitter-vbnet"] +all = ["mcp", "neo4j", "pypdf", "html2text", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper", "yt-dlp", "matplotlib", "openai", "tree-sitter-sql", "tree-sitter-vbnet"] [project.scripts] graphify = "graphify.__main__:main" diff --git a/tests/fixtures/sample.vb b/tests/fixtures/sample.vb new file mode 100644 index 00000000..999c1554 --- /dev/null +++ b/tests/fixtures/sample.vb @@ -0,0 +1,59 @@ +Imports System +Imports System.Collections.Generic +Imports System.Net.Http + +Namespace GraphifyDemo + + Public Interface IProcessor + Function Process(items As List(Of String)) As List(Of String) + End Interface + + Public Class DataProcessor + Inherits BaseProcessor + Implements IProcessor + + Private ReadOnly _client As HttpClient + + Public Sub New() + _client = New HttpClient() + End Sub + + Public Function Process(items As List(Of String)) As List(Of String) + Return Validate(items) + End Function + + Private Function Validate(items As List(Of String)) As List(Of String) + Dim result As New List(Of String) + For Each item In items + If Not String.IsNullOrEmpty(item) Then + result.Add(item.Trim()) + End If + Next + Return result + End Function + + End Class + + Public Module AppHelper + + Public Sub Run(processor As IProcessor) + Dim data As New List(Of String) + data.Add("hello") + processor.Process(data) + End Sub + + End Module + + Public Structure Point + Implements IComparable + + Public X As Double + Public Y As Double + + Public Function CompareTo(obj As Object) As Integer + Return 0 + End Function + + End Structure + +End Namespace diff --git a/tests/test_languages.py b/tests/test_languages.py index 680bb4e2..d470f570 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -1,11 +1,11 @@ -"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia.""" +"""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_swift, extract_go, extract_julia, extract_vbnet, ) FIXTURES = Path(__file__).parent / "fixtures" @@ -405,6 +405,77 @@ def test_swift_emits_calls(): 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 ────────────────────────────────────────────────────────────────────