From cfbc1d48e36571aac2324df49f8d0d171a75d81f Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 6 Oct 2026 09:36:16 -0500 Subject: [PATCH] fix(vbnet): link fields and properties read or written through a value 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 --- CHANGELOG.md | 1 + __tests__/vbnet-instance-member-refs.test.ts | 338 +++++++++++++++++++ __tests__/vbnet-shared-member-refs.test.ts | 13 +- src/extraction/tree-sitter.ts | 155 +++++++-- src/resolution/index.ts | 17 +- src/resolution/name-matcher.ts | 2 +- src/resolution/vbnet-receivers.ts | 272 +++++++++++++-- 7 files changed, 729 insertions(+), 69 deletions(-) create mode 100644 __tests__/vbnet-instance-member-refs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5216219e9..ca9d61015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In VB.NET, a name that begins with a keyword, like `SharedCache`, `Dimension`, `PublicKey` or `FriendlyName`, is now indexed whole, and a method like `Sub NewItem()` is no longer taken for a constructor. Before, the keyword was cut off (`Cache`, `ension`, `Key`) or the declaration was lost, and a line in a method that starts with such a name, like `Constants.Reload()` or `ConstVBV.Value = False`, was misread as a declaration, so reads of and calls to these names didn't link. A `Private Protected` member now counts as private instead of public. Re-index VB.NET projects after upgrading. - 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 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 diff --git a/__tests__/vbnet-instance-member-refs.test.ts b/__tests__/vbnet-instance-member-refs.test.ts new file mode 100644 index 000000000..b7199a160 --- /dev/null +++ b/__tests__/vbnet-instance-member-refs.test.ts @@ -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 = { + // 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'); + }); +}); diff --git a/__tests__/vbnet-shared-member-refs.test.ts b/__tests__/vbnet-shared-member-refs.test.ts index bbcfa77ff..21a5b40fd 100644 --- a/__tests__/vbnet-shared-member-refs.test.ts +++ b/__tests__/vbnet-shared-member-refs.test.ts @@ -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 @@ -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([]); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index da8dc9bbe..893fd07aa 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -381,8 +381,8 @@ const MEMBER_ACCESS_TYPES: ReadonlySet = new Set([ * static read is pure duplication) — while adding real graph noise (+1813 edges / * +2448 `references` on excalidraw, the retrieval-perf benchmark, all pointing at * already-covered types). Don't re-add `member_expression`/`attribute` here. - * VB.NET (#2305) sends the member with its receiver instead, and its resolver - * decides whether the receiver is a type (see extractVbMemberRead). + * VB.NET (#2305) sends every member read with its receiver instead, and its + * resolver decides whether the receiver is a type (see extractVbMemberRead). * Rust imports too, but a variant is usually written through a path that names * its module, not its enum (`mode::Mode::A` under `use crate::mode;`), so no * `use` names the enum at all (#2328). @@ -403,7 +403,8 @@ const RUST_NON_MEMBER_PATH_PARENTS: ReadonlySet = new Set([ /** * VB.NET receivers no project type can be named: the namespace roots * (`System.IO.Path`, `My.Settings`, `Global.X`) and the built-in type keywords - * (`String.Empty`, `Integer.MaxValue`). A read through one is never sent. + * (`String.Empty`, `Integer.MaxValue`). A read through one, or a path that + * starts with one, is never sent. */ const VB_NON_TYPE_RECEIVERS = /^(?:Global|System|Microsoft|My|Boolean|Byte|Char|Date|Decimal|Double|Integer|Long|Object|SByte|Short|Single|String|UInteger|ULong|UShort)$/i; @@ -4531,6 +4532,15 @@ export class TreeSitterExtractor { } } else if (fn.type === 'identifier') { calleeName = getNodeText(fn, this.source); + } else if (fn.type === 'with_member_access_expression') { + // `.Run()` in a `With x` block is `x.Run()`; through a longer path + // (`Me._h`, `{T}` for `With DirectCast(o, T)`) the resolver types each link. + const member = fn.namedChild(0); + const target = this.vbWithTarget(fn); + const receiver = target ? this.vbReceiverPath(target) : null; + if (member?.type !== 'identifier' || !receiver || VB_NON_TYPE_RECEIVERS.test(receiver.split('.')[0]!)) return; + const memberName = getNodeText(member, this.source); + calleeName = /^(?:me|mybase|myclass)$/i.test(receiver) ? memberName : `${receiver}.${memberName}`; } else { return; // parenthesized/chained receivers: no static name to link } @@ -5809,6 +5819,11 @@ export class TreeSitterExtractor { return; } + if (this.language === 'vbnet') { + this.extractVbMemberRead(node, knownParent, ownerId); + return; + } + if (!MEMBER_ACCESS_TYPES.has(node.type)) return; // Skip `Type.method()` — the access is the callee of a call, already linked. @@ -5830,10 +5845,6 @@ export class TreeSitterExtractor { getChildByField(node, 'scope') ?? node.namedChild(0); if (!recv) return; - if (this.language === 'vbnet') { - this.extractVbMemberRead(node, recv, ownerId); - return; - } const t = recv.type; if ( t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' || @@ -5855,30 +5866,130 @@ export class TreeSitterExtractor { } /** - * VB.NET: a value read or write through a name — `AppSession.SessionId`, - * `AppSession.CurrentUser = "demo"`, `Logger.Level`, `Mode.Fast` — is a use - * of the member as well as of what the name names (#2305). One `references` - * ref carries both, as `Name.Member` (the receiver kept, as a call's is); - * the resolver links the member and the type when the name means a project - * type or module there, and nothing when it holds a value (a local, a - * parameter, a field), which only it can tell in case-insensitive VB.NET - * (see vbnet-receivers' matchVbMemberRead). Its types are Capitalized all - * the same, so a lowercase receiver — a local, nearly always — is skipped. + * VB.NET: a value read or write through a receiver — `AppSession.SessionId`, + * `AppSession.CurrentUser = "demo"`, `x.Normal = 3`, `Me._h.Title`, `.Value` + * in a `With` block, `.Switch` in `New BoolParam With {.Switch = "--x"}` — + * is a use of the member, and through a type name of the type (#2305). One + * `references` ref carries the member with its receiver as a path: + * `x.Normal`, `Me._h.Title`, or `{T}.Switch` for a value whose type the code + * writes (a cast's, a `New`'s). The resolver decides what the receiver is — + * a project type, or a value of the type it is declared as — which only it + * can tell in case-insensitive VB.NET (see vbnet-receivers' + * matchVbMemberRead). A call through the access is the call extractor's. */ - private extractVbMemberRead(node: SyntaxNode, recv: SyntaxNode, ownerId: string): void { - const member = getChildByField(node, 'member'); - if (recv.type !== 'identifier' || member?.type !== 'identifier') return; - const name = getNodeText(recv, this.source); - if (!/^[A-Z]\w*$/.test(name) || VB_NON_TYPE_RECEIVERS.test(name)) return; + private extractVbMemberRead(node: SyntaxNode, knownParent: SyntaxNode | undefined, ownerId: string): void { + let receiver: string | null; + let member: SyntaxNode | null; + if (node.type === 'member_access_expression') { + member = getChildByField(node, 'member'); + const object = getChildByField(node, 'object'); + receiver = object && !this.isVbSplitNew(object) ? this.vbReceiverPath(object) : null; + } else if (node.type === 'with_member_access_expression') { + member = node.namedChild(0); + const target = this.vbWithTarget(node); + receiver = target ? this.vbReceiverPath(target) : null; + } else if (node.type === 'member_initializer') { + member = getChildByField(node, 'name'); + const creation = (knownParent ?? node.parent)?.parent; + receiver = creation?.type === 'object_creation_expression' ? this.vbReceiverPath(creation) : null; + } else return; + if (!receiver || member?.type !== 'identifier' || VB_NON_TYPE_RECEIVERS.test(receiver.split('.')[0]!)) return; + // `x.Method()`: the access is the callee of a call, which the call extractor links. + const parent = node.type === 'member_initializer' ? null : knownParent ?? node.parent; + if (parent && this.extractor!.callTypes.includes(parent.type)) { + const callee = getChildByField(parent, 'function') ?? getChildByField(parent, 'method') ?? parent.namedChild(0); + if (callee && callee.startIndex === node.startIndex) return; + } this.unresolvedReferences.push({ fromNodeId: ownerId, - referenceName: `${name}.${getNodeText(member, this.source)}`, + referenceName: `${receiver}.${getNodeText(member, this.source)}`, referenceKind: 'references', line: node.startPosition.row + 1, column: node.startPosition.column, }); } + /** + * A VB.NET receiver as the resolver reads it: a name, `Me` / `MyClass` / + * `MyBase`, a member of one (`Me._h`, `user.Settings`), a `With` block's + * `.Inner`, or `{T}` for a value whose type the code writes there + * (`DirectCast(o, T)`, `New T(…)`). Null for anything else — an index, a + * call's result, a built-in type — which no declaration here types. + */ + private vbReceiverPath(node: SyntaxNode, depth = 0): string | null { + if (depth > 6) return null; + switch (node.type) { + case 'identifier': + return getNodeText(node, this.source); + case 'me_expression': + return 'Me'; + case 'myclass_expression': + return 'MyClass'; + case 'mybase_expression': + return 'MyBase'; + case 'parenthesized_expression': { + const inner = node.namedChild(0); + return inner ? this.vbReceiverPath(inner, depth + 1) : null; + } + case 'member_access_expression': + case 'with_member_access_expression': { + const through = node.type === 'member_access_expression'; + const member = through ? getChildByField(node, 'member') : node.namedChild(0); + if (member?.type !== 'identifier') return null; + const object = through ? getChildByField(node, 'object') : this.vbWithTarget(node); + if (through && object && this.isVbSplitNew(object)) return null; + const head = object ? this.vbReceiverPath(object, depth + 1) : null; + return head ? `${head}.${getNodeText(member, this.source)}` : null; + } + case 'cast_expression': + case 'object_creation_expression': { + let type = getChildByField(node, 'type'); + // `New T()` parses as an array type whose rank is the constructor's empty parentheses. + if (node.type === 'object_creation_expression' && type?.type === 'array_type') { + const rank = type.namedChildren.find((c: SyntaxNode) => c.type === 'array_rank_specifier'); + type = rank && /^\(\s*\)$/.test(getNodeText(rank, this.source)) ? getChildByField(type, 'element_type') : null; + } + if (!type || type.type === 'predefined_type' || type.type === 'array_type') return null; + const written = getNodeText(type, this.source).replace(/\s+/g, ' '); + // .NET's own types (`System.Drawing.Point`) are never the project's. + return /^(?:System|Microsoft|My)\./i.test(written) ? null : `{${written}}`; + } + default: + return null; + } + } + + /** + * Whether `node`, a member access's object, is `New System` of `New + * System.Drawing.Size(…)` or `New Ns` of `New Ns.Holder`: the grammar reads + * a qualified type after `New` as members read through a `New` of its first + * name. A value's `New` has its parentheses (`New Holder().Normal`). + */ + private isVbSplitNew(node: SyntaxNode): boolean { + return node.type === 'object_creation_expression' && !getChildByField(node, 'arguments') && + !getChildByField(node, 'initializer') && getChildByField(node, 'type')?.type !== 'array_type'; + } + + /** + * What a VB.NET `.Member` is read through: the expression of the `With` + * block it is in (a nested block's own `With .Inner` is read through the + * block around it), or the object an initializer sets up (`New T With {.X = + * 1}`). Null in an anonymous type's `New With {.X = 1}`, which declares X. + */ + private vbWithTarget(node: SyntaxNode): SyntaxNode | null { + let child = node; + for (let p = node.parent; p; child = p, p = p.parent) { + if (p.type === 'with_statement') { + const expression = getChildByField(p, 'expression'); + if (expression && expression.startIndex === child.startIndex && expression.endIndex === child.endIndex) continue; + return expression; + } + if (p.type === 'object_initializer') return p.parent?.type === 'object_creation_expression' ? p.parent : null; + if (p.type === 'anonymous_object_creation_expression') return null; + } + return null; + } + /** * Find a `class_body` child of an `object_creation_expression` — the * marker for an anonymous class (`new T() { ... }`). Returns the body diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 7364c3e25..c3c06fadf 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -21,11 +21,11 @@ import { isImportableKind, CPP_DEFINE_SIGNATURE, } from './types'; -import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead } from './name-matcher'; +import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isStdMethodName } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility'; -import { clearVbnetReceiverMemos, isVbMemberRead, matchVbMemberRead } from './vbnet-receivers'; +import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers'; import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters'; import { resolveViaImport, resolvePhpImportedStaticCall, resolvePhpQualifiedClassRef, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isJsPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver'; import { ResolverPool, minRefsForPool, shouldEngageAdaptively } from './resolver-pool'; @@ -1200,11 +1200,16 @@ export class ReferenceResolver { return this.gateLanguage(matchJsStoreBindingCall(ref, this.context), ref); } - // A VB.NET value read through a name (`AppSession.SessionId`, #2305) means - // what VB.NET's scoping says the name is — a project type, whose member - // and the type itself it links, or a value, which links nothing here — and - // no framework, import or name strategy guesses past that. + // A VB.NET member read (`AppSession.SessionId`, #2305; `x.Normal`, + // `Me._h.Title`) means what VB.NET's scoping says its receiver is — a + // project type, whose member and the type itself it links, or a value, + // whose declared type's member it links — and no framework, import or + // name strategy guesses past that. Nor past a `With` block's call through + // a receiver path (`.Run()` in `With Me._h`). if (isVbMemberRead(ref)) return this.gateLanguage(matchVbMemberRead(ref, this.context), ref); + if (isVbPathCall(ref)) { + return this.gateLanguage(matchVbPathCall(ref, this.context, (name) => isStdMethodName('vbnet', name)), ref); + } // Function-as-value refs (#756) get a dedicated, strictly-gated path: // import-based resolution first (an imported callback resolves through its diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c7d7a0e07..1492ce24b 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -3362,7 +3362,7 @@ function stdMethodNames(language: string): ReadonlySet | null { } /** Whether `name` is one of `language`'s standard-library method names (VB.NET's in any case). */ -function isStdMethodName(language: string, name: string): boolean { +export function isStdMethodName(language: string, name: string): boolean { return stdMethodNames(language)?.has(language === 'vbnet' ? name.toLowerCase() : name) ?? false; } diff --git a/src/resolution/vbnet-receivers.ts b/src/resolution/vbnet-receivers.ts index f252dfc97..b78704a85 100644 --- a/src/resolution/vbnet-receivers.ts +++ b/src/resolution/vbnet-receivers.ts @@ -17,7 +17,9 @@ * `Dim x = obj.GetString(…)` returns. A typed receiver's call is the * type's own method or one it inherits, else an extension method declared * for the type, else nothing of the project's: a type the project does - * not define (`SFile`, `String`, `List(Of T)`) has none of its methods. + * not define (`SFile`, `String`, `List(Of T)`) has none of its methods. A + * member read through it (`x.Normal`, `Me._h.Title`, a `With` block's + * `.Value`) is the type's member in the same way, link by link. * 2. Which type a name means where it is written: the namespaces around it, * outward, then its file's and project's `Imports` (aliases included), * then the project a file belongs to — the `.vbproj` above it, whose @@ -437,16 +439,46 @@ function projectTypesNamed(name: string, context: ResolutionContext, kinds: Read return context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && kinds.has(n.kind)); } +const VB_FILE_QNS = new WeakMap>>(); + +/** A file's VB.NET nodes by lowercased qualified name, indexed once. */ +function nodesByQualifiedName(file: string, context: ResolutionContext): Map { + let memo = VB_FILE_QNS.get(context); + if (!memo) VB_FILE_QNS.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const index = new Map(); + for (const n of context.getNodesInFile(file)) { + if (n.language !== 'vbnet') continue; + const key = n.qualifiedName.toLowerCase(); + const list = index.get(key); + if (list) list.push(n); + else index.set(key, [n]); + } + if (memo.size >= 1024) memo.delete(memo.keys().next().value!); + memo.set(file, index); + return index; +} + const VB_MEMBERS = new WeakMap>(); -/** Members named `name` declared directly in `type` (any partial part of it), case aside. */ +/** + * Members named `name` declared directly in `type` (any partial part of it), + * case aside. A member is declared inside a part of its type, so it is looked + * up in those parts' files: one lookup per type name, not one per member name. + */ function membersNamed(type: Node, name: string, context: ResolutionContext): Node[] { let memo = VB_MEMBERS.get(context); if (!memo) VB_MEMBERS.set(context, (memo = new Map())); const qn = `${type.qualifiedName}::${name}`.toLowerCase(); const hit = memo.get(qn); if (hit) return hit; - const members = context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && n.qualifiedName.toLowerCase() === qn); + const own = type.qualifiedName.toLowerCase(); + const files = new Set([type.filePath]); + for (const part of context.getNodesByLowerName(type.name.toLowerCase())) { + if (part.language === 'vbnet' && part.qualifiedName.toLowerCase() === own) files.add(part.filePath); + } + const members = [...files].flatMap((file) => nodesByQualifiedName(file, context).get(qn) ?? []); if (memo.size >= 65536) memo.delete(memo.keys().next().value!); memo.set(qn, members); return members; @@ -1253,9 +1285,31 @@ export function matchVbTypedCall( const owners = ownersOf(type, context); // A type name that leaves two of the project's types: no guess between them. if (owners === null) return null; + return callOnValue(type, owners, method, ref, context, isStdMethod); +} + +/** + * A call of `method` on a value of type `type` (`owners`, the project type + * it names; none for an outside type): the type's own method or one it + * inherits; an index into its field or property (`x.Items(0)`), which VB.NET + * writes as a call, reads the member; else an extension method for the type. + */ +function callOnValue( + type: VbType, + owners: Node[], + method: string, + ref: UnresolvedRef, + context: ResolutionContext, + isStdMethod: (name: string) => boolean, +): ResolvedRef | null { if (owners.length > 0) { const own = memberOn(owners, method, ref, context, false, type); if (own) return { original: ref, targetNodeId: own.node.id, confidence: 0.9, resolvedBy: 'instance-method' }; + const indexed = memberOn(owners, method, ref, context, true, type); + if (indexed) { + return indexed.node.id === ref.fromNodeId ? null + : { original: ref, targetNodeId: indexed.node.id, confidence: 0.9, resolvedBy: 'instance-method', edgeKind: 'references' }; + } } return extensionFor(type, owners, method, ref, context, isStdMethod); } @@ -1270,30 +1324,129 @@ const VB_READ_TYPE_KINDS: ReadonlySet = new Set([...VB_TYPE_KINDS, 'enum */ const VB_READ_KINDS: ReadonlySet = new Set([...VB_VALUE_KINDS, 'enum_member', 'method', ...VB_TYPE_LIKE_KINDS]); -/** `Name.Member`, as the extractor sends a VB.NET value read through a name (extractVbMemberRead). */ -const VB_MEMBER_READ = /^([A-Za-z_]\w*)\.(\[?[A-Za-z_]\w*\]?)$/; +/** What a read through a value names: a field, property, constant or event of its type, or a method it runs. */ +const VB_INSTANCE_READ_KINDS: ReadonlySet = new Set([...VB_VALUE_KINDS, 'method']); + +/** Members that belong to a type, not an instance, besides those declared `Shared`: a constant, an `Enum` case, a nested type. */ +const VB_SHARED_READ_KINDS: ReadonlySet = new Set(['constant', 'enum_member', ...VB_TYPE_LIKE_KINDS]); + +/** + * Whether a member belongs to its type rather than to an instance: `Shared`, + * an `Enum` case, a nested type, or a `Const` — Shared without saying so, + * which a VB.NET field's node doesn't record, so its declaration is read. + */ +function isSharedMember(n: Node, context: ResolutionContext): boolean { + if (n.isStatic || VB_SHARED_READ_KINDS.has(n.kind)) return true; + if (n.kind !== 'field') return false; + const code = codeLines(n.filePath, context).slice(n.startLine - 1, n.startLine + 2).join(' '); + return new RegExp(`\\bConst\\s+(?:[^=]*,\\s*)?${escapeRegex(n.name)}(?![\\w$%&!#@])`, 'i').test(code); +} -/** Whether a reference is a VB.NET value read through a name, which matchVbMemberRead alone resolves. */ +/** + * A member read (or a `With` block's call) as the extractor sends it + * (extractVbMemberRead): the receiver — a name, `Me` / `MyClass` / `MyBase`, + * or `{T}` for a value of a type the code writes — then each member read + * through it, the last the one used: `AppSession.SessionId`, `x.Normal`, + * `Me._h.Title`, `{BoolParam}.Switch`. + */ +const VB_MEMBER_PATH = /^(\{[^{}]+\}|\[?[A-Za-z_]\w*\]?)((?:\.\[?[A-Za-z_]\w*\]?)*)\.(\[?[A-Za-z_]\w*\]?)$/; + +/** Whether a reference is a VB.NET member read through a receiver, which matchVbMemberRead alone resolves. */ export function isVbMemberRead(ref: UnresolvedRef): boolean { - return ref.language === 'vbnet' && ref.referenceKind === 'references' && VB_MEMBER_READ.test(ref.referenceName); + return ref.language === 'vbnet' && ref.referenceKind === 'references' && VB_MEMBER_PATH.test(ref.referenceName); } /** - * The project types `name` means where a value is read through it - * (`AppSession` in `AppSession.SessionId`): a class, module, structure, - * interface or enum. Null when the name holds a value there — a local, a - * parameter, a member of a type around the read or of a module — or names - * no project type, or two. A member typed as the type of its own name - * (`Public Property Settings As Settings`) reaches that type's members - * either way, as VB.NET's "Color Color" rule has it. + * Whether a reference is a VB.NET call through a receiver path, which only a + * `With` block sends (`.Run()` in `With Me._h`, `With DirectCast(o, T)`) and + * matchVbPathCall alone resolves. A call through a name (`x.Run`) is the + * name matcher's, which types it with matchVbTypedCall. */ -function typesReadThrough(name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] | null { +export function isVbPathCall(ref: UnresolvedRef): boolean { + if (ref.language !== 'vbnet' || ref.referenceKind !== 'calls') return false; + const m = VB_MEMBER_PATH.exec(ref.referenceName); + return m !== null && (m[2] !== '' || m[1]!.startsWith('{') || /^(?:Me|MyClass|MyBase)$/i.test(m[1]!)); +} + +/** What a receiver path reaches: the project type whose members are read through it, and how. */ +interface VbReceiver { + /** The parts of the project type the receiver names or holds a value of; none for an outside type. */ + owners: Node[]; + /** The type a value is declared as, with the type arguments it gives; absent through a type's name. */ + typed?: VbType; + /** The receiver names a type (`AppSession`, `Outer.Mode`), whose own members are read, not a value. */ + throughType: boolean; + /** The name is a value whose type has the same name (`theme As Theme`): VB.NET's "Color Color" rule. */ + colorColor?: boolean; +} + +/** `Name` for an escaped `[Name]`. */ +function unescaped(name: string): string { + return name.replace(/^\[(.*)\]$/, '$1'); +} + +/** A receiver holding a value of type `t`; null when that type isn't known, is `Object` (late-bound), or names two types. */ +function valueReceiver(t: VbType | null | undefined, context: ResolutionContext): VbReceiver | null { + if (!t || typeKey(t) === 'object') return null; + const owners = ownersOf(t, context); + return owners ? { owners, typed: t, throughType: false } : null; +} + +/** + * What a receiver path's first link is where a member is read through it: + * `Me` / `MyClass` the type around the read, `MyBase` the class that type + * inherits, `{T}` a value of `T`. A name holds a value of its declared type + * when it is a local, a parameter, or a member of a type around the read or + * of a Module; any other name means the class, module, structure, interface + * or enum it names there. A value whose type has its name (`Public Property + * Settings As Settings`, `theme As Theme`) may mean either, by VB.NET's + * "Color Color" rule: the member read through it decides (matchVbMemberRead). + */ +function receiverAt(head: string, ref: UnresolvedRef, context: ResolutionContext): VbReceiver | null { + if (head.startsWith('{')) { + const owner = context.getNodeById?.(ref.fromNodeId) ?? null; + return valueReceiver(resolveTypeParameter(sited(readType(head.slice(1, -1), 0), ref.filePath, ref.line), owner, context), context); + } + if (/^(?:Me|MyClass|MyBase)$/i.test(head)) { + const own = typesAround(ref.filePath, ref.line, context)[0]; + if (!own) return null; + if (/^MyBase$/i.test(head)) return valueReceiver(supertypesOf(own, context).find((s) => !s.implemented), context); + return { owners: [own], typed: { name: own.name, array: false }, throughType: false }; + } + const name = unescaped(head); const bound = receiverType(name, ref, context, 0); const written: VbType | null = bound === undefined ? { name, array: false, file: ref.filePath, line: ref.line } : bound && !bound.array && bound.name.toLowerCase() === name.toLowerCase() ? bound : null; - if (!written) return null; + if (!written) return valueReceiver(bound, context); const found = typesNamedAt(written, context, VB_READ_TYPE_KINDS); - return found.ambiguous || found.owners.length === 0 ? null : found.owners; + return found.ambiguous || found.owners.length === 0 ? null + : { owners: found.owners, throughType: true, ...(bound ? { colorColor: true } : {}) }; +} + +/** + * What `name`, read through `at`, is as a receiver in turn: a value of the + * type a field, property or function declares, or a type nested in a type + * named for it (`Outer.Mode`). Null where the member, or its type, is not + * known here. + */ +function memberAt(at: VbReceiver, name: string, ref: UnresolvedRef, context: ResolutionContext): VbReceiver | null { + if (at.owners.length === 0) return null; + const fits = (n: Node) => VB_INSTANCE_READ_KINDS.has(n.kind) || (at.throughType && VB_READ_TYPE_KINDS.has(n.kind)); + const found = memberOn(at.owners, name, ref, context, true, at.typed, fits) ?? + memberOn(at.owners, `[${name}]`, ref, context, true, at.typed, fits); + if (!found) return null; + if (VB_READ_TYPE_KINDS.has(found.node.kind)) return { owners: [found.node], throughType: true }; + return valueReceiver(valueMemberType(found.node, context, found.args), context); +} + +/** What the receiver path `head.links…` reaches (see receiverAt and memberAt), or null where a link has no type here. */ +function pathReceiver(head: string, links: string[], ref: UnresolvedRef, context: ResolutionContext): VbReceiver | null { + let at = receiverAt(head, ref, context); + for (const link of links) { + if (!at) return null; + at = memberAt(at, unescaped(link), ref, context); + } + return at; } /** @@ -1330,43 +1483,92 @@ function namesWithoutRunning(ref: UnresolvedRef, context: ResolutionContext): bo } /** - * Resolve a VB.NET value read or write through a name (#2305) — - * `AppSession.SessionId`, `AppSession.CurrentUser = "demo"`, `Logger.Level`, - * `Mode.Fast` — to the member that the type the name means declares or - * inherits, and to the type. A method named this way is called (no type is - * linked, as for a call written with parentheses), unless `AddressOf` or - * `NameOf` only names it. Null when the name holds a value there (an - * instance's members are not read through its type), names no project type, - * or names one without that member: `Color.Red` is not a project `Color`'s. + * Resolve a VB.NET member read or write to the member the receiver's type + * declares or inherits. Through a type's name (#2305) — `AppSession.SessionId`, + * `AppSession.CurrentUser = "demo"`, `Logger.Level`, `Mode.Fast` — the read + * links the type as well, and a `Shared` member before a same-named instance + * one. Through a value — `x.Normal = 3` on a parameter `x As Holder`, + * `Me._h.Title`, a `With` block's `.Value`, an initializer's `.Switch` — it + * links the member of the type the value is declared as, whatever that + * type's name; never a member of the type the receiver's own name would + * mean. A value named like its type (`theme As Theme`) reads an instance + * member as a value does and a Shared one as the type does, by VB.NET's + * "Color Color" rule. A method named either way is called, as without + * parentheses VB.NET runs it, unless `AddressOf` or `NameOf` only names it. + * Null when a link of the path is not typed here (an outside type: + * `Color.Red`, `str.Length`), is late-bound (`Object`), or names two of the + * project's types. */ export function matchVbMemberRead(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { - const m = VB_MEMBER_READ.exec(ref.referenceName); + const m = VB_MEMBER_PATH.exec(ref.referenceName); if (!m) return null; - const owners = typesReadThrough(m[1]!, ref, context); - if (!owners) return null; + const links = m[2] ? m[2].slice(1).split('.') : []; + const at = pathReceiver(m[1]!, links, ref, context); + if (!at || at.owners.length === 0) return null; + // `MySettings.Default` reads the property declared `[Default]`, and back. + const name = unescaped(m[3]!); + const calls = (member: Node) => member.kind === 'method' && !namesWithoutRunning(ref, context); + const throughValue = (member: Node): ResolvedRef => ({ + original: ref, + targetNodeId: member.id, + confidence: 0.9, + resolvedBy: 'instance-method', + ...(calls(member) ? { edgeKind: 'calls' as const } : {}), + }); + if (!at.throughType) { + // The member the name means: the nearest the type declares or inherits. + // The read's own member (`Return Me.Count` in `Count`) runs itself. + const fits = (n: Node) => VB_INSTANCE_READ_KINDS.has(n.kind); + const member = (memberOn(at.owners, name, ref, context, true, at.typed, fits) ?? + memberOn(at.owners, `[${name}]`, ref, context, true, at.typed, fits))?.node; + return member && member.id !== ref.fromNodeId ? throughValue(member) : null; + } // Not the member the read is written in: staxrip's `Overrides ReadOnly // Property Package` returns the class's `Shared ReadOnly Property Package`. const fits = (n: Node) => VB_READ_KINDS.has(n.kind) && n.id !== ref.fromNodeId; - // `MySettings.Default` reads the property declared `[Default]`, and back. - const name = m[2]!.replace(/^\[(.*)\]$/, '$1'); - let member = (memberOn(owners, name, ref, context, true, undefined, fits) ?? - memberOn(owners, `[${name}]`, ref, context, true, undefined, fits))?.node; + let member = (memberOn(at.owners, name, ref, context, true, undefined, fits) ?? + memberOn(at.owners, `[${name}]`, ref, context, true, undefined, fits))?.node; if (!member) return null; // Of a `Shared` member and an instance one of the same name, a type's name reads the `Shared` one. if (!member.isStatic) { const found = member; member = context.getNodesByQualifiedName(found.qualifiedName).find((n) => n.isStatic && n.filePath === found.filePath && fits(n)) ?? found; } + // "Color Color": a value named like its type (`theme As Theme`) reads an + // instance member through itself, and only a Shared one through the type. + if (at.colorColor && !isSharedMember(member, context)) return throughValue(member); if (member.kind === 'method') { return { original: ref, targetNodeId: member.id, confidence: 0.85, resolvedBy: 'qualified-name', - ...(namesWithoutRunning(ref, context) ? {} : { edgeKind: 'calls' as const }), + ...(calls(member) ? { edgeKind: 'calls' as const } : {}), }; } - return readThrough(member, owners, ref, context, { confidence: 0.9 }); + // A type named through another (`Outer.Mode.Fast`) is linked by the read + // that names it (`Outer.Mode`), so only a type named itself is linked here. + return links.length === 0 ? readThrough(member, at.owners, ref, context, { confidence: 0.9 }) + : { original: ref, targetNodeId: member.id, confidence: 0.9, resolvedBy: 'qualified-name' }; +} + +/** + * Resolve a VB.NET call through a receiver path, which a `With` block sends + * for its `.Run()` (`With Me._h`, `With user.Settings`, `With + * DirectCast(o, T)`), as matchVbTypedCall resolves one through a typed name: + * the method the path's type has or inherits, a read of a field or property + * it indexes, or an extension method for the type. Null — never a guess by + * the method's name — when a link of the path is not typed here. + */ +export function matchVbPathCall( + ref: UnresolvedRef, + context: ResolutionContext, + isStdMethod: (name: string) => boolean, +): ResolvedRef | null { + const m = VB_MEMBER_PATH.exec(ref.referenceName); + if (!m) return null; + const at = pathReceiver(m[1]!, m[2] ? m[2].slice(1).split('.') : [], ref, context); + return at?.typed && !at.throughType ? callOnValue(at.typed, at.owners, unescaped(m[3]!), ref, context, isStdMethod) : null; } const VB_TYPE_IMPORTS = new WeakMap>>(); @@ -1445,7 +1647,7 @@ export function isVbMemberInScope(n: Node, ref: UnresolvedRef, context: Resoluti * sync never reads a changed file's old declarations. */ export function clearVbnetReceiverMemos(context: ResolutionContext): void { - for (const memo of [VB_CODE_LINES, VB_WORD_LINES, VB_BINDINGS, VB_FILE_TYPES, VB_MEMBERS, VB_MODULES, VB_SUPERS, VB_ANCESTRIES, + for (const memo of [VB_CODE_LINES, VB_WORD_LINES, VB_BINDINGS, VB_FILE_TYPES, VB_FILE_QNS, VB_MEMBERS, VB_MODULES, VB_SUPERS, VB_ANCESTRIES, VB_PROJECTS, VB_PROJECT_INFO, VB_FILE_IMPORTS, VB_ALIASES, VB_BASE_NAMES, VB_TYPE_PARAMS, VB_EXTENSIONS, VB_TYPE_IMPORTS, VB_SCOPES] as Array>) { memo.delete(context);