diff --git a/CHANGELOG.md b/CHANGELOG.md index b5476b128..9c5f79e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/__tests__/vbnet-shared-member-refs.test.ts b/__tests__/vbnet-shared-member-refs.test.ts new file mode 100644 index 000000000..bbcfa77ff --- /dev/null +++ b/__tests__/vbnet-shared-member-refs.test.ts @@ -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 = { + // 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']); + }); +}); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 222291f14..17b0b02ca 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -361,7 +361,7 @@ const PHP_TYPE_NODES: ReadonlySet = new Set([ */ const MEMBER_ACCESS_TYPES: ReadonlySet = 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 = 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 = 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 diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 4c9fb45f2..fcd47b772 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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 diff --git a/src/resolution/vbnet-receivers.ts b/src/resolution/vbnet-receivers.ts index 07732927e..f252dfc97 100644 --- a/src/resolution/vbnet-receivers.ts +++ b/src/resolution/vbnet-receivers.ts @@ -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 = VB_TYPE_KINDS): Node[] { + return context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && kinds.has(n.kind)); } const VB_MEMBERS = new WeakMap>(); @@ -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 = 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 } | 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 = 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 = 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 | 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>>(); /** The last names of a file's `Imports` (a type imported this way lends its shared members; an alias lends none). */