mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
489b4749c7
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>
210 lines
7.2 KiB
TypeScript
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');
|
|
});
|
|
});
|