Vendored patched govindbanura/tree-sitter-vbnet grammar (MIT, ~20-fix patch + new external scanner for XML literals and multi-line LINQ continuation; provenance + rebuild instructions in docs/grammars/tree-sitter-vbnet.md), vbnet extractor with VB-specific call/index disambiguation, Inherits/ Implements heritage, As New instantiation, events, Declare P/Invoke, and MustOverride abstract members. Parse health on five real repos: PolicyPlus 100%, CompactGUI 100%, staxrip 95.2%, SCrawler 87.2%, PCL 87.5% (upstream grammar: 3-18%). Retrieval A/B (sonnet): 26-43% faster with 0-5 file reads vs 7-20 without. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d7afc8cc1f
commit
63e1b5a23a
@@ -43,6 +43,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
|
||||
cfscript: 'tree-sitter-cfscript.wasm',
|
||||
cfquery: 'tree-sitter-cfquery.wasm',
|
||||
cobol: 'tree-sitter-cobol.wasm',
|
||||
vbnet: 'tree-sitter-vbnet.wasm',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -131,6 +132,9 @@ export const EXTENSION_MAP: Record<string, Language> = {
|
||||
'.cob': 'cobol',
|
||||
'.cobol': 'cobol',
|
||||
'.cpy': 'cobol',
|
||||
// VB.NET: vendored grammar (patched govindbanura/tree-sitter-vbnet) — classes,
|
||||
// modules, interfaces, structures, properties, events, Handles clauses, LINQ.
|
||||
'.vb': 'vbnet',
|
||||
// Spring config: `application.properties` / `application-*.properties`. Same
|
||||
// shape as the `.yml` variants — the YAML/properties extractor emits one node
|
||||
// per leaf key, and the Spring resolver links `@Value("${k}")` references.
|
||||
@@ -249,7 +253,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
|
||||
// `class Foo(...)` as an ERROR that swallows the whole class (#237); we
|
||||
// vendor the upstream ABI-15 tree-sitter-c-sharp 0.23.5 wasm, which parses
|
||||
// primary constructors natively.
|
||||
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'csharp' || lang === 'r' || lang === 'cfml' || lang === 'cfscript' || lang === 'cfquery' || lang === 'cobol')
|
||||
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'csharp' || lang === 'r' || lang === 'cfml' || lang === 'cfscript' || lang === 'cfquery' || lang === 'cobol' || lang === 'vbnet')
|
||||
? path.join(__dirname, 'wasm', wasmFile)
|
||||
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
|
||||
const language = await WasmLanguage.load(wasmPath);
|
||||
@@ -468,6 +472,7 @@ export function getLanguageDisplayName(language: Language): string {
|
||||
cfscript: 'CFScript',
|
||||
cfquery: 'CFQuery (SQL)',
|
||||
cobol: 'COBOL',
|
||||
vbnet: 'Visual Basic .NET',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
return names[language] || language;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { objcExtractor } from './objc';
|
||||
import { cfscriptExtractor } from './cfscript';
|
||||
import { cfqueryExtractor } from './cfquery';
|
||||
import { cobolExtractor } from './cobol';
|
||||
import { vbnetExtractor } from './vbnet';
|
||||
|
||||
export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
|
||||
typescript: typescriptExtractor,
|
||||
@@ -57,4 +58,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
|
||||
cfscript: cfscriptExtractor,
|
||||
cfquery: cfqueryExtractor,
|
||||
cobol: cobolExtractor,
|
||||
vbnet: vbnetExtractor,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { getNodeText } from '../tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
|
||||
/**
|
||||
* The vendored VB.NET grammar has no true end-of-file token (its `_eof` rule is
|
||||
* a literal-`$` placeholder that never matches real input), so a file whose
|
||||
* last line lacks a trailing newline ends every parse with a MISSING-newline
|
||||
* error on the final statement. Appending a newline is offset-preserving for
|
||||
* all existing content.
|
||||
*/
|
||||
export function ensureTrailingNewline(source: string): string {
|
||||
return source.endsWith('\n') ? source : source + '\n';
|
||||
}
|
||||
|
||||
/** Case-insensitive member-modifier scan (VB keywords are case-insensitive). */
|
||||
function hasModifier(node: SyntaxNode, re: RegExp): boolean {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'member_modifier' && re.test(child.text)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A VB.NET method's declared return type (`Function Foo(...) As Bar`),
|
||||
* normalized to the bare class name a chained `Foo.Create().Bar()` could be
|
||||
* called on (the #645/#608 mechanism). The type lives in the method's
|
||||
* `as_clause` child; predefined types (Integer/String/…) and arrays yield
|
||||
* undefined, generics `List(Of Foo)` unwrap to the base type, and a dotted
|
||||
* `Ns.Foo` reduces to the simple name. Subs have no as_clause → undefined.
|
||||
*/
|
||||
function extractVbnetReturnType(node: SyntaxNode, source: string): string | undefined {
|
||||
const asClause = node.namedChildren.find((c: SyntaxNode) => c.type === 'as_clause');
|
||||
if (!asClause) return undefined;
|
||||
const typeNode = asClause.childForFieldName('declared_type');
|
||||
if (!typeNode || typeNode.type === 'predefined_type' || typeNode.type === 'array_type') return undefined;
|
||||
let t = getNodeText(typeNode, source).trim();
|
||||
t = t.replace(/\?+$/, ''); // nullable `Foo?`
|
||||
t = t.replace(/\(\s*Of\b[^)]*\)/gi, ''); // generics `List(Of Foo)` → `List`
|
||||
const last = t.split('.').pop()?.trim();
|
||||
if (!last || !/^[A-Za-z_]\w*$/.test(last)) return undefined;
|
||||
return last;
|
||||
}
|
||||
|
||||
export const vbnetExtractor: LanguageExtractor = {
|
||||
preParse: ensureTrailingNewline,
|
||||
functionTypes: [],
|
||||
// VB Modules are static containers (Shared members, no instantiation) —
|
||||
// indexed as classes so their members get normal containment/qualification.
|
||||
classTypes: ['class_declaration', 'module_declaration'],
|
||||
methodTypes: [
|
||||
'method_declaration',
|
||||
'constructor_declaration',
|
||||
// `Declare Function GetWindowLong Lib "user32" ...` (P/Invoke)
|
||||
'external_method_declaration',
|
||||
// Interface members are distinct node types in this grammar (unlike C#).
|
||||
'interface_method_declaration',
|
||||
// `MustOverride Sub/Function ...` — body-less abstract members.
|
||||
'abstract_method_declaration',
|
||||
],
|
||||
interfaceTypes: ['interface_declaration'],
|
||||
structTypes: ['structure_declaration'],
|
||||
enumTypes: ['enum_declaration'],
|
||||
enumMemberTypes: ['enum_member_declaration'],
|
||||
typeAliasTypes: ['delegate_declaration'],
|
||||
packageTypes: ['namespace_declaration'],
|
||||
extractPackage: (node: SyntaxNode, source: string) => {
|
||||
const name = node.childForFieldName('name');
|
||||
return name ? getNodeText(name, source) : null;
|
||||
},
|
||||
importTypes: ['imports_statement'],
|
||||
// VB uses parentheses for BOTH calls and indexing, so the grammar can only
|
||||
// split them heuristically (empty parens → invocation, args → array access;
|
||||
// even Roslyn parses both as InvocationExpression and disambiguates during
|
||||
// binding). Both are treated as call sites — extractCall has a vbnet branch
|
||||
// — and name matching simply never resolves an index read on a collection.
|
||||
callTypes: ['invocation_expression', 'array_access_expression', 'generic_invocation_expression'],
|
||||
variableTypes: ['declaration_statement'],
|
||||
fieldTypes: ['field_declaration'],
|
||||
propertyTypes: ['property_declaration', 'interface_property_declaration', 'abstract_property_declaration'],
|
||||
nameField: 'name',
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
// Method/property statements are direct children of the declaration node
|
||||
// (this grammar has no body wrapper), so the node is its own body — without
|
||||
// this, calls inside every Sub/Function would be skipped.
|
||||
resolveBody: (node: SyntaxNode) => node,
|
||||
getReturnType: extractVbnetReturnType,
|
||||
getVisibility: (node) => {
|
||||
if (hasModifier(node, /^private$/i)) return 'private';
|
||||
if (hasModifier(node, /^protected(\s+friend)?$/i)) return 'protected';
|
||||
if (hasModifier(node, /^friend$/i)) return 'internal';
|
||||
return 'public'; // VB members default to Public in practice
|
||||
},
|
||||
isStatic: (node) => hasModifier(node, /^shared$/i),
|
||||
isConst: (node) => hasModifier(node, /^const$/i) || (hasModifier(node, /^shared$/i) && hasModifier(node, /^readonly$/i)),
|
||||
isAsync: (node) => hasModifier(node, /^async$/i),
|
||||
extractImport: (node, source) => {
|
||||
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
||||
// `Imports System.Collections.Generic` / `Imports Alias = Some.Namespace` /
|
||||
// `Imports Global.Company.Product`. The name reference is the last
|
||||
// qualified/simple/global name child (skips the alias identifier).
|
||||
const nameNode = [...node.namedChildren]
|
||||
.reverse()
|
||||
.find((c: SyntaxNode) =>
|
||||
c.type === 'qualified_name' || c.type === 'simple_name' || c.type === 'global_qualified_name' || c.type === 'identifier'
|
||||
);
|
||||
if (nameNode) {
|
||||
return { moduleName: getNodeText(nameNode, source), signature: importText };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
visitNode: (node, ctx) => {
|
||||
// Events are indexed so `RaiseEvent X` / `Handles obj.X` flows have a
|
||||
// findable declaration (WinForms/WPF code is built around them).
|
||||
if (node.type === 'event_declaration' || node.type === 'custom_event_declaration') {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode) {
|
||||
ctx.createNode('field', getNodeText(nameNode, ctx.source), node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// `Sub New(...)` lexes as one token with no name field — without this,
|
||||
// constructors index as `<anonymous>`.
|
||||
if (node.type === 'constructor_declaration') {
|
||||
const ctor = ctx.createNode('method', 'New', node);
|
||||
if (ctor) {
|
||||
ctx.pushScope(ctor.id);
|
||||
ctx.visitFunctionBody(node, ctor.id);
|
||||
ctx.popScope();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -1153,7 +1153,7 @@ export class TreeSitterExtractor {
|
||||
// produce an `instantiates` reference. Children still walked so
|
||||
// nested calls inside the constructor args (`new Foo(bar())`) get
|
||||
// their own `calls` refs.
|
||||
else if (INSTANTIATION_KINDS.has(nodeType)) {
|
||||
else if (INSTANTIATION_KINDS.has(nodeType) || this.isVbnetConstructorShapedArrayCreation(node)) {
|
||||
this.extractInstantiation(node);
|
||||
// Java/C# `new T(...) { ... }` — anonymous class with body. Without
|
||||
// extracting it as a class node + its methods, the interface→impl
|
||||
@@ -3498,6 +3498,51 @@ export class TreeSitterExtractor {
|
||||
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!callerId) return;
|
||||
|
||||
// VB.NET: `foo(args)` is syntactically ambiguous between a call and an
|
||||
// index read, so the grammar parses non-empty parens as
|
||||
// array_access_expression (field `array`, not `function`) — even Roslyn
|
||||
// parses both as InvocationExpression and resolves during binding. Treat
|
||||
// all three shapes as call sites: the callee is the member/identifier
|
||||
// under the array/function field, qualified with a simple-identifier
|
||||
// receiver for resolution. Index reads on collections simply never
|
||||
// resolve to a callable, so they cost nothing.
|
||||
if (
|
||||
this.language === 'vbnet' &&
|
||||
(node.type === 'array_access_expression' ||
|
||||
node.type === 'invocation_expression' ||
|
||||
node.type === 'generic_invocation_expression')
|
||||
) {
|
||||
const fn = getChildByField(node, 'function') || getChildByField(node, 'array');
|
||||
if (!fn) return;
|
||||
let calleeName = '';
|
||||
if (fn.type === 'member_access_expression') {
|
||||
const member = getChildByField(fn, 'member');
|
||||
const memberName = member ? getNodeText(member, this.source) : '';
|
||||
if (!memberName) return;
|
||||
const receiver = getChildByField(fn, 'object');
|
||||
const SKIP = new Set(['me', 'mybase', 'myclass']);
|
||||
if (receiver && receiver.type === 'identifier' && !SKIP.has(getNodeText(receiver, this.source).toLowerCase())) {
|
||||
calleeName = `${getNodeText(receiver, this.source)}.${memberName}`;
|
||||
} else {
|
||||
calleeName = memberName;
|
||||
}
|
||||
} else if (fn.type === 'identifier') {
|
||||
calleeName = getNodeText(fn, this.source);
|
||||
} else {
|
||||
return; // parenthesized/chained receivers: no static name to link
|
||||
}
|
||||
if (calleeName) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ruby `call` nodes use `receiver` + `method` fields (tree-sitter-ruby), not
|
||||
// the `object`/`name`/`function` fields the branches below expect — so
|
||||
// without this they fell through to the generic path, which took the
|
||||
@@ -3904,6 +3949,24 @@ export class TreeSitterExtractor {
|
||||
* Children are still walked so nested calls inside the constructor
|
||||
* arguments (`new Foo(bar())`) get their own `calls` references.
|
||||
*/
|
||||
/**
|
||||
* VB.NET `New Invoice(1)` is syntactically ambiguous between constructing
|
||||
* Invoice with an argument and allocating an Invoice array of bound 1; the
|
||||
* grammar parses the parenthesized form as array_creation_expression. A
|
||||
* user-defined type with no `{...}` array initializer is overwhelmingly a
|
||||
* constructor call, so treat it as an instantiation. Predefined element
|
||||
* types (`New Byte(1023)`) and brace-initialized forms stay arrays.
|
||||
*/
|
||||
private isVbnetConstructorShapedArrayCreation(node: SyntaxNode): boolean {
|
||||
if (this.language !== 'vbnet' || node.type !== 'array_creation_expression') return false;
|
||||
const typeNode = getChildByField(node, 'type');
|
||||
if (!typeNode || typeNode.type === 'predefined_type' || typeNode.type === 'array_type') return false;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child?.type === 'array_initializer') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractInstantiation(node: SyntaxNode): void {
|
||||
if (this.nodeStack.length === 0) return;
|
||||
const fromId = this.nodeStack[this.nodeStack.length - 1];
|
||||
@@ -3965,6 +4028,13 @@ export class TreeSitterExtractor {
|
||||
// because no class is named with the angle-bracket suffix.
|
||||
const ltIdx = className.indexOf('<');
|
||||
if (ltIdx > 0) className = className.slice(0, ltIdx);
|
||||
// VB.NET spells generics with parentheses: `New List(Of String)` /
|
||||
// `New Dictionary(Of K, V)(cap)` — strip from the `(` so the bare
|
||||
// type name is what resolution matches.
|
||||
if (this.language === 'vbnet') {
|
||||
const parenIdx = className.indexOf('(');
|
||||
if (parenIdx > 0) className = className.slice(0, parenIdx);
|
||||
}
|
||||
// For namespaced/qualified constructors (`new ns.Foo()`,
|
||||
// `new ns::Foo()`) keep the trailing identifier — that's what
|
||||
// matches a class node in the index.
|
||||
@@ -4374,7 +4444,7 @@ export class TreeSitterExtractor {
|
||||
|
||||
if (this.extractor!.callTypes.includes(nodeType)) {
|
||||
this.extractCall(node);
|
||||
} else if (INSTANTIATION_KINDS.has(nodeType)) {
|
||||
} else if (INSTANTIATION_KINDS.has(nodeType) || this.isVbnetConstructorShapedArrayCreation(node)) {
|
||||
// `new Foo()` inside a function body — emit an `instantiates`
|
||||
// reference. Without this branch the body walker only knew
|
||||
// about `call_expression`, so constructor invocations
|
||||
@@ -4747,6 +4817,33 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
// VB.NET: `Inherits Base` / `Implements IFoo, IBar(Of T)` are STATEMENTS
|
||||
// inside the class body (children of the class node), not header clauses.
|
||||
// Each name is a simple/qualified/generic reference; generics unwrap to
|
||||
// the base identifier and dotted paths keep the trailing segment.
|
||||
if (
|
||||
this.language === 'vbnet' &&
|
||||
(child.type === 'inherits_statement' || child.type === 'implements_statement')
|
||||
) {
|
||||
const kind = child.type === 'inherits_statement' ? 'extends' : 'implements';
|
||||
for (const ref of child.namedChildren) {
|
||||
if (!ref || (ref.type !== 'simple_name' && ref.type !== 'qualified_name' && ref.type !== 'generic_name' && ref.type !== 'global_qualified_name')) continue;
|
||||
let name = getNodeText(ref, this.source);
|
||||
name = name.replace(/\(\s*Of\b[^)]*\)/gi, '');
|
||||
const lastDot = name.lastIndexOf('.');
|
||||
if (lastDot >= 0) name = name.slice(lastDot + 1);
|
||||
name = name.trim();
|
||||
if (!name) continue;
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: classId,
|
||||
referenceName: name,
|
||||
referenceKind: kind,
|
||||
line: ref.startPosition.row + 1,
|
||||
column: ref.startPosition.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// C#: `class Movie : BaseItem, IPlugin` → base_list with identifier children
|
||||
// base_list combines both base class and interfaces in a single colon-separated list.
|
||||
// We emit all as 'extends' since the syntax doesn't distinguish them.
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -99,6 +99,7 @@ export const LANGUAGES = [
|
||||
'cfscript',
|
||||
'cfquery',
|
||||
'cobol',
|
||||
'vbnet',
|
||||
'unknown',
|
||||
] as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user