fix(vbnet): link Shared fields and properties read through their class (#2305) (#2355)

VB.NET emitted a reference for calls only, so a value read or write
through a type name -- `AppSession.SessionId`,
`AppSession.CurrentUser = "demo"`, `Logger.Level` -- linked nothing, and
`codegraph callers` on the field, the property and the class came back
empty. The reporter's central session class showed 6 callers against
~460 referencing files.

- Extraction (TS only; VB.NET is not kernel-routed): the static-member
  pass now covers VB.NET. A value read through a Capitalized name is sent
  as one `references` ref, `Name.Member`, the receiver kept as a call's
  is. Namespace roots (`System`, `Microsoft`, `My`, `Global`) and the
  built-in type keywords (`String.Empty`) are not sent.
- Resolution (vbnet-receivers.ts matchVbMemberRead, ahead of the
  framework, import and name strategies): the receiver is looked up with
  #2351's VB.NET scoping, case-insensitively. A local, parameter, field,
  property or Module member of that name holds a value and links nothing;
  one typed as the type of its own name ("Color Color") reads that type.
  Otherwise the class, Module, Structure, Interface or Enum the
  namespaces, Imports and aliases around the read see is the type: the
  read links the member it declares or inherits (field, property,
  constant, event, Enum value, nested type; Shared before a same-named
  instance member, never the member the read is in) and, from outside
  the type, the type itself. A method named this way is a call made
  without parentheses (`calls`) unless AddressOf / NameOf only names it.
- `AppSession.Items(0)`, an index into a Shared field that VB.NET writes
  as a call, now reads the field and the type instead of linking nothing.

Contributor PR #2321 (@ChrisPrapas) had the right idea -- enable the
static-member pass for VB.NET with a member-level ref -- and its test
scenarios are adopted. Its resolution is replaced: it name-matched the
receiver's bare name and checked a member only against the receiver's
spelling. Ported onto main it passes its own tests, but binds a
parameter `appSession As OtherSession` to AppSession's members, misses
import aliases, and adds 8,652 / 13,554 edges on SCrawler / staxrip,
about 2,000 per repo binding a receiver to another class's same-named
member (a plugin form's local `Dim CONTAINER_MAIN` went to another
form's control field).

Verification: new vbnet-shared-member-refs.test.ts (7 tests) fails 6/7
on main and passes; the other VB.NET suites pass; tsc and build clean;
the issue's repro lists Consumer.Run and Consumer2.Describe for
SessionId, CurrentUser and AppSession.

Validation, main vs this branch: nodes unchanged, 0 edges removed, no
self-loops. SCrawler 14,893 -> 17,437 edges, staxrip 26,647 -> 32,568:
Enum values 803 / 1,448, Shared fields and properties 363 / 1,459, the
type read through 1,374 / 2,879, Shared functions called without
parentheses 3 / 126, AddressOf / NameOf 1 / 9. Spot-checked against
source: per-site SiteSettings, import aliases, escaped `[Date]` /
`[New]`, nested enums, "Color Color" fields; designer `Point.Empty` and
staxrip's AutoCrop duplicates stay unlinked. Index time within noise;
value reads that link nothing are kept for re-resolution like failed
calls (db 22.6 -> 28.9 MB, 28.3 -> 35.8 MB).

Not covered: reads through Me / MyBase, unqualified reads, instance
reads through a typed variable, Module variables as receivers,
namespace-qualified receivers and With blocks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ChrisPrapas <129202926+ChrisPrapas@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-10-05 20:05:18 +00:00
committed by GitHub
parent 69ee3b9a85
commit 6c61dd74ca
5 changed files with 396 additions and 14 deletions
+1
View File
@@ -28,6 +28,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335)
- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
## [1.6.2] - 2026-10-03
+218
View File
@@ -0,0 +1,218 @@
/**
* VB.NET: a value read or written through a type or module name —
* `AppSession.SessionId`, `AppSession.CurrentUser = "demo"`, `Logger.Level`,
* `Mode.Fast` — is a use of the member and of the type (#2305). Before, only
* calls linked, so `codegraph callers` on a Shared field or property, and on
* the class itself, came back empty.
*
* The name is looked up as VB.NET does, without regard to case: a local, a
* parameter or a field of that name holds a value, 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
* the read, its `Imports` and aliases see — SCrawler declares a
* `SiteSettings` in every site's namespace. A method named without
* parentheses is called, unless `AddressOf` only names it, and an index into
* a Shared field (`AppSession.Items(0)`), which VB.NET writes as a call, reads
* the field.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
const files: Record<string, string> = {
// The issue's three files.
'AppSession.vb': `Public Class AppSession
Public Shared SessionId As Guid = Guid.NewGuid()
Public Shared Property CurrentUser As String
Public Shared Items As New List(Of String)
Public Shared Function GetGreeting() As String
Return "Hello " & CurrentUser
End Function
Public Shared Sub OnTick(ByVal sender As Object, ByVal e As EventArgs)
End Sub
Public Shared Function Describe() As String
Return AppSession.CurrentUser
End Function
End Class
`,
'Consumer.vb': `Public Class Consumer
Public Sub Run()
Dim id As Guid = AppSession.SessionId
AppSession.CurrentUser = "demo"
Console.WriteLine(AppSession.GetGreeting())
End Sub
End Class
`,
'Consumer2.vb': `Public Class Consumer2
Public Function Describe() As String
Return AppSession.CurrentUser & AppSession.SessionId.ToString()
End Function
End Class
`,
'OtherSession.vb': `Public Class OtherSession
Public Shared SessionId As Guid = Guid.NewGuid()
Public Property Size As Integer
End Class
`,
'Size.vb': `Public Class Size
End Class
`,
'Logger.vb': `Public Module Logger
Public Level As Integer
End Module
`,
'Mode.vb': `Public Enum Mode
Fast
Slow
End Enum
`,
'Form1.vb': `Public Class Form1
Private Panel1 As Panel
Public Sub Setup()
Logger.Level = 3
Dim a = Panel1.Size
Me.Panel1.Size = New System.Drawing.Size(1, 2)
Dim speed = Mode.Fast
Dim greeting = AppSession.GetGreeting
AddHandler Timer1.Tick, AddressOf AppSession.OnTick
Dim first = AppSession.Items(0)
Dim empty = String.Empty
End Sub
Public Sub Shadowed(ByVal appSession As OtherSession)
Dim id = AppSession.SessionId
End Sub
End Class
`,
// A type declared in each site's namespace, and one read through an import alias.
'Sites/Reddit/SiteSettings.vb': `Namespace API.Reddit
Friend Class SiteSettings
Friend Const Header As String = "r"
End Class
Friend Class UserData
Friend Function Key() As String
Return SiteSettings.Header
End Function
End Class
End Namespace
`,
'Sites/Twitter/SiteSettings.vb': `Namespace API.Twitter
Friend Class SiteSettings
Friend Const Header As String = "t"
End Class
Friend Class UserData
Friend Function Key() As String
Return SiteSettings.Header
End Function
End Class
End Namespace
`,
'Sites/Facebook/UserData.vb': `Imports RS = API.Reddit.SiteSettings
Namespace API.Facebook
Friend Class UserData
Friend Function Key() As String
Return RS.Header
End Function
End Class
End Namespace
`,
// A property typed as the type of its own name; a property initializer.
'Settings.vb': `Public Class Settings
Public Property Theme As String
End Class
`,
'Window.vb': `Public Class Window
Public Property Settings As Settings
Public Property Title As String = AppSession.CurrentUser
Public Function CurrentTheme() As String
Return Settings.Theme
End Function
End Class
`,
// CRLF line endings and multibyte text before the read.
'Weird.vb': [
'Public Class Weird',
' Public Sub Go()',
' Dim s = "SessionId éééé 😀" & AppSession.SessionId',
' End Sub',
'End Class',
'',
].join('\r\n'),
};
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-shared-'));
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
function nodeId(qualifiedName: string): string {
const found = cg.getNodesInFiles(Object.keys(files)).filter((n) => n.qualifiedName === qualifiedName);
if (found.length !== 1) throw new Error(`${found.length} nodes named ${qualifiedName}`);
return found[0]!.id;
}
/** The qualified names of what links to `qualifiedName` with an edge of `kind`. */
function linkedFrom(qualifiedName: string, kind: 'references' | 'calls'): string[] {
return cg
.getIncomingEdgesTo([nodeId(qualifiedName)], [kind])
.map((e) => cg.getNode(e.source)!.qualifiedName)
.sort();
}
describe('VB.NET Shared members read through their type (#2305)', () => {
it('link a Shared field and property to the methods that read or write them', () => {
expect(linkedFrom('AppSession::SessionId', 'references')).toEqual(['Consumer2::Describe', 'Consumer::Run', 'Weird::Go']);
expect(linkedFrom('AppSession::CurrentUser', 'references'))
.toEqual(['AppSession::Describe', 'Consumer2::Describe', 'Consumer::Run', 'Window::Title']);
});
it('link the class to the code outside it that reads through it', () => {
expect(linkedFrom('AppSession', 'references'))
.toEqual(['Consumer2::Describe', 'Consumer2::Describe', 'Consumer::Run', 'Consumer::Run', 'Form1::Setup', 'Weird::Go', 'Window::Title']);
expect(cg.getCallers(nodeId('AppSession')).map((c) => c.node.qualifiedName).sort())
.toEqual(['Consumer2::Describe', 'Consumer::Run', 'Form1::Setup', 'Weird::Go', 'Window::Title']);
});
it('keep the call to a Shared method, and call one named without parentheses', () => {
expect(linkedFrom('AppSession::GetGreeting', 'calls')).toEqual(['Consumer::Run', 'Form1::Setup']);
expect(linkedFrom('AppSession::OnTick', 'references')).toEqual(['Form1::Setup']);
expect(linkedFrom('AppSession::OnTick', 'calls')).toEqual([]);
});
it('read a Shared field indexed like a call, a Module variable and an Enum value', () => {
expect(linkedFrom('AppSession::Items', 'references')).toEqual(['Form1::Setup']);
expect(linkedFrom('Logger::Level', 'references')).toEqual(['Form1::Setup']);
expect(linkedFrom('Mode::Fast', 'references')).toEqual(['Form1::Setup']);
expect(linkedFrom('Mode', 'references')).toEqual(['Form1::Setup']);
});
it('link nothing through a local, parameter or field, whatever its case', () => {
expect(linkedFrom('OtherSession::SessionId', 'references')).toEqual([]);
expect(linkedFrom('OtherSession::Size', 'references')).toEqual([]);
expect(linkedFrom('Size', 'references')).toEqual([]);
expect(linkedFrom('OtherSession', 'references')).toEqual([]);
});
it('read the type the namespaces and imports around the read see', () => {
expect(linkedFrom('API.Reddit::SiteSettings::Header', 'references'))
.toEqual(['API.Facebook::UserData::Key', 'API.Reddit::UserData::Key']);
expect(linkedFrom('API.Twitter::SiteSettings::Header', 'references')).toEqual(['API.Twitter::UserData::Key']);
});
it('read through a member typed as the type of its own name', () => {
expect(linkedFrom('Settings::Theme', 'references')).toEqual(['Window::CurrentTheme']);
});
});
+41 -2
View File
@@ -361,7 +361,7 @@ const PHP_TYPE_NODES: ReadonlySet<string> = new Set([
*/
const MEMBER_ACCESS_TYPES: ReadonlySet<string> = new Set([
'field_access', // java (`Foo.BAR`)
'member_access_expression', // c# (`Foo.Bar`)
'member_access_expression', // c# / vb.net (`Foo.Bar`)
'navigation_expression', // kotlin / swift (`Foo.bar`)
'field_expression', // scala (`Foo.bar`)
'class_constant_access_expression', // php (`Foo::CONST`, `Foo::class`)
@@ -380,11 +380,21 @@ 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).
*/
const STATIC_MEMBER_LANGS: ReadonlySet<string> = new Set([
'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp',
'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp', 'vbnet',
]);
/**
* 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.
*/
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;
/**
* Tree-sitter node kinds that represent constructor invocations
* (`new Foo()` and friends). Used by extractInstantiation to emit
@@ -5549,6 +5559,10 @@ 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' ||
@@ -5569,6 +5583,31 @@ 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.
*/
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;
this.unresolvedReferences.push({
fromNodeId: ownerId,
referenceName: `${name}.${getNodeText(member, this.source)}`,
referenceKind: 'references',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
/**
* Find a `class_body` child of an `object_creation_expression` — the
* marker for an anonymous class (`new T() { ... }`). Returns the body
+7 -1
View File
@@ -25,7 +25,7 @@ import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, is
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
import { clearVbnetReceiverMemos } from './vbnet-receivers';
import { clearVbnetReceiverMemos, isVbMemberRead, matchVbMemberRead } 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';
@@ -1134,6 +1134,12 @@ 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.
if (isVbMemberRead(ref)) return this.gateLanguage(matchVbMemberRead(ref, this.context), ref);
// Function-as-value refs (#756) get a dedicated, strictly-gated path:
// import-based resolution first (an imported callback resolves through its
// import, the most precise cross-file signal), then matchFunctionRef
+129 -11
View File
@@ -432,9 +432,9 @@ function typesAround(file: string, line: number, context: ResolutionContext): No
return types.filter((n) => n.startLine <= line && n.endLine >= line);
}
/** The project's VB.NET types (classes, modules, structures, interfaces) named `name`, case aside. */
function projectTypesNamed(name: string, context: ResolutionContext): Node[] {
return context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && VB_TYPE_KINDS.has(n.kind));
/** The project's VB.NET types (classes, modules, structures, interfaces — or `kinds`) named `name`, case aside. */
function projectTypesNamed(name: string, context: ResolutionContext, kinds: ReadonlySet<string> = VB_TYPE_KINDS): Node[] {
return context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && kinds.has(n.kind));
}
const VB_MEMBERS = new WeakMap<ResolutionContext, Map<string, Node[]>>();
@@ -768,11 +768,16 @@ export function isVbTypeQualifiedBy(n: Node, qualifier: string, file: string, co
* SCrawler declares
* a `SiteSettings` in each site's namespace (`API.Pinterest`, `API.Bluesky`,
* …); a member typed `SiteSettings` in `API.Pinterest.UserData` is
* Pinterest's. `ambiguous` when what is left are different types.
* Pinterest's. `ambiguous` when what is left are different types. `kinds`
* widens the types looked for (an `Enum` a value is read through).
*/
function typesNamedAt(written: VbType, context: ResolutionContext): { owners: Node[]; ambiguous: boolean } {
function typesNamedAt(
written: VbType,
context: ResolutionContext,
kinds: ReadonlySet<string> = VB_TYPE_KINDS,
): { owners: Node[]; ambiguous: boolean } {
const t = written.file ? unalias(written, written.file, context) : written;
let candidates = projectTypesNamed(t.name, context);
let candidates = projectTypesNamed(t.name, context, kinds);
// `System.Drawing.Color` is not the project's `Color`; `API.Base.UserDataBase` is that one.
if (t.qualifier) candidates = candidates.filter((c) => endsWith(fullSegments(c, context).slice(0, -1), t.qualifier!));
if (candidates.length === 0 || !t.file) return { owners: candidates, ambiguous: false };
@@ -993,9 +998,9 @@ function ownersOf(t: VbType, context: ResolutionContext): Node[] | null {
}
/**
* A method — or, `withValues`, a field or property — named `name` on one of
* `owners` (the type `typed` names) or a project type they inherit, with what
* the type parameters of the type declaring it stand for.
* A method — or, `withValues`, a field or property; or what `fits` — named
* `name` on one of `owners` (the type `typed` names) or a project type they
* inherit, with what the type parameters of the type declaring it stand for.
*/
function memberOn(
owners: Node[],
@@ -1004,8 +1009,8 @@ function memberOn(
context: ResolutionContext,
withValues: boolean,
typed?: VbType,
fits: (n: Node) => boolean = (n) => n.kind === 'method' || (withValues && VB_VALUE_KINDS.has(n.kind)),
): { node: Node; args: Map<string, VbType> } | null {
const fits = (n: Node) => n.kind === 'method' || (withValues && VB_VALUE_KINDS.has(n.kind));
for (const level of [0, 1]) {
for (const owner of preferVbProject(owners, ref, context)) {
// What the owner's own parameters are, as the receiver's type writes them (`Repo(Of Foo)`).
@@ -1238,7 +1243,11 @@ export function matchVbTypedCall(
if (named.owners.length === 0) return undefined;
if (named.ambiguous) return null;
const shared = memberOn(named.owners, method, ref, context, false);
return shared ? { original: ref, targetNodeId: shared.node.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null;
if (shared) return { original: ref, targetNodeId: shared.node.id, confidence: 0.85, resolvedBy: 'qualified-name' };
// `AppSession.Items(0)`: an index into a shared field or property, which
// VB.NET writes as a call — a read of the member and of its type (#2305).
const indexed = memberOn(named.owners, method, ref, context, true);
return indexed ? readThrough(indexed.node, named.owners, ref, context, { edgeKind: 'references' }) : null;
}
if (!type || typeKey(type) === 'object') return undefined;
const owners = ownersOf(type, context);
@@ -1251,6 +1260,115 @@ export function matchVbTypedCall(
return extensionFor(type, owners, method, ref, context, isStdMethod);
}
/** The types a value is read through: those a shared call is made on, and an `Enum`. */
const VB_READ_TYPE_KINDS: ReadonlySet<string> = new Set([...VB_TYPE_KINDS, 'enum']);
/**
* What a read through a type names: a value it holds or gives (a field, a
* property, a constant, an `Enum` case, an event), a method — which VB.NET
* runs when it is named without parentheses — or a type nested in it.
*/
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*\]?)$/;
/** 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);
}
/**
* 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.
*/
function typesReadThrough(name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] | null {
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;
const found = typesNamedAt(written, context, VB_READ_TYPE_KINDS);
return found.ambiguous || found.owners.length === 0 ? null : found.owners;
}
/**
* A read of `member` through one of `owners` (the parts of the type its
* receiver names): the member, and the type as a second target — unless the
* read is written inside that type, which doesn't depend on itself.
*/
function readThrough(
member: Node,
owners: Node[],
ref: UnresolvedRef,
context: ResolutionContext,
extra: Partial<ResolvedRef> = {},
): ResolvedRef | null {
if (member.id === ref.fromNodeId) return null;
const owner = owners.find((o) => o.filePath === member.filePath) ?? owners[0]!;
const from = context.getNodeById?.(ref.fromNodeId)?.qualifiedName.toLowerCase();
const own = owner.qualifiedName.toLowerCase();
const inside = owner.id === ref.fromNodeId || (from !== undefined && (from === own || from.startsWith(`${own}::`)));
return {
original: ref,
targetNodeId: member.id,
confidence: 0.85,
resolvedBy: 'qualified-name',
...extra,
...(inside ? {} : { alsoTargets: [{ targetNodeId: owner.id }] }),
};
}
/** Whether a method is named at a reference without being run: `AddressOf Type.Method`, `NameOf(Type.Method)`. */
function namesWithoutRunning(ref: UnresolvedRef, context: ResolutionContext): boolean {
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
return /\b(?:AddressOf\s+|NameOf\s*\(\s*)$/i.test(line.slice(0, Math.max(0, ref.column)));
}
/**
* 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.
*/
export function matchVbMemberRead(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const m = VB_MEMBER_READ.exec(ref.referenceName);
if (!m) return null;
const owners = typesReadThrough(m[1]!, ref, context);
if (!owners) return 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;
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;
}
if (member.kind === 'method') {
return {
original: ref,
targetNodeId: member.id,
confidence: 0.85,
resolvedBy: 'qualified-name',
...(namesWithoutRunning(ref, context) ? {} : { edgeKind: 'calls' as const }),
};
}
return readThrough(member, owners, ref, context, { confidence: 0.9 });
}
const VB_TYPE_IMPORTS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
/** The last names of a file's `Imports` (a type imported this way lends its shared members; an alias lends none). */