mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
5a11cd7ab7
VB.NET linked a member read only through a type or module name (#2355), so a read or write through an instance -- a parameter `x.Normal = 3`, a local `h.Normal`, a field `_h.Title`, `Me._h.Normal` -- produced no edge, and `codegraph callers` on an instance field or property listed almost none of its uses. - Extraction (TS only; VB.NET is not kernel-routed): every member read or write is sent with its receiver as a path, whatever the receiver's case: `x.Normal`, `Me._h.Normal`, `MyBase.Count`, `.Value` in a `With` block (nested `With .Inner` included), `.Switch` in an object initializer `New BoolParam With {...}`, and `{T}.X` for a value whose type the code writes (`DirectCast(o, T).X`, `New T().X`). A `With` block's `.Run()` call, which recorded nothing, is sent the same way. The grammar splits `New System.Drawing.Size(...)` into members read through `New System`; those, anonymous types' `New With {.X = 1}`, and paths that start with System, Microsoft or My are not sent. - Resolution (vbnet-receivers.ts): each link of the path is typed by the rules calls use since #2351 -- declarations, casts, `For Each` over a typed collection, call results, type-parameter constraints, Module variables, namespaces and Imports -- and the read links the member the type declares or inherits. A method named without parentheses is called (unless AddressOf or NameOf only names it), and `x.Items(0)`, an index that VB.NET writes as a call, reads `Items`. An outside, `Object`-typed, ambiguous or untyped receiver links nothing; nothing is guessed by name. - "Color Color": a value named like its type (`theme As Theme`) now reads an instance member through the value, so only the member is linked; a Shared member, a Const (read from its declaration) or an Enum case still links the type as well, as #2355 did for every such read. - membersNamed looks a type's members up in its parts' files rather than with one query per member name. The graph is identical on SCrawler; on staxrip, whose AutoCrop tool redeclares `ServerInfo`, 35 reads that main typed through the other project's field, and so left unlinked, now link (70 edges). Verification: vbnet-instance-member-refs.test.ts (11 tests) fails 9/11 on main and passes; one vbnet-shared-member-refs assertion now expects `OtherSession::SessionId` from `Shadowed(appSession As OtherSession)`; the VB.NET suites, tsc and the full suite pass (462 files, 5,859 tests; 22 kernel/platform files skipped). The issue's repro on the built CLI links every read and write, and `codegraph callers` lists `Run` for `SharedCache`, `Normal` and `Title`. Validation, main vs this branch: nodes unchanged, self-loops unchanged. SCrawler 17,437 -> 31,344 edges, 1 removed (same edge, new metadata). staxrip 32,569 -> 71,362 edges, 585 removed: 318 same edge with new metadata (Color Color reads of instance members, qualified-name -> instance-method) and 267 class links of those reads, every one beside an instance member. About 42k added two-link reads were re-derived from the source text by a separate check; its 837 disagreements were all its own blind spots (constraints, Imports aliases, sibling-block locals, call results, staxrip's global `p`). No added edge crosses between staxrip and its AutoCrop tool. Cost (5-run medians): CPU 16.1 -> 19.3 s and 20.2 -> 25.4 s, DB 28.1 -> 43.2 MB and 34.9 -> 59.0 MB; the DB growth is the new edges plus reads through outside types kept as failed refs for re-resolution (#1240). Not covered: unqualified reads, reads through an index or a call result (`list(0).X`), Handles clauses, and calls through a chained receiver written directly (`g.MainForm.Update()`), which still resolve by name. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
/**
|
|
* VB.NET: a field or property read or written through a value — a parameter
|
|
* (`x.Normal = 3`), a local (`h.Normal`), a field (`_h.Title`), `Me._h.Normal`,
|
|
* `MyBase.Count`, a `With` block's `.Value`, an object initializer's
|
|
* `.Switch` — is a use of the member of the type that value is declared as.
|
|
* Before, calls through a typed receiver linked (#2351) and reads through a
|
|
* type's name (#2305), but no read through a value, so `codegraph callers` on
|
|
* an instance field or property listed almost none of its uses.
|
|
*
|
|
* The receiver is typed by the rules calls use — its declaration (`As`, `As
|
|
* New`, a cast, a `For Each` over a typed collection, a type parameter's
|
|
* constraint, a Module variable), the namespaces around the read, what the
|
|
* type inherits — and a path (`Me._h.Normal`, `n.Child.Value`) one link at a
|
|
* time. An outside type, `Object`, an untyped name or an index links nothing:
|
|
* a value's member is never guessed by name.
|
|
*/
|
|
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 { extractFromSource } from '../src/extraction';
|
|
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
|
|
|
let root = '';
|
|
let cg: CodeGraph;
|
|
|
|
const files: Record<string, string> = {
|
|
// The issue's two files, verbatim.
|
|
'Holder.vb': `Public Class Holder
|
|
Public Shared SharedCache As Integer
|
|
Public Normal As Integer
|
|
Public Property Title As String
|
|
Public Sub EndSession()
|
|
End Sub
|
|
End Class
|
|
`,
|
|
'User.vb': `Public Class User
|
|
Private _h As New Holder()
|
|
Public Sub Run(x As Holder)
|
|
Dim b As Integer = x.SharedCache
|
|
Dim n As Integer = x.Normal
|
|
x.Normal = 3
|
|
Dim t As String = _h.Title
|
|
Me._h.Normal = 4
|
|
Dim h As New Holder()
|
|
Dim m As Integer = h.Normal
|
|
x.EndSession()
|
|
End Sub
|
|
End Class
|
|
`,
|
|
'Node.vb': `Public Class Node
|
|
Public Value As Integer
|
|
Public Property Child As Node
|
|
Public Items As New List(Of String)
|
|
Public Event Changed As EventHandler
|
|
Public Sub Reset()
|
|
End Sub
|
|
Public Function Describe() As String
|
|
Return ""
|
|
End Function
|
|
End Class
|
|
`,
|
|
// Me, MyBase, a path through members, an inherited member; an index into a
|
|
// field, a method named without parentheses, an event, AddressOf.
|
|
'Views.vb': `Public Class BaseView
|
|
Protected Caption As String
|
|
Public Overridable Property Count As Integer
|
|
End Class
|
|
|
|
Public Class DerivedView
|
|
Inherits BaseView
|
|
Private _node As Node
|
|
Public Sub New(caption As String)
|
|
Me.Caption = caption
|
|
End Sub
|
|
Public Overrides Property Count As Integer
|
|
Get
|
|
Return MyBase.Count + Me._node.Child.Value
|
|
End Get
|
|
Set(value As Integer)
|
|
MyBase.Count = value
|
|
End Set
|
|
End Property
|
|
Public Sub Watch(n As Node)
|
|
Dim first = n.Items(0)
|
|
Dim text = n.Describe
|
|
AddHandler n.Changed, AddressOf OnChanged
|
|
Dim reset As Action = AddressOf n.Reset
|
|
Dim nodes As New List(Of Node)
|
|
For Each child In nodes
|
|
child.Value = 1
|
|
Next
|
|
End Sub
|
|
Private Sub OnChanged(sender As Object, e As EventArgs)
|
|
End Sub
|
|
End Class
|
|
`,
|
|
// With blocks (nested, through Me, through a cast), an object initializer, a
|
|
// cast; an anonymous type declares its own members.
|
|
'Builder.vb': `Public Class Builder
|
|
Private _node As New Node()
|
|
Property Seed As New Node With {.Value = 7}
|
|
Public Sub Fill(o As Object, n As Node)
|
|
With n
|
|
.Value = 1
|
|
Dim d = .Describe()
|
|
With .Child
|
|
.Reset()
|
|
End With
|
|
End With
|
|
With Me._node
|
|
.Child = Nothing
|
|
End With
|
|
With DirectCast(o, Node)
|
|
.Items.Clear()
|
|
End With
|
|
Dim v = TryCast(o, Node).Value
|
|
Dim anonymous = New With {.Value = 2}
|
|
End Sub
|
|
End Class
|
|
`,
|
|
// A Module variable, a type parameter's constraint.
|
|
'Globals.vb': `Public Module Globals
|
|
Public Current As Node
|
|
End Module
|
|
`,
|
|
'Repo.vb': `Public Class Repo(Of T As Node)
|
|
Private _item As T
|
|
Public Function Peek() As Integer
|
|
Return _item.Value + Current.Value
|
|
End Function
|
|
End Class
|
|
`,
|
|
// A field's type is the one the namespace around it declares.
|
|
'Sites/Reddit.vb': `Namespace API.Reddit
|
|
Friend Class SiteSettings
|
|
Friend Property Header As String
|
|
End Class
|
|
Friend Class UserData
|
|
Private _settings As SiteSettings
|
|
Friend Function Key() As String
|
|
Return _settings.Header
|
|
End Function
|
|
End Class
|
|
End Namespace
|
|
`,
|
|
'Sites/Twitter.vb': `Namespace API.Twitter
|
|
Friend Class SiteSettings
|
|
Friend Property Header As String
|
|
End Class
|
|
End Namespace
|
|
`,
|
|
// staxrip's encoder options: a property an initializer sets up, written later.
|
|
'Encoder.vb': `Public MustInherit Class CommandLineParam
|
|
Property Switch As String
|
|
Property Text As String
|
|
End Class
|
|
|
|
Public Class BoolParam
|
|
Inherits CommandLineParam
|
|
Property Value As Boolean
|
|
End Class
|
|
|
|
Public Class x265Enc
|
|
Property ConstVBV As New BoolParam With {
|
|
.Switch = "--const-vbv",
|
|
.Text = "Enable VBV algorithm to be consistent across runs"}
|
|
Sub Apply()
|
|
ConstVBV.Value = False
|
|
End Sub
|
|
End Class
|
|
`,
|
|
// "Color Color": a value named like its type reads an instance member
|
|
// through itself, a Shared member and an Enum case through the type. And
|
|
// staxrip names enums in lower case.
|
|
'Theme.vb': `Public Class Theme
|
|
Public Shared Current As Theme
|
|
Public Const Version As String = "1"
|
|
Public Property General As String
|
|
End Class
|
|
|
|
Public Enum Mode
|
|
Fast
|
|
End Enum
|
|
|
|
Public Enum x265RateMode
|
|
Bitrate
|
|
End Enum
|
|
|
|
Public Class Painter
|
|
Public Property Mode As Mode
|
|
Public Sub Paint(theme As Theme)
|
|
Dim g = theme.General
|
|
Dim c = theme.Current
|
|
Dim v = theme.Version
|
|
Dim f = Mode.Fast
|
|
Dim r = x265RateMode.Bitrate
|
|
End Sub
|
|
End Class
|
|
`,
|
|
// What links nothing: an outside type, Object, an untyped name, an index's
|
|
// element; and a parameter named like a type is read as the parameter.
|
|
'Lookalike.vb': `Public Class Lookalike
|
|
Public Length As Integer
|
|
Public Count As Integer
|
|
Public Value As Integer
|
|
End Class
|
|
`,
|
|
'Negatives.vb': `Public Class Negatives
|
|
Public Sub Go(s As String, o As Object, list As List(Of Node), ByVal untyped)
|
|
Dim a = s.Length
|
|
Dim b = list.Count
|
|
Dim c = o.Value
|
|
Dim d = untyped.Value
|
|
Dim e = list(0).Value
|
|
End Sub
|
|
Public Sub Shadowed(holder As Node)
|
|
Dim f = Holder.SharedCache
|
|
Dim g = holder.Value
|
|
End Sub
|
|
End Class
|
|
`,
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadGrammarsForLanguages(['vbnet']);
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-instance-'));
|
|
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 nodeId(qualifiedName: string): string {
|
|
const found = cg.getNodesInFiles(Object.keys(files)).filter((n) => n.qualifiedName === qualifiedName);
|
|
if (found.length !== 1) throw new Error(`${found.length} nodes named ${qualifiedName}`);
|
|
return found[0]!.id;
|
|
}
|
|
|
|
/** The qualified names of what links to `qualifiedName` with an edge of `kind`, one per edge. */
|
|
function linkedFrom(qualifiedName: string, kind: 'references' | 'calls'): string[] {
|
|
return cg
|
|
.getIncomingEdgesTo([nodeId(qualifiedName)], [kind])
|
|
.map((e) => cg.getNode(e.source)!.qualifiedName)
|
|
.sort();
|
|
}
|
|
|
|
describe('VB.NET members read and written through a value', () => {
|
|
it('link each read and write of the issue to the member', () => {
|
|
expect(linkedFrom('Holder::SharedCache', 'references')).toEqual(['User::Run']);
|
|
expect(linkedFrom('Holder::Normal', 'references')).toEqual(['User::Run', 'User::Run', 'User::Run', 'User::Run']);
|
|
expect(linkedFrom('Holder::Title', 'references')).toEqual(['User::Run']);
|
|
expect(linkedFrom('Holder::EndSession', 'calls')).toEqual(['User::Run']);
|
|
expect(cg.getCallers(nodeId('Holder::Normal')).map((c) => c.node.qualifiedName)).toEqual(['User::Run']);
|
|
});
|
|
|
|
it('link the member alone: the type is not written at the read', () => {
|
|
expect(linkedFrom('Holder', 'references')).toEqual([]);
|
|
expect(linkedFrom('Node', 'references')).toEqual([]);
|
|
});
|
|
|
|
it('read through Me, MyBase and a path of members, one link at a time', () => {
|
|
expect(linkedFrom('BaseView::Caption', 'references')).toEqual(['DerivedView::New']);
|
|
expect(linkedFrom('BaseView::Count', 'references')).toEqual(['DerivedView::Count', 'DerivedView::Count']);
|
|
expect(linkedFrom('DerivedView::_node', 'references')).toEqual(['DerivedView::Count']);
|
|
expect(linkedFrom('Node::Child', 'references')).toEqual(['Builder::Fill', 'Builder::Fill', 'DerivedView::Count']);
|
|
});
|
|
|
|
it('index a field, run a method named without parentheses, name an event and a method', () => {
|
|
expect(linkedFrom('Node::Items', 'references')).toEqual(['Builder::Fill', 'DerivedView::Watch']);
|
|
expect(linkedFrom('Node::Describe', 'calls')).toEqual(['Builder::Fill', 'DerivedView::Watch']);
|
|
expect(linkedFrom('Node::Changed', 'references')).toEqual(['DerivedView::Watch']);
|
|
expect(linkedFrom('Node::Reset', 'references')).toEqual(['DerivedView::Watch']);
|
|
expect(linkedFrom('Node::Reset', 'calls')).toEqual(['Builder::Fill']);
|
|
});
|
|
|
|
it('read through With blocks, an object initializer, a cast, a loop variable, a Module variable and a type parameter', () => {
|
|
expect(linkedFrom('Node::Value', 'references')).toEqual([
|
|
'Builder::Fill', 'Builder::Fill', 'Builder::Seed', 'DerivedView::Count', 'DerivedView::Watch',
|
|
'Negatives::Shadowed', 'Repo::Peek', 'Repo::Peek',
|
|
]);
|
|
});
|
|
|
|
it('read the type the namespace around the declaration sees', () => {
|
|
expect(linkedFrom('API.Reddit::SiteSettings::Header', 'references')).toEqual(['API.Reddit::UserData::Key']);
|
|
expect(linkedFrom('API.Twitter::SiteSettings::Header', 'references')).toEqual([]);
|
|
});
|
|
|
|
it('link an initializer and a later write to the members the type declares or inherits', () => {
|
|
expect(linkedFrom('CommandLineParam::Switch', 'references')).toEqual(['x265Enc::ConstVBV']);
|
|
expect(linkedFrom('CommandLineParam::Text', 'references')).toEqual(['x265Enc::ConstVBV']);
|
|
expect(linkedFrom('BoolParam::Value', 'references')).toEqual(['x265Enc::Apply']);
|
|
});
|
|
|
|
it('read an instance member of a value named like its type through the value, a Shared one through the type', () => {
|
|
expect(linkedFrom('Theme::General', 'references')).toEqual(['Painter::Paint']);
|
|
expect(linkedFrom('Theme::Current', 'references')).toEqual(['Painter::Paint']);
|
|
expect(linkedFrom('Theme::Version', 'references')).toEqual(['Painter::Paint']);
|
|
// `theme.Current` and the Const `theme.Version`; not `theme.General`.
|
|
expect(linkedFrom('Theme', 'references')).toEqual(['Painter::Paint', 'Painter::Paint']);
|
|
expect(linkedFrom('Mode::Fast', 'references')).toEqual(['Painter::Paint']);
|
|
expect(linkedFrom('Mode', 'references')).toEqual(['Painter::Paint']);
|
|
});
|
|
|
|
it('read through a type whose name is in lower case', () => {
|
|
expect(linkedFrom('x265RateMode::Bitrate', 'references')).toEqual(['Painter::Paint']);
|
|
expect(linkedFrom('x265RateMode', 'references')).toEqual(['Painter::Paint']);
|
|
});
|
|
|
|
it('send no read for a qualified type after New, which the grammar splits into members', () => {
|
|
// `New System.Windows.Forms.ListBox()` parses as `.Windows.Forms` read through `New System`.
|
|
const reads = extractFromSource('Designer.vb', `Public Class Form1
|
|
Sub InitializeComponent()
|
|
Me.ListBox1 = New System.Windows.Forms.ListBox()
|
|
Dim h = New Sites.Holder
|
|
Dim n = New Holder().Normal
|
|
Dim p = DirectCast(sender, System.Windows.Forms.Control).Parent
|
|
End Sub
|
|
End Class
|
|
`).unresolvedReferences.filter((r) => r.referenceKind === 'references').map((r) => r.referenceName);
|
|
expect(reads).toEqual(['Me.ListBox1', '{Holder}.Normal']);
|
|
});
|
|
|
|
it('link nothing through an outside type, Object, an untyped name or an index', () => {
|
|
for (const member of ['Lookalike::Length', 'Lookalike::Count', 'Lookalike::Value']) {
|
|
expect(linkedFrom(member, 'references')).toEqual([]);
|
|
}
|
|
expect(linkedFrom('Holder::SharedCache', 'references')).not.toContain('Negatives::Shadowed');
|
|
});
|
|
});
|