mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 09:35:22 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user