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
@@ -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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user