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 @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- VB.NET is now fully supported — indexing a `.vb` file extracts classes, modules, interfaces, methods, properties, fields, enums, and imports, along with call, inheritance, and event-handler edges. Extraction is powered by a bundled Roslyn-based backend (`codegraph-roslyn`) that runs alongside the existing tree-sitter extractors.
- `codegraph init` now builds the initial index by default — you no longer need the `-i`/`--index` flag (it's still accepted, so existing commands and scripts keep working). (#483)
- Go: Gin middleware chains now connect end-to-end in `codegraph_trace` and `codegraph_explore` — following a request reaches the middleware and route handlers registered via `.Use()` / `.GET()` instead of dead-ending where the framework dispatches the chain dynamically.
- `codegraph_explore` now sizes its response to the *answer* instead of the file count: it shows the mechanism and the exact methods you asked about in full — even when they're buried deep in a large file — while collapsing the redundant interchangeable implementations of an interface (an HTTP interceptor chain, a query-compiler family) down to signatures. Fewer tokens for a more complete answer, so on the flows that used to occasionally cost more than plain grep/read it's now clearly cheaper — and the win holds across small, medium, and large codebases. Distinct, non-interchangeable code is shown in full as before. Disable with `CODEGRAPH_ADAPTIVE_EXPLORE=0`.
Expand Down
16 changes: 16 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4387,3 +4387,19 @@ void helperFunction(int count) {
expect(getSupportedLanguages()).toContain('objc');
});
});

describe('VB.NET language registration', () => {
it('should detect .vb files as vbnet', () => {
expect(detectLanguage('Form1.vb')).toBe('vbnet');
expect(detectLanguage('Module1.vb')).toBe('vbnet');
expect(detectLanguage('C:/src/MyProject/FrmMain.vb')).toBe('vbnet');
});

it('should report vbnet as supported', () => {
expect(isLanguageSupported('vbnet')).toBe(true);
});

it('should include vbnet in getSupportedLanguages()', () => {
expect(getSupportedLanguages()).toContain('vbnet');
});
});
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"files": [
"dist",
"bin",
"scripts",
"README.md"
],
Expand Down
8 changes: 6 additions & 2 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as path from 'path';
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
import { Language } from '../types';

export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'yaml' | 'twig' | 'xml' | 'properties' | 'unknown'>;
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'yaml' | 'twig' | 'xml' | 'properties' | 'vbnet' | 'unknown'>;

/**
* WASM filename map — maps each language to its .wasm grammar file
Expand Down Expand Up @@ -63,6 +63,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.hpp': 'cpp',
'.hxx': 'cpp',
'.cs': 'csharp',
'.vb': 'vbnet',
'.php': 'php',
// Drupal-specific PHP file extensions
'.module': 'php',
Expand Down Expand Up @@ -276,6 +277,7 @@ export function isLanguageSupported(language: Language): boolean {
if (language === 'twig') return true; // file-level tracking only
if (language === 'xml') return true; // MyBatis mapper extractor
if (language === 'properties') return true; // Spring config keys
if (language === 'vbnet') return true; // Roslyn extractor (no WASM grammar)
if (language === 'unknown') return false;
return language in WASM_GRAMMAR_FILES;
}
Expand All @@ -287,6 +289,7 @@ export function isGrammarLoaded(language: Language): boolean {
if (language === 'svelte' || language === 'vue' || language === 'liquid') return true;
if (language === 'yaml' || language === 'twig') return true; // no WASM grammar needed
if (language === 'xml' || language === 'properties') return true; // no WASM grammar needed
if (language === 'vbnet') return true; // Roslyn extractor; no WASM grammar needed
return languageCache.has(language);
}

Expand All @@ -307,7 +310,7 @@ export function isFileLevelOnlyLanguage(language: Language): boolean {
* Get all supported languages (those with grammar definitions).
*/
export function getSupportedLanguages(): Language[] {
return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'vue', 'liquid'];
return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'vue', 'liquid', 'vbnet'];
}

/**
Expand Down Expand Up @@ -365,6 +368,7 @@ export function getLanguageDisplayName(language: Language): string {
c: 'C',
cpp: 'C++',
csharp: 'C#',
vbnet: 'VB.NET',
php: 'PHP',
ruby: 'Ruby',
swift: 'Swift',
Expand Down
150 changes: 150 additions & 0 deletions src/extraction/roslyn-extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { execFileSync } from 'child_process';
import * as path from 'path';
import type { ExtractionResult, Node, Edge, UnresolvedReference, Language } from '../types';

// Binary location: CODEGRAPH_ROSLYN_BIN env var (dev) or bundled binary (prod)
function getRoslynBin(): string {
if (process.env.CODEGRAPH_ROSLYN_BIN) return process.env.CODEGRAPH_ROSLYN_BIN;
let platform: string;
if (process.platform === 'win32') {
platform = 'win-x64';
} else if (process.platform === 'darwin') {
platform = process.arch === 'arm64' ? 'osx-arm64' : 'osx-x64';
} else {
platform = process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
}
const ext = process.platform === 'win32' ? '.exe' : '';
return path.join(__dirname, `../../bin/codegraph-roslyn-${platform}${ext}`);
}

// ── Roslyn JSON schema ────────────────────────────────────────────────────────

interface RoslynNode {
id: string;
kind: string;
name: string;
qualifiedName: string;
filePath: string;
startLine: number;
endLine: number;
visibility: string | null;
isStatic: boolean;
isAsync: boolean;
parentId: string | null;
}

interface RoslynEdge {
kind: string;
fromId: string;
toId: string;
toQualifiedName: string;
}

interface RoslynUnresolvedRef {
fromId: string;
toQualifiedName: string;
kind: string;
}

interface RoslynOutput {
nodes: RoslynNode[];
edges: RoslynEdge[];
unresolvedReferences: RoslynUnresolvedRef[];
errors: Array<{ message: string }>;
}

// ── Extractor ─────────────────────────────────────────────────────────────────

export class RoslynExtractor {
private readonly language: Language;

constructor(
private readonly filePath: string,
private readonly source: string
) {
this.language = path.extname(filePath).toLowerCase() === '.vb' ? 'vbnet' : 'csharp';
}

extract(): ExtractionResult {
const startTime = Date.now();
let raw: RoslynOutput;

try {
// Pass source via stdin so the binary does not need to locate the file
// on disk relative to its own working directory. The --file arg is kept
// for node IDs and qualified name generation inside the binary.
const stdout = execFileSync(getRoslynBin(), ['--file', this.filePath, '--stdin'], {
encoding: 'utf8',
input: this.source,
timeout: 30_000,
maxBuffer: 50 * 1024 * 1024,
});
raw = JSON.parse(stdout) as RoslynOutput;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return {
nodes: [],
edges: [],
unresolvedReferences: [],
errors: [{ message: `codegraph-roslyn failed: ${msg}`, severity: 'error' }],
durationMs: Date.now() - startTime,
};
}

const now = Date.now();
const nodeIds = new Set(raw.nodes.map((n) => n.id));

const nodes: Node[] = raw.nodes.map((n) => ({
id: n.id,
kind: n.kind as Node['kind'],
name: n.name,
qualifiedName: n.qualifiedName,
filePath: n.filePath,
language: this.language,
startLine: n.startLine,
endLine: n.endLine,
startColumn: 0,
endColumn: 0,
visibility: n.visibility as Node['visibility'],
isStatic: n.isStatic,
isAsync: n.isAsync,
updatedAt: now,
}));

const edges: Edge[] = [];
const unresolvedReferences: UnresolvedReference[] = [];

for (const e of raw.edges) {
if (nodeIds.has(e.toId)) {
edges.push({ kind: e.kind as Edge['kind'], source: e.fromId, target: e.toId });
} else {
unresolvedReferences.push({
fromNodeId: e.fromId,
referenceName: e.toQualifiedName || e.toId,
referenceKind: e.kind as Edge['kind'],
line: 0,
column: 0,
filePath: this.filePath,
});
}
}

for (const u of raw.unresolvedReferences) {
unresolvedReferences.push({
fromNodeId: u.fromId,
referenceName: u.toQualifiedName,
referenceKind: u.kind as Edge['kind'],
line: 0,
column: 0,
filePath: this.filePath,
});
}

const errors = raw.errors.map((e) => ({
message: e.message,
severity: 'error' as const,
}));

return { nodes, edges, unresolvedReferences, errors, durationMs: Date.now() - startTime };
}
}
9 changes: 7 additions & 2 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SvelteExtractor } from './svelte-extractor';
import { DfmExtractor } from './dfm-extractor';
import { VueExtractor } from './vue-extractor';
import { MyBatisExtractor } from './mybatis-extractor';
import { RoslynExtractor } from './roslyn-extractor';
import {
getAllFrameworkResolvers,
getApplicableFrameworks,
Expand Down Expand Up @@ -3055,8 +3056,12 @@ export function extractFromSource(

let result: ExtractionResult;

// Use custom extractor for Svelte
if (detectedLanguage === 'svelte') {
// Use Roslyn for VB.NET (tree-sitter has no VB grammar)
if (detectedLanguage === 'vbnet') {
const extractor = new RoslynExtractor(filePath, source);
result = extractor.extract();
} else if (detectedLanguage === 'svelte') {
// Use custom extractor for Svelte
const extractor = new SvelteExtractor(filePath, source);
result = extractor.extract();
} else if (detectedLanguage === 'vue') {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const LANGUAGES = [
'c',
'cpp',
'csharp',
'vbnet',
'php',
'ruby',
'swift',
Expand Down