Files
codegraph/__tests__/vbnet-member-bodies.test.ts
Colby Mchenry 489b4749c7 fix(vbnet,csharp): index every Structure member; walk property bodies and VB initializers (#2345)
VB.NET's grammar tags each member of a `Structure` as its own `body`
field, so extractAggregate's field lookup saw only the first member.
SCrawler's UserMedia indexed none of its members, and a structure that
opened with a nested enum indexed only the enum. Aggregates now resolve
their body the way classes and enums do (resolveBody, which VB maps to
the declaration itself). Objective-C, the only other struct language
with that hook, is unchanged.

Property bodies were never walked: the propertyTypes branch set
skipChildren after extractProperty, so calls, instantiations and reads
in a VB `Get`/`Set` block, `= initializer` or `As New`, and in a C#
accessor body or `=> expr`, were lost.

- propertyBodies() names the parts of a property that run code (VB: the
  whole declaration, as its methods are walked; C#: each accessor's body
  and an arrow value), walked with the property on the stack. The
  candidates-only fn-ref scan skips the walked subtrees by node id, so a
  function value is captured once, from the property. C# `= initializer`s
  stay unwalked, as C# field initializers are.
- The C# kernel mirrors it (property_bodies, scan skip list). The parity
  suite and a new micro pass; a serilog sweep is 209/214 byte-identical
  with 0 diffs (5 deferred for parse errors).
- VB field declarators (`= expr`, `As New T`) and Custom Event
  AddHandler/RemoveHandler/RaiseEvent blocks are walked as their field or
  event.

Before/after (nodes / edges): SCrawler 9,950 -> 10,427 / 14,336 -> 15,092;
staxrip 13,440 -> 13,644 / 24,120 -> 28,002, most of it its
`Property X As New NumParam With {...}` settings; serilog edges
6,642 -> 6,654. All 23 removed edges are the same call sites re-resolved
by VB's name-guessing resolver now that struct members are candidates:
10 wrong edges dropped, 9 correct ones now guessed wrong, 4 wrong either
way. Designer -> Add/Size edge counts are unchanged.

EXTRACTION_VERSION 27 -> 28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:41:59 +00:00

210 lines
7.2 KiB
TypeScript

/**
* VB.NET member bodies the extractor used to skip:
*
* - A `Structure` lists each member as its own `body` field, so reading the
* field returned only the first member: SCrawler's `UserMedia` indexed its
* nested `States` enum (declared first) and nothing else, and a structure
* that opened with a field indexed no members at all.
* - A property's `Get` / `Set` blocks, its `= initializer` and its
* `As New T` were never walked, nor a `Custom Event`'s accessors.
* - A field's `= initializer` and `As New T` were never walked.
*
* The calls, instantiations and reads in those places belong to the member
* that declares them.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-bodies-'));
const files: Record<string, string> = {
'Helper.vb': `Public Class Helper
Public Sub New(ByVal n As Integer)
End Sub
Public Shared Function Compute(ByVal n As Integer) As Integer
Return n
End Function
Public Shared Sub Store(ByVal n As Integer)
End Sub
Public Shared Function Seed() As Integer
Return 1
End Function
Public Shared Function Format(ByVal o As Object) As String
Return ""
End Function
Public Shared Sub Log(ByVal s As String)
End Sub
Public Shared Sub Attach(ByVal h As EventHandler)
End Sub
End Class
`,
'Widget.vb': `Public Class Widget
End Class
`,
'UserMedia.vb': `Public Structure UserMedia
Public State As States
Private Count As Integer
Public Enum States
Unknown
Downloaded
End Enum
Public ReadOnly Property Name As String
Get
Return Helper.Format(State)
End Get
End Property
Public Sub New(ByVal s As States)
State = s
Helper.Log("created")
End Sub
Public Function IsDone() As Boolean
Return State = States.Downloaded
End Function
End Structure
`,
'Box.vb': `Public Class Box
Private _x As Integer
Private _items As New Widget
Private _helper As Helper = New Helper(1)
Private Shared ReadOnly Def As Integer = Helper.Compute(3), Other As Integer = Helper.Seed()
Public Property Value As Integer
Get
Return Helper.Compute(_x)
End Get
Set(ByVal v As Integer)
Helper.Store(v)
_x = v
End Set
End Property
Public Property Auto As Integer = Helper.Seed()
Public ReadOnly Property Lst As New Widget
Public Custom Event Changed As EventHandler
AddHandler(ByVal value As EventHandler)
Helper.Attach(value)
End AddHandler
RemoveHandler(ByVal value As EventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
End RaiseEvent
End Event
End Class
`,
// The 1.6.2 receiver gate must hold in the newly walked places too:
// `Me.Panel.Controls.Add(…)` and `New System.Drawing.Size(…)` name no
// project member.
'Collections/DataColorCollection.vb': `Friend Class DataColorCollection
Friend Sub Add(ByVal Item As Object)
End Sub
End Class
`,
'Editors/UsersInfoForm.vb': `Friend Class UsersInfoForm
Private Enum EComparers
Name
Size
End Enum
End Class
`,
'MainForm.vb': `Public Class MainForm
Private ReadOnly DefaultSize As System.Drawing.Size = New System.Drawing.Size(184, 25)
Public ReadOnly Property Ready As Boolean
Get
Me.Panel.Controls.Add(Me.Button1)
Return True
End Get
End Property
End Class
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
function member(file: string, qualifiedName: string): Node {
const node = cg.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName);
expect(node, `${qualifiedName} in ${file}`).toBeDefined();
return node!;
}
/** `kind` edges leaving `node`, as target qualified names. */
function targets(node: Node, kind: string): string[] {
return cg
.getOutgoingEdgesFrom([node.id])
.filter((e) => e.kind === kind)
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('VB.NET member bodies', () => {
it('indexes every member of a Structure, not just the first', () => {
const struct = member('UserMedia.vb', 'UserMedia');
expect(struct.kind).toBe('struct');
const contained = cg
.getOutgoingEdgesFrom([struct.id])
.filter((e) => e.kind === 'contains')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind} ${n.qualifiedName}`)
.sort();
expect(contained).toEqual([
'enum UserMedia::States',
'field UserMedia::Count',
'field UserMedia::State',
'method UserMedia::IsDone',
'method UserMedia::New',
'property UserMedia::Name',
]);
expect(member('UserMedia.vb', 'UserMedia::States::Downloaded').kind).toBe('enum_member');
expect(targets(member('UserMedia.vb', 'UserMedia::New'), 'calls')).toEqual(['Helper::Log']);
});
it("attributes a property's Get and Set blocks to the property", () => {
expect(targets(member('Box.vb', 'Box::Value'), 'calls')).toEqual(['Helper::Compute', 'Helper::Store']);
expect(targets(member('UserMedia.vb', 'UserMedia::Name'), 'calls')).toEqual(['Helper::Format']);
});
it("walks a property's initializer and its As New", () => {
expect(targets(member('Box.vb', 'Box::Auto'), 'calls')).toEqual(['Helper::Seed']);
expect(targets(member('Box.vb', 'Box::Lst'), 'instantiates')).toEqual(['Widget']);
});
it("walks each field's initializer and its As New, per declarator", () => {
expect(targets(member('Box.vb', 'Box::Def'), 'calls')).toEqual(['Helper::Compute']);
expect(targets(member('Box.vb', 'Box::Other'), 'calls')).toEqual(['Helper::Seed']);
expect(targets(member('Box.vb', 'Box::_items'), 'instantiates')).toEqual(['Widget']);
expect(targets(member('Box.vb', 'Box::_helper'), 'instantiates')).toEqual(['Helper']);
});
it("attributes a Custom Event's accessors to the event", () => {
expect(targets(member('Box.vb', 'Box::Changed'), 'calls')).toEqual(['Helper::Attach']);
});
it('keeps the class out of what its members do', () => {
expect(targets(member('Box.vb', 'Box'), 'calls')).toEqual([]);
expect(targets(member('Box.vb', 'Box'), 'instantiates')).toEqual([]);
});
it('still links nothing through a receiver no name resolves', () => {
const ids = cg.getNodesInFile('MainForm.vb').map((n) => n.id);
const linked = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind !== 'contains')
.map((e) => cg.getNode(e.target)!.qualifiedName);
expect(linked).not.toContain('DataColorCollection::Add');
expect(linked).not.toContain('UsersInfoForm::EComparers::Size');
});
});