mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-23 14:05:43 +00:00
feat: add VB.NET (.vb) language support via tree-sitter (#648)
This commit is contained in:
@@ -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, Pi, 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.
|
||||
|
||||
@@ -357,7 +357,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 |
|
||||
|
||||
+1
-1
@@ -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', '.r'}
|
||||
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', '.r', '.vb'}
|
||||
DOC_EXTENSIONS = {'.md', '.mdx', '.txt', '.rst', '.html', '.yaml', '.yml'}
|
||||
PAPER_EXTENSIONS = {'.pdf'}
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
||||
|
||||
@@ -501,6 +501,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(
|
||||
@@ -719,6 +738,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"}),
|
||||
@@ -734,6 +771,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 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -937,6 +988,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]]
|
||||
@@ -997,6 +1085,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")
|
||||
@@ -1028,6 +1119,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
|
||||
@@ -1049,6 +1144,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)
|
||||
@@ -1489,6 +1590,11 @@ def _extract_python_rationale(path: Path, result: dict) -> None:
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
def extract_vbnet(path: Path) -> dict:
|
||||
"""Extract classes, modules, functions, and imports from a .vb file."""
|
||||
return _extract_generic(path, _VBNET_CONFIG)
|
||||
|
||||
|
||||
def extract_python(path: Path) -> dict:
|
||||
"""Extract classes, functions, and imports from a .py file via tree-sitter AST."""
|
||||
result = _extract_generic(path, _PYTHON_CONFIG)
|
||||
@@ -3540,6 +3646,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)
|
||||
|
||||
+2
-1
@@ -52,7 +52,8 @@ office = ["python-docx", "openpyxl"]
|
||||
video = ["faster-whisper", "yt-dlp"]
|
||||
kimi = ["openai"]
|
||||
sql = ["tree-sitter-sql"]
|
||||
all = ["mcp", "neo4j", "pypdf", "markdownify", "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", "markdownify", "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"
|
||||
|
||||
Vendored
+59
@@ -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
|
||||
+73
-2
@@ -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"
|
||||
@@ -417,6 +417,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 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user