Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- VB.NET is now fully supported — CodeGraph indexes `.vb` files natively using the bundled tree-sitter grammar, extracting classes, modules, interfaces, methods (including constructors), properties, fields, constants, enums, and imports, along with call, inheritance, and event-handler edges.
- `codegraph status --json` now also reports the running CLI `version`, the index directory (`indexPath`), and a `lastIndexed` timestamp (ISO-8601, or null when nothing's indexed yet), so CI and scripts can pin the CLI version and check index freshness from a single command. A matching `CodeGraph.getLastIndexedAt()` library method exposes the same freshness check without shelling out. Thanks @12122J and @eddieran. (#329)

### Fixes
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ CodeGraph cuts **tokens, tool calls, and wall-clock time on every repo** — acr
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi |
| **21+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 14 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down Expand Up @@ -624,6 +624,7 @@ is written):
| Svelte | `.svelte` | Full support (script extraction, Svelte 5 runes, SvelteKit routes) |
| Vue | `.vue` | Full support (script + script-setup extraction, Nuxt page/API/middleware routes) |
| Liquid | `.liquid` | Full support |
| VB.NET | `.vb` | Full support (classes, modules, interfaces, enums, methods, properties, constants, call edges) |
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
Expand Down
90 changes: 90 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4459,3 +4459,93 @@ func (s Stack[T]) Len() int { return len(s.items) }
expect(js.nodes.find((n) => n.name === 'handleRequest' && n.kind === 'function')).toBeDefined();
});
});

describe('VB.NET Extraction', () => {
it('extracts classes, modules, structs, interfaces, enums, methods, properties, imports, and calls', () => {
const code = `
Imports System
Imports System.Collections.Generic

Namespace MyApp

Public Interface IAnimal
Function Speak() As String
End Interface

Public Class Animal
Implements IAnimal
Private _name As String
Public Property Name As String
Public Sub New(name As String)
_name = name
End Sub
Public Function Speak() As String
Return _name
End Function
End Class

Public Structure Point
Public X As Double
Public Y As Double
Public Function DistanceTo(other As Point) As Double
Return 0
End Function
End Structure

Public Enum Color
Red
Green
Blue
End Enum

Module MathHelper
Public Const PI As Double = 3.14159
Public Shared Function Add(a As Integer, b As Integer) As Integer
Return a + b
End Function
Public Sub Run()
Dim x = Add(1, 2)
End Sub
End Module

End Namespace
`;
const result = extractFromSource('Sample.vb', code);
const byKind = (k: string) => result.nodes.filter((n) => n.kind === k).map((n) => n.name);

// Types
expect(byKind('class')).toContain('Animal');
expect(byKind('interface')).toContain('IAnimal');
expect(byKind('struct')).toContain('Point');
expect(byKind('enum')).toContain('Color');
expect(byKind('enum_member')).toEqual(expect.arrayContaining(['Red', 'Green', 'Blue']));
// Module is distinct from class (static sealed type)
const mod = result.nodes.find((n) => n.name === 'MathHelper' && n.kind === 'module');
expect(mod).toBeDefined();
expect(mod?.isStatic).toBe(true);

// Members
expect(byKind('method')).toEqual(expect.arrayContaining(['New', 'Speak', 'DistanceTo', 'Add']));
expect(byKind('property')).toContain('Name');
expect(byKind('field')).toContain('_name');
expect(byKind('constant')).toContain('PI');

// Shared method is static; constructor is not
expect(result.nodes.find((n) => n.name === 'Add')?.isStatic).toBe(true);
expect(result.nodes.find((n) => n.name === 'New')?.isStatic).toBeFalsy();

// Inherits/Implements lines must not appear as fields
expect(byKind('field')).not.toContain('Implements');
expect(byKind('field')).not.toContain('IAnimal');

// Namespace and imports
expect(byKind('namespace')).toContain('MyApp');
expect(byKind('import')).toEqual(expect.arrayContaining(['System', 'System.Collections.Generic']));

// Calls
expect(result.unresolvedReferences.some((r) => r.referenceKind === 'calls')).toBe(true);

// Language tag
expect(result.nodes.find((n) => n.name === 'Animal')?.language).toBe('vbnet');
});
});
5 changes: 4 additions & 1 deletion src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
lua: 'tree-sitter-lua.wasm',
luau: 'tree-sitter-luau.wasm',
objc: 'tree-sitter-objc.wasm',
vbnet: 'tree-sitter-vbnet.wasm',
};

/**
Expand Down Expand Up @@ -101,6 +102,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.luau': 'luau',
'.m': 'objc',
'.mm': 'objc',
'.vb': 'vbnet',
// XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
// shape and emits SQL-statement nodes (other XML returns empty).
'.xml': 'xml',
Expand Down Expand Up @@ -185,7 +187,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
// ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
// 0.25 (drops nested calls/imports on every file after the first); we
// vendor the upstream ABI-15 wasm instead.
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'vbnet')
? path.join(__dirname, 'wasm', wasmFile)
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
const language = await WasmLanguage.load(wasmPath);
Expand Down Expand Up @@ -384,6 +386,7 @@ export function getLanguageDisplayName(language: Language): string {
lua: 'Lua',
luau: 'Luau',
objc: 'Objective-C',
vbnet: 'VB.NET',
yaml: 'YAML',
twig: 'Twig',
xml: 'XML',
Expand Down
2 changes: 2 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { scalaExtractor } from './scala';
import { luaExtractor } from './lua';
import { luauExtractor } from './luau';
import { objcExtractor } from './objc';
import { vbnetExtractor } from './vbnet';

export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand All @@ -49,4 +50,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
lua: luaExtractor,
luau: luauExtractor,
objc: objcExtractor,
vbnet: vbnetExtractor,
};
204 changes: 204 additions & 0 deletions src/extraction/languages/vbnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

// VB.NET keywords that appear as `field_declaration` misparsed nodes when the
// grammar encounters `Inherits X` / `Implements Y` inside a class body.
const VB_INHERIT_KEYWORDS = new Set([
'Inherits', 'Implements', 'MustInherit', 'NotInheritable',
'MustOverride', 'NotOverridable', 'WithEvents',
]);

function getModifierText(node: SyntaxNode): string[] {
const mods: string[] = [];
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'modifiers') {
for (let j = 0; j < child.namedChildCount; j++) {
const mod = child.namedChild(j);
if (mod?.type === 'modifier') mods.push(mod.text);
}
}
}
return mods;
}

function resolveVisibility(mods: string[]): 'public' | 'private' | 'protected' | 'internal' | undefined {
if (mods.includes('Public')) return 'public';
if (mods.includes('Private')) return 'private';
if (mods.includes('Protected')) return 'protected';
if (mods.includes('Friend')) return 'internal';
return undefined;
}

export const vbnetExtractor: LanguageExtractor = {
functionTypes: [],
// module_block and structure_block are handled in visitNode below so they get
// the correct 'module' and 'struct' kinds respectively; only class_block here.
classTypes: ['class_block'],
// constructor_declaration handles `Sub New(...)`
methodTypes: ['method_declaration', 'constructor_declaration'],
interfaceTypes: ['interface_block'],
structTypes: [],
enumTypes: ['enum_block'],
enumMemberTypes: ['enum_member'],
typeAliasTypes: [],
importTypes: ['imports_statement'],
callTypes: ['invocation'],
variableTypes: [],
propertyTypes: ['property_declaration'],
fieldTypes: ['field_declaration'],

nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
returnField: 'return_type',

// VB.NET block nodes are their own body (no separate child named 'body').
resolveBody: (node) => node,

getVisibility: (node) => resolveVisibility(getModifierText(node)),

isStatic: (node) => getModifierText(node).includes('Shared'),

isAsync: (node) => getModifierText(node).includes('Async'),

resolveName: (node) => {
// constructor_declaration has no name field; canonical VB.NET name is "New"
if (node.type === 'constructor_declaration') return 'New';
return undefined;
},

isExported: (node) => getModifierText(node).includes('Public'),

// Produce a signature like `(x As Double, y As Double) As Double` for functions,
// or just `(x As Double, y As Double)` for subs (no return type).
getSignature: (node, source) => {
let params: SyntaxNode | null = null;
let returnType: SyntaxNode | null = null;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
if (child.type === 'parameter_list') params = child;
if (child.type === 'type') returnType = child;
}
if (!params && !returnType) return undefined;
const paramStr = params ? source.substring(params.startIndex, params.endIndex) : '()';
return returnType
? `${paramStr} As ${source.substring(returnType.startIndex, returnType.endIndex)}`
: paramStr;
},

extractImport: (node, source) => {
// imports_statement → namespace: namespace_name → identifier+
const nsNode = node.childForFieldName('namespace');
if (!nsNode) return null;
const moduleName = source.substring(nsNode.startIndex, nsNode.endIndex).trim();
return {
moduleName,
signature: source.substring(node.startIndex, node.endIndex).trim(),
};
},

visitNode: (node, ctx) => {
// namespace_block — create namespace scope and recurse
if (node.type === 'namespace_block') {
const nameNode = node.childForFieldName('name');
if (!nameNode) return false;
const name = getNodeText(nameNode, ctx.source);
const nsNode = ctx.createNode('namespace', name, node);
if (nsNode) {
ctx.pushScope(nsNode.id);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) ctx.visitNode(child);
}
ctx.popScope();
}
return true;
}

// module_block — VB.NET Module is a static sealed type; extract as 'module'
if (node.type === 'module_block') {
const nameNode = node.childForFieldName('name');
if (!nameNode) return false;
const name = getNodeText(nameNode, ctx.source);
const mods = getModifierText(node);
const moduleNode = ctx.createNode('module', name, node, {
visibility: resolveVisibility(mods),
isExported: mods.includes('Public'),
isStatic: true,
});
if (moduleNode) {
ctx.pushScope(moduleNode.id);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) ctx.visitNode(child);
}
ctx.popScope();
}
return true;
}

// structure_block — VB.NET Structure is a value type; extract as 'struct'
if (node.type === 'structure_block') {
const nameNode = node.childForFieldName('name');
if (!nameNode) return false;
const name = getNodeText(nameNode, ctx.source);
const mods = getModifierText(node);
const structNode = ctx.createNode('struct', name, node, {
visibility: resolveVisibility(mods),
isExported: mods.includes('Public'),
});
if (structNode) {
ctx.pushScope(structNode.id);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) ctx.visitNode(child);
}
ctx.popScope();
}
return true;
}

// const_declaration — extract as constant node
if (node.type === 'const_declaration') {
const nameNode = node.childForFieldName('name');
if (nameNode) {
const name = getNodeText(nameNode, ctx.source);
const mods = getModifierText(node);
ctx.createNode('constant', name, node, {
visibility: resolveVisibility(mods),
isStatic: mods.includes('Shared'),
});
}
return true;
}

// Filter out Inherits/Implements lines misparsed as field_declaration.
// VB.NET grammar splits `Inherits BaseClass` into two adjacent field_declarations
// on the same source line: one for the keyword and one for the base name.
if (node.type === 'field_declaration') {
const decls = node.namedChildren.filter(c => c.type === 'variable_declarator');
const nameNode = decls[0]?.childForFieldName('name');
const name = nameNode ? getNodeText(nameNode, ctx.source) : '';

// Skip the keyword node itself
if (VB_INHERIT_KEYWORDS.has(name)) return true;

// Skip the value node that follows a keyword on the same source line
const prev = node.previousNamedSibling;
if (prev?.type === 'field_declaration') {
const prevDecls = prev.namedChildren.filter(c => c.type === 'variable_declarator');
const prevNameNode = prevDecls[0]?.childForFieldName('name');
const prevName = prevNameNode ? getNodeText(prevNameNode, ctx.source) : '';
if (VB_INHERIT_KEYWORDS.has(prevName) &&
prev.startPosition.row === node.startPosition.row) {
return true;
}
}
}

return false;
},
};
Binary file added src/extraction/wasm/tree-sitter-vbnet.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const LANGUAGES = [
'lua',
'luau',
'objc',
'vbnet',
'yaml',
'twig',
'xml',
Expand Down