mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 14:32:59 +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>
222 lines
8.3 KiB
TypeScript
222 lines
8.3 KiB
TypeScript
/**
|
|
* VB.NET: a value read or written through a type or module name —
|
|
* `AppSession.SessionId`, `AppSession.CurrentUser = "demo"`, `Logger.Level`,
|
|
* `Mode.Fast` — is a use of the member and of the type (#2305). Before, only
|
|
* calls linked, so `codegraph callers` on a Shared field or property, and on
|
|
* 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, 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
|
|
* a Shared field (`AppSession.Items(0)`), which VB.NET writes as a call, reads
|
|
* the field.
|
|
*/
|
|
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';
|
|
|
|
let root = '';
|
|
let cg: CodeGraph;
|
|
|
|
const files: Record<string, string> = {
|
|
// The issue's three files.
|
|
'AppSession.vb': `Public Class AppSession
|
|
Public Shared SessionId As Guid = Guid.NewGuid()
|
|
Public Shared Property CurrentUser As String
|
|
Public Shared Items As New List(Of String)
|
|
Public Shared Function GetGreeting() As String
|
|
Return "Hello " & CurrentUser
|
|
End Function
|
|
Public Shared Sub OnTick(ByVal sender As Object, ByVal e As EventArgs)
|
|
End Sub
|
|
Public Shared Function Describe() As String
|
|
Return AppSession.CurrentUser
|
|
End Function
|
|
End Class
|
|
`,
|
|
'Consumer.vb': `Public Class Consumer
|
|
Public Sub Run()
|
|
Dim id As Guid = AppSession.SessionId
|
|
AppSession.CurrentUser = "demo"
|
|
Console.WriteLine(AppSession.GetGreeting())
|
|
End Sub
|
|
End Class
|
|
`,
|
|
'Consumer2.vb': `Public Class Consumer2
|
|
Public Function Describe() As String
|
|
Return AppSession.CurrentUser & AppSession.SessionId.ToString()
|
|
End Function
|
|
End Class
|
|
`,
|
|
'OtherSession.vb': `Public Class OtherSession
|
|
Public Shared SessionId As Guid = Guid.NewGuid()
|
|
Public Property Size As Integer
|
|
End Class
|
|
`,
|
|
'Size.vb': `Public Class Size
|
|
End Class
|
|
`,
|
|
'Logger.vb': `Public Module Logger
|
|
Public Level As Integer
|
|
End Module
|
|
`,
|
|
'Mode.vb': `Public Enum Mode
|
|
Fast
|
|
Slow
|
|
End Enum
|
|
`,
|
|
'Form1.vb': `Public Class Form1
|
|
Private Panel1 As Panel
|
|
Public Sub Setup()
|
|
Logger.Level = 3
|
|
Dim a = Panel1.Size
|
|
Me.Panel1.Size = New System.Drawing.Size(1, 2)
|
|
Dim speed = Mode.Fast
|
|
Dim greeting = AppSession.GetGreeting
|
|
AddHandler Timer1.Tick, AddressOf AppSession.OnTick
|
|
Dim first = AppSession.Items(0)
|
|
Dim empty = String.Empty
|
|
End Sub
|
|
Public Sub Shadowed(ByVal appSession As OtherSession)
|
|
Dim id = AppSession.SessionId
|
|
End Sub
|
|
End Class
|
|
`,
|
|
// A type declared in each site's namespace, and one read through an import alias.
|
|
'Sites/Reddit/SiteSettings.vb': `Namespace API.Reddit
|
|
Friend Class SiteSettings
|
|
Friend Const Header As String = "r"
|
|
End Class
|
|
Friend Class UserData
|
|
Friend Function Key() As String
|
|
Return SiteSettings.Header
|
|
End Function
|
|
End Class
|
|
End Namespace
|
|
`,
|
|
'Sites/Twitter/SiteSettings.vb': `Namespace API.Twitter
|
|
Friend Class SiteSettings
|
|
Friend Const Header As String = "t"
|
|
End Class
|
|
Friend Class UserData
|
|
Friend Function Key() As String
|
|
Return SiteSettings.Header
|
|
End Function
|
|
End Class
|
|
End Namespace
|
|
`,
|
|
'Sites/Facebook/UserData.vb': `Imports RS = API.Reddit.SiteSettings
|
|
Namespace API.Facebook
|
|
Friend Class UserData
|
|
Friend Function Key() As String
|
|
Return RS.Header
|
|
End Function
|
|
End Class
|
|
End Namespace
|
|
`,
|
|
// A property typed as the type of its own name; a property initializer.
|
|
'Settings.vb': `Public Class Settings
|
|
Public Property Theme As String
|
|
End Class
|
|
`,
|
|
'Window.vb': `Public Class Window
|
|
Public Property Settings As Settings
|
|
Public Property Title As String = AppSession.CurrentUser
|
|
Public Function CurrentTheme() As String
|
|
Return Settings.Theme
|
|
End Function
|
|
End Class
|
|
`,
|
|
// CRLF line endings and multibyte text before the read.
|
|
'Weird.vb': [
|
|
'Public Class Weird',
|
|
' Public Sub Go()',
|
|
' Dim s = "SessionId éééé 😀" & AppSession.SessionId',
|
|
' End Sub',
|
|
'End Class',
|
|
'',
|
|
].join('\r\n'),
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-shared-'));
|
|
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`. */
|
|
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 Shared members read through their type (#2305)', () => {
|
|
it('link a Shared field and property to the methods that read or write them', () => {
|
|
expect(linkedFrom('AppSession::SessionId', 'references')).toEqual(['Consumer2::Describe', 'Consumer::Run', 'Weird::Go']);
|
|
expect(linkedFrom('AppSession::CurrentUser', 'references'))
|
|
.toEqual(['AppSession::Describe', 'Consumer2::Describe', 'Consumer::Run', 'Window::Title']);
|
|
});
|
|
|
|
it('link the class to the code outside it that reads through it', () => {
|
|
expect(linkedFrom('AppSession', 'references'))
|
|
.toEqual(['Consumer2::Describe', 'Consumer2::Describe', 'Consumer::Run', 'Consumer::Run', 'Form1::Setup', 'Weird::Go', 'Window::Title']);
|
|
expect(cg.getCallers(nodeId('AppSession')).map((c) => c.node.qualifiedName).sort())
|
|
.toEqual(['Consumer2::Describe', 'Consumer::Run', 'Form1::Setup', 'Weird::Go', 'Window::Title']);
|
|
});
|
|
|
|
it('keep the call to a Shared method, and call one named without parentheses', () => {
|
|
expect(linkedFrom('AppSession::GetGreeting', 'calls')).toEqual(['Consumer::Run', 'Form1::Setup']);
|
|
expect(linkedFrom('AppSession::OnTick', 'references')).toEqual(['Form1::Setup']);
|
|
expect(linkedFrom('AppSession::OnTick', 'calls')).toEqual([]);
|
|
});
|
|
|
|
it('read a Shared field indexed like a call, a Module variable and an Enum value', () => {
|
|
expect(linkedFrom('AppSession::Items', 'references')).toEqual(['Form1::Setup']);
|
|
expect(linkedFrom('Logger::Level', 'references')).toEqual(['Form1::Setup']);
|
|
expect(linkedFrom('Mode::Fast', 'references')).toEqual(['Form1::Setup']);
|
|
expect(linkedFrom('Mode', 'references')).toEqual(['Form1::Setup']);
|
|
});
|
|
|
|
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([]);
|
|
});
|
|
|
|
it('read the type the namespaces and imports around the read see', () => {
|
|
expect(linkedFrom('API.Reddit::SiteSettings::Header', 'references'))
|
|
.toEqual(['API.Facebook::UserData::Key', 'API.Reddit::UserData::Key']);
|
|
expect(linkedFrom('API.Twitter::SiteSettings::Header', 'references')).toEqual(['API.Twitter::UserData::Key']);
|
|
});
|
|
|
|
it('read through a member typed as the type of its own name', () => {
|
|
expect(linkedFrom('Settings::Theme', 'references')).toEqual(['Window::CurrentTheme']);
|
|
});
|
|
});
|