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 ────────────────────────────────────────────────────────────────────