fix(vbnet): link fields and properties read or written through a value (#2377)

VB.NET linked a member read only through a type or module name (#2355),
so a read or write through an instance -- a parameter `x.Normal = 3`, a
local `h.Normal`, a field `_h.Title`, `Me._h.Normal` -- produced no edge,
and `codegraph callers` on an instance field or property listed almost
none of its uses.

- Extraction (TS only; VB.NET is not kernel-routed): every member read or
  write is sent with its receiver as a path, whatever the receiver's case:
  `x.Normal`, `Me._h.Normal`, `MyBase.Count`, `.Value` in a `With` block
  (nested `With .Inner` included), `.Switch` in an object initializer
  `New BoolParam With {...}`, and `{T}.X` for a value whose type the code
  writes (`DirectCast(o, T).X`, `New T().X`). A `With` block's `.Run()`
  call, which recorded nothing, is sent the same way. The grammar splits
  `New System.Drawing.Size(...)` into members read through `New System`;
  those, anonymous types' `New With {.X = 1}`, and paths that start with
  System, Microsoft or My are not sent.
- Resolution (vbnet-receivers.ts): each link of the path is typed by the
  rules calls use since #2351 -- declarations, casts, `For Each` over a
  typed collection, call results, type-parameter constraints, Module
  variables, namespaces and Imports -- and the read links the member the
  type declares or inherits. A method named without parentheses is called
  (unless AddressOf or NameOf only names it), and `x.Items(0)`, an index
  that VB.NET writes as a call, reads `Items`. An outside, `Object`-typed,
  ambiguous or untyped receiver links nothing; nothing is guessed by name.
- "Color Color": a value named like its type (`theme As Theme`) now reads
  an instance member through the value, so only the member is linked; a
  Shared member, a Const (read from its declaration) or an Enum case still
  links the type as well, as #2355 did for every such read.
- membersNamed looks a type's members up in its parts' files rather than
  with one query per member name. The graph is identical on SCrawler; on
  staxrip, whose AutoCrop tool redeclares `ServerInfo`, 35 reads that
  main typed through the other project's field, and so left unlinked,
  now link (70 edges).

Verification: vbnet-instance-member-refs.test.ts (11 tests) fails 9/11 on
main and passes; one vbnet-shared-member-refs assertion now expects
`OtherSession::SessionId` from `Shadowed(appSession As OtherSession)`;
the VB.NET suites, tsc and the full suite pass (462 files, 5,859 tests;
22 kernel/platform files skipped). The issue's repro on the built CLI
links every read and write, and `codegraph callers` lists `Run` for
`SharedCache`, `Normal` and `Title`.

Validation, main vs this branch: nodes unchanged, self-loops unchanged.
SCrawler 17,437 -> 31,344 edges, 1 removed (same edge, new metadata).
staxrip 32,569 -> 71,362 edges, 585 removed: 318 same edge with new
metadata (Color Color reads of instance members, qualified-name ->
instance-method) and 267 class links of those reads, every one beside an
instance member. About 42k added two-link reads were re-derived from the
source text by a separate check; its 837 disagreements were all its own
blind spots (constraints, Imports aliases, sibling-block locals, call
results, staxrip's global `p`). No added edge crosses between staxrip
and its AutoCrop tool. Cost (5-run medians): CPU 16.1 -> 19.3 s and
20.2 -> 25.4 s, DB 28.1 -> 43.2 MB and 34.9 -> 59.0 MB; the DB growth is
the new edges plus reads through outside types kept as failed refs for
re-resolution (#1240).

Not covered: unqualified reads, reads through an index or a call result
(`list(0).X`), Handles clauses, and calls through a chained receiver
written directly (`g.MainForm.Update()`), which still resolve by name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 14:39:26 +00:00
committed by GitHub
parent b7e05f3ccd
commit 5a11cd7ab7
7 changed files with 730 additions and 70 deletions
+1
View File
@@ -45,6 +45,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a store whose name contains a `$`, like `const items$ = create(…)` exported on a later line with `export default items$;`, now has its actions indexed. Before, they could be missing from the graph, for example in a file that also had a syntax error. Re-index JavaScript and TypeScript projects after upgrading.
- In Dart, what a top-level variable's or a field's initializer calls, creates and reads is now linked. In Riverpod apps, where most wiring lives in provider closures like `final repoProvider = Provider((ref) => Repository(ref.watch(dioProvider)));`, `callers`, impact and `codegraph_explore` now follow a provider to the code it builds instead of stopping at the provider. A `final` or `const` initializer belongs to its constant, and a `var`'s or a field's to its file or class. A call through a type's static constant, like `FutureProvider.autoDispose(…)`, now links to that constant instead of an unrelated method that shares its name. Re-index Dart projects after upgrading.
- In Dart, a call further down a chain, like Riverpod's `FutureProvider.autoDispose.family(…)` or `events.map(mapper).transform(…)` on a stream, now links to the method of the type the chain before it produces, worked out from what each step is declared to return, and to nothing when that type can't be worked out. Before, it linked to whichever project method shared its name, so `X.autoDispose.family(…)` reached the same provider builder whatever `X` was, and a stream's `transform` reached an unrelated `transform` method. `super.` calls now reach the superclass's method, `context.read<CounterBloc>().add(…)` reaches the bloc's own `add`, and the members of an extension marked `@internal` are found too. Re-index Dart projects after upgrading. (#750)
- In VB.NET, reading or setting a field or property through a variable, parameter, field, `Me` or `MyBase`, like `x.Normal = 3`, `Me._h.Title`, `.Value` inside a `With` block or `.Switch` in `New BoolParam With {…}`, now counts as a use of that member, and so do calls inside a `With` block, functions called without parentheses and reads through a class whose name starts in lower case. Before, `codegraph callers` on an instance field or property missed most of its uses. Only a member that the value's declared type has or inherits is linked, nothing when that type comes from outside your project, and a value named after its class, like `theme As Theme`, counts as a use of the class only when it reads a `Shared` member. Re-index VB.NET projects after upgrading.
## [1.6.2] - 2026-10-03
@@ -0,0 +1,338 @@
/**
* VB.NET: a field or property read or written through a value — a parameter
* (`x.Normal = 3`), a local (`h.Normal`), a field (`_h.Title`), `Me._h.Normal`,
* `MyBase.Count`, a `With` block's `.Value`, an object initializer's
* `.Switch` — is a use of the member of the type that value is declared as.
* Before, calls through a typed receiver linked (#2351) and reads through a
* type's name (#2305), but no read through a value, so `codegraph callers` on
* an instance field or property listed almost none of its uses.
*
* The receiver is typed by the rules calls use — its declaration (`As`, `As
* New`, a cast, a `For Each` over a typed collection, a type parameter's
* constraint, a Module variable), the namespaces around the read, what the
* type inherits — and a path (`Me._h.Normal`, `n.Child.Value`) one link at a
* time. An outside type, `Object`, an untyped name or an index links nothing:
* a value's member is never guessed by name.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
let root = '';
let cg: CodeGraph;
const files: Record<string, string> = {
// The issue's two files, verbatim.
'Holder.vb': `Public Class Holder
Public Shared SharedCache As Integer
Public Normal As Integer
Public Property Title As String
Public Sub EndSession()
End Sub
End Class
`,
'User.vb': `Public Class User
Private _h As New Holder()
Public Sub Run(x As Holder)
Dim b As Integer = x.SharedCache
Dim n As Integer = x.Normal
x.Normal = 3
Dim t As String = _h.Title
Me._h.Normal = 4
Dim h As New Holder()
Dim m As Integer = h.Normal
x.EndSession()
End Sub
End Class
`,
'Node.vb': `Public Class Node
Public Value As Integer
Public Property Child As Node
Public Items As New List(Of String)
Public Event Changed As EventHandler
Public Sub Reset()
End Sub
Public Function Describe() As String
Return ""
End Function
End Class
`,
// Me, MyBase, a path through members, an inherited member; an index into a
// field, a method named without parentheses, an event, AddressOf.
'Views.vb': `Public Class BaseView
Protected Caption As String
Public Overridable Property Count As Integer
End Class
Public Class DerivedView
Inherits BaseView
Private _node As Node
Public Sub New(caption As String)
Me.Caption = caption
End Sub
Public Overrides Property Count As Integer
Get
Return MyBase.Count + Me._node.Child.Value
End Get
Set(value As Integer)
MyBase.Count = value
End Set
End Property
Public Sub Watch(n As Node)
Dim first = n.Items(0)
Dim text = n.Describe
AddHandler n.Changed, AddressOf OnChanged
Dim reset As Action = AddressOf n.Reset
Dim nodes As New List(Of Node)
For Each child In nodes
child.Value = 1
Next
End Sub
Private Sub OnChanged(sender As Object, e As EventArgs)
End Sub
End Class
`,
// With blocks (nested, through Me, through a cast), an object initializer, a
// cast; an anonymous type declares its own members.
'Builder.vb': `Public Class Builder
Private _node As New Node()
Property Seed As New Node With {.Value = 7}
Public Sub Fill(o As Object, n As Node)
With n
.Value = 1
Dim d = .Describe()
With .Child
.Reset()
End With
End With
With Me._node
.Child = Nothing
End With
With DirectCast(o, Node)
.Items.Clear()
End With
Dim v = TryCast(o, Node).Value
Dim anonymous = New With {.Value = 2}
End Sub
End Class
`,
// A Module variable, a type parameter's constraint.
'Globals.vb': `Public Module Globals
Public Current As Node
End Module
`,
'Repo.vb': `Public Class Repo(Of T As Node)
Private _item As T
Public Function Peek() As Integer
Return _item.Value + Current.Value
End Function
End Class
`,
// A field's type is the one the namespace around it declares.
'Sites/Reddit.vb': `Namespace API.Reddit
Friend Class SiteSettings
Friend Property Header As String
End Class
Friend Class UserData
Private _settings As SiteSettings
Friend Function Key() As String
Return _settings.Header
End Function
End Class
End Namespace
`,
'Sites/Twitter.vb': `Namespace API.Twitter
Friend Class SiteSettings
Friend Property Header As String
End Class
End Namespace
`,
// staxrip's encoder options: a property an initializer sets up, written later.
'Encoder.vb': `Public MustInherit Class CommandLineParam
Property Switch As String
Property Text As String
End Class
Public Class BoolParam
Inherits CommandLineParam
Property Value As Boolean
End Class
Public Class x265Enc
Property ConstVBV As New BoolParam With {
.Switch = "--const-vbv",
.Text = "Enable VBV algorithm to be consistent across runs"}
Sub Apply()
ConstVBV.Value = False
End Sub
End Class
`,
// "Color Color": a value named like its type reads an instance member
// through itself, a Shared member and an Enum case through the type. And
// staxrip names enums in lower case.
'Theme.vb': `Public Class Theme
Public Shared Current As Theme
Public Const Version As String = "1"
Public Property General As String
End Class
Public Enum Mode
Fast
End Enum
Public Enum x265RateMode
Bitrate
End Enum
Public Class Painter
Public Property Mode As Mode
Public Sub Paint(theme As Theme)
Dim g = theme.General
Dim c = theme.Current
Dim v = theme.Version
Dim f = Mode.Fast
Dim r = x265RateMode.Bitrate
End Sub
End Class
`,
// What links nothing: an outside type, Object, an untyped name, an index's
// element; and a parameter named like a type is read as the parameter.
'Lookalike.vb': `Public Class Lookalike
Public Length As Integer
Public Count As Integer
Public Value As Integer
End Class
`,
'Negatives.vb': `Public Class Negatives
Public Sub Go(s As String, o As Object, list As List(Of Node), ByVal untyped)
Dim a = s.Length
Dim b = list.Count
Dim c = o.Value
Dim d = untyped.Value
Dim e = list(0).Value
End Sub
Public Sub Shadowed(holder As Node)
Dim f = Holder.SharedCache
Dim g = holder.Value
End Sub
End Class
`,
};
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['vbnet']);
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-instance-'));
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
function nodeId(qualifiedName: string): string {
const found = cg.getNodesInFiles(Object.keys(files)).filter((n) => n.qualifiedName === qualifiedName);
if (found.length !== 1) throw new Error(`${found.length} nodes named ${qualifiedName}`);
return found[0]!.id;
}
/** The qualified names of what links to `qualifiedName` with an edge of `kind`, one per edge. */
function linkedFrom(qualifiedName: string, kind: 'references' | 'calls'): string[] {
return cg
.getIncomingEdgesTo([nodeId(qualifiedName)], [kind])
.map((e) => cg.getNode(e.source)!.qualifiedName)
.sort();
}
describe('VB.NET members read and written through a value', () => {
it('link each read and write of the issue to the member', () => {
expect(linkedFrom('Holder::SharedCache', 'references')).toEqual(['User::Run']);
expect(linkedFrom('Holder::Normal', 'references')).toEqual(['User::Run', 'User::Run', 'User::Run', 'User::Run']);
expect(linkedFrom('Holder::Title', 'references')).toEqual(['User::Run']);
expect(linkedFrom('Holder::EndSession', 'calls')).toEqual(['User::Run']);
expect(cg.getCallers(nodeId('Holder::Normal')).map((c) => c.node.qualifiedName)).toEqual(['User::Run']);
});
it('link the member alone: the type is not written at the read', () => {
expect(linkedFrom('Holder', 'references')).toEqual([]);
expect(linkedFrom('Node', 'references')).toEqual([]);
});
it('read through Me, MyBase and a path of members, one link at a time', () => {
expect(linkedFrom('BaseView::Caption', 'references')).toEqual(['DerivedView::New']);
expect(linkedFrom('BaseView::Count', 'references')).toEqual(['DerivedView::Count', 'DerivedView::Count']);
expect(linkedFrom('DerivedView::_node', 'references')).toEqual(['DerivedView::Count']);
expect(linkedFrom('Node::Child', 'references')).toEqual(['Builder::Fill', 'Builder::Fill', 'DerivedView::Count']);
});
it('index a field, run a method named without parentheses, name an event and a method', () => {
expect(linkedFrom('Node::Items', 'references')).toEqual(['Builder::Fill', 'DerivedView::Watch']);
expect(linkedFrom('Node::Describe', 'calls')).toEqual(['Builder::Fill', 'DerivedView::Watch']);
expect(linkedFrom('Node::Changed', 'references')).toEqual(['DerivedView::Watch']);
expect(linkedFrom('Node::Reset', 'references')).toEqual(['DerivedView::Watch']);
expect(linkedFrom('Node::Reset', 'calls')).toEqual(['Builder::Fill']);
});
it('read through With blocks, an object initializer, a cast, a loop variable, a Module variable and a type parameter', () => {
expect(linkedFrom('Node::Value', 'references')).toEqual([
'Builder::Fill', 'Builder::Fill', 'Builder::Seed', 'DerivedView::Count', 'DerivedView::Watch',
'Negatives::Shadowed', 'Repo::Peek', 'Repo::Peek',
]);
});
it('read the type the namespace around the declaration sees', () => {
expect(linkedFrom('API.Reddit::SiteSettings::Header', 'references')).toEqual(['API.Reddit::UserData::Key']);
expect(linkedFrom('API.Twitter::SiteSettings::Header', 'references')).toEqual([]);
});
it('link an initializer and a later write to the members the type declares or inherits', () => {
expect(linkedFrom('CommandLineParam::Switch', 'references')).toEqual(['x265Enc::ConstVBV']);
expect(linkedFrom('CommandLineParam::Text', 'references')).toEqual(['x265Enc::ConstVBV']);
expect(linkedFrom('BoolParam::Value', 'references')).toEqual(['x265Enc::Apply']);
});
it('read an instance member of a value named like its type through the value, a Shared one through the type', () => {
expect(linkedFrom('Theme::General', 'references')).toEqual(['Painter::Paint']);
expect(linkedFrom('Theme::Current', 'references')).toEqual(['Painter::Paint']);
expect(linkedFrom('Theme::Version', 'references')).toEqual(['Painter::Paint']);
// `theme.Current` and the Const `theme.Version`; not `theme.General`.
expect(linkedFrom('Theme', 'references')).toEqual(['Painter::Paint', 'Painter::Paint']);
expect(linkedFrom('Mode::Fast', 'references')).toEqual(['Painter::Paint']);
expect(linkedFrom('Mode', 'references')).toEqual(['Painter::Paint']);
});
it('read through a type whose name is in lower case', () => {
expect(linkedFrom('x265RateMode::Bitrate', 'references')).toEqual(['Painter::Paint']);
expect(linkedFrom('x265RateMode', 'references')).toEqual(['Painter::Paint']);
});
it('send no read for a qualified type after New, which the grammar splits into members', () => {
// `New System.Windows.Forms.ListBox()` parses as `.Windows.Forms` read through `New System`.
const reads = extractFromSource('Designer.vb', `Public Class Form1
Sub InitializeComponent()
Me.ListBox1 = New System.Windows.Forms.ListBox()
Dim h = New Sites.Holder
Dim n = New Holder().Normal
Dim p = DirectCast(sender, System.Windows.Forms.Control).Parent
End Sub
End Class
`).unresolvedReferences.filter((r) => r.referenceKind === 'references').map((r) => r.referenceName);
expect(reads).toEqual(['Me.ListBox1', '{Holder}.Normal']);
});
it('link nothing through an outside type, Object, an untyped name or an index', () => {
for (const member of ['Lookalike::Length', 'Lookalike::Count', 'Lookalike::Value']) {
expect(linkedFrom(member, 'references')).toEqual([]);
}
expect(linkedFrom('Holder::SharedCache', 'references')).not.toContain('Negatives::Shadowed');
});
});
+8 -5
View File
@@ -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([]);
+133 -22
View File
@@ -381,8 +381,8 @@ const MEMBER_ACCESS_TYPES: ReadonlySet<string> = 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<string> = 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
+11 -6
View File
@@ -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
+1 -1
View File
@@ -4163,7 +4163,7 @@ function stdMethodNames(language: string): ReadonlySet<string> | 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;
}
+238 -36
View File
@@ -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<ResolutionContext, Map<string, Map<string, Node[]>>>();
/** A file's VB.NET nodes by lowercased qualified name, indexed once. */
function nodesByQualifiedName(file: string, context: ResolutionContext): Map<string, Node[]> {
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<string, Node[]>();
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<ResolutionContext, Map<string, Node[]>>();
/** 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<string> = new Set([...VB_TYPE_KINDS, 'enum
*/
const VB_READ_KINDS: ReadonlySet<string> = 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<string> = new Set([...VB_VALUE_KINDS, 'method']);
/** Whether a reference is a VB.NET value read through a name, which matchVbMemberRead alone resolves. */
export function isVbMemberRead(ref: UnresolvedRef): boolean {
return ref.language === 'vbnet' && ref.referenceKind === 'references' && VB_MEMBER_READ.test(ref.referenceName);
/** 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<string> = 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);
}
/**
* 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.
* 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`.
*/
function typesReadThrough(name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] | null {
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_PATH.test(ref.referenceName);
}
/**
* 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.
*/
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<ResolutionContext, Map<string, Set<string>>>();
@@ -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<WeakMap<ResolutionContext, unknown>>) {
memo.delete(context);