Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a store whose name contains a `$`, like `const items$ = create(…)` exported on a later line with `export default items$;`, now has its actions indexed. Before, they could be missing from the graph, for example in a file that also had a syntax error. Re-index JavaScript and TypeScript projects after upgrading.
- In Dart, what a top-level variable's or a field's initializer calls, creates and reads is now linked. In Riverpod apps, where most wiring lives in provider closures like `final repoProvider = Provider((ref) => Repository(ref.watch(dioProvider)));`, `callers`, impact and `codegraph_explore` now follow a provider to the code it builds instead of stopping at the provider. A `final` or `const` initializer belongs to its constant, and a `var`'s or a field's to its file or class. A call through a type's static constant, like `FutureProvider.autoDispose(…)`, now links to that constant instead of an unrelated method that shares its name. Re-index Dart projects after upgrading.
- In Dart, a call further down a chain, like Riverpod's `FutureProvider.autoDispose.family(…)` or `events.map(mapper).transform(…)` on a stream, now links to the method of the type the chain before it produces, worked out from what each step is declared to return, and to nothing when that type can't be worked out. Before, it linked to whichever project method shared its name, so `X.autoDispose.family(…)` reached the same provider builder whatever `X` was, and a stream's `transform` reached an unrelated `transform` method. `super.` calls now reach the superclass's method, `context.read<CounterBloc>().add(…)` reaches the bloc's own `add`, and the members of an extension marked `@internal` are found too. Re-index Dart projects after upgrading. (#750)
- In VB.NET, reading or setting a field or property through a variable, parameter, field, `Me` or `MyBase`, like `x.Normal = 3`, `Me._h.Title`, `.Value` inside a `With` block or `.Switch` in `New BoolParam With {…}`, now counts as a use of that member, and so do calls inside a `With` block, functions called without parentheses and reads through a class whose name starts in lower case. Before, `codegraph callers` on an instance field or property missed most of its uses. Only a member that the value's declared type has or inherits is linked, nothing when that type comes from outside your project, and a value named after its class, like `theme As Theme`, counts as a use of the class only when it reads a `Shared` member. Re-index VB.NET projects after upgrading.

## [1.6.2] - 2026-10-03

Expand Down
338 changes: 338 additions & 0 deletions __tests__/vbnet-instance-member-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
/**
* 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');
});
});
13 changes: 8 additions & 5 deletions __tests__/vbnet-shared-member-refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
* the class itself, came back empty.
*
* The name is looked up as VB.NET does, without regard to case: a local, a
* parameter or a field of that name holds a value, and links nothing here (a
* member typed as its own name's type, `Property Settings As Settings`, reads
* that type's members either way); a type is the one the namespaces around
* parameter or a field of that name holds a value, read as the type it is
* declared as (vbnet-instance-member-refs.test.ts; a member typed as its own
* name's type, `Property Settings As Settings`, reads that type's members
* either way); a type is the one the namespaces around
* the read, its `Imports` and aliases see — SCrawler declares a
* `SiteSettings` in every site's namespace. A method named without
* parentheses is called, unless `AddressOf` only names it, and an index into
Expand Down Expand Up @@ -199,8 +200,10 @@ describe('VB.NET Shared members read through their type (#2305)', () => {
expect(linkedFrom('Mode', 'references')).toEqual(['Form1::Setup']);
});

it('link nothing through a local, parameter or field, whatever its case', () => {
expect(linkedFrom('OtherSession::SessionId', 'references')).toEqual([]);
it('read through a local, parameter or field, whatever its case, the type it holds', () => {
// `AppSession.SessionId` in `Shadowed(ByVal appSession As OtherSession)`
// reads the parameter: OtherSession's field, never AppSession's.
expect(linkedFrom('OtherSession::SessionId', 'references')).toEqual(['Form1::Shadowed']);
expect(linkedFrom('OtherSession::Size', 'references')).toEqual([]);
expect(linkedFrom('Size', 'references')).toEqual([]);
expect(linkedFrom('OtherSession', 'references')).toEqual([]);
Expand Down
Loading