diff --git a/CHANGELOG.md b/CHANGELOG.md index a25c1a90a..8fd0b0a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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().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 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 198446483..0b952a641 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, isDartChainLink, matchDartChainLink } 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, isDartChainLink, matchDartChainLink, 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'; @@ -1204,11 +1204,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 1b844ed84..7569c6c10 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -4163,7 +4163,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);