fix(vbnet): resolve receiver calls by declared type; guard .NET names, bare calls and ties (#2351)

VB.NET's resolver guessed a receiver call's target from the receiver's
name whenever no class matched it, without any of the guards C# has.
Indexing Structure members (#2345) gave those guesses new targets: 23
SCrawler `xxxFile.Delete()` calls on PersonalUtilities' SFile went to a
nested struct's `TempFileConversion::Delete`, and staxrip's main app
sent 90 calls to its AutoCrop tool's duplicate `ColorHSL`.

- Declared types (new src/resolution/vbnet-receivers.ts): a receiver's
  `As` type decides the call (local, parameter, field, property,
  `For Each`, `As New`, ...). The candidates are that type's member, one
  it inherits, or an extension method written for it. A type from
  outside the project gets no link.
- .NET standard method names (`Add`, `Contains`, `Clear`, `Dispose`,
  `ToString`, ...) on an untyped receiver need a receiver named after
  the owner, as in C#. VB.NET matches them case-insensitively.
- A bare call, or one on `Me`, reaches the enclosing class, what it
  inherits, or a Module. A nested type is matched by its bare name only
  from inside its owner. Types resolve through enclosing namespaces,
  `Imports` (aliases included) and the caller's project. A tie between
  equally good guesses goes to the caller's file, then its project, then
  the nearer directory, or gets no link.
- vbReceiverOf reads the name at or after the reference's column, so
  `Me.Size = New System.Drawing.Size(...)` is read through
  `System.Drawing`, not `Me`.

Before/after on main (nodes unchanged): SCrawler 15,092 -> 14,893
edges, staxrip 28,002 -> 26,647. Of the non-containment edges removed,
855 / 2,802 are the same link with new resolver metadata. The rest:

- .NET names on untyped receivers no longer guessed: 196 / 1,378.
  731 of staxrip's are StringBuilder `sb.Append` calls that had gone to
  `LogBuilder::Append`.
- Other receiver guesses dropped: 86 / 103, including the SFile.Delete
  calls.
- Bare calls out of scope: 38 / 834. 590 of staxrip's are
  `New Point(...)` that had gone to a class nested in `ButtonEx`; others
  are WinForms `Refresh()`, `Focus()` and `Activate()`.
- Retargeted: 227 / 532, e.g. 208 bare `Add(...)` calls in encoder
  classes now reach the inherited `CommandLineParams::Add`.

121 / 958 call sites resolve that did not before, e.g. `cms.Add` ->
ContextMenuStripEx and `td.AddButton` -> TaskDialog. Designer -> Size
links fall from 5 / 1 to 0; designer -> Add stays 0.

Known trade-off, shared with C#'s guard: about 31 staxrip calls like
`switches.Join(BR)` that may be the project's own `MiscExtensions`
extension methods are no longer linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-05 17:32:53 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 355afc70b1
commit c9944147bb
9 changed files with 2235 additions and 6 deletions
+2
View File
@@ -25,6 +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 { 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';
@@ -457,6 +458,7 @@ export class ReferenceResolver {
clearNameMatcherMemos(this.context);
clearCppMacroVisibility(this.context);
clearSwiftTypeVisibility(this.context);
clearVbnetReceiverMemos(this.context);
clearTypeParameterMemos(this.context);
}
}
+75 -6
View File
@@ -11,6 +11,7 @@ import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSuperty
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
import { isTestPath } from '../search/query-utils';
import { isMinifiedContent } from '../extraction/generated-detection';
import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace';
@@ -3112,6 +3113,9 @@ const CSHARP_STD_METHODS: ReadonlySet<string> = new Set([
'ForEach', 'Sort', 'Reverse', 'Clone', 'Seek', 'SetLength',
]);
/** The same .NET names as VB.NET writes them — in any case. */
const VBNET_STD_METHODS: ReadonlySet<string> = new Set([...CSHARP_STD_METHODS].map((m) => m.toLowerCase()));
/**
* A request handler the web framework dispatches to: a Django / DRF / Flask
* view's `get` / `post` / …, a controller's `index` / `store` / `update` /
@@ -3156,11 +3160,17 @@ function stdMethodNames(language: string): ReadonlySet<string> | null {
case 'rust': return RUST_STD_METHODS;
case 'kotlin': return KOTLIN_STD_METHODS;
case 'csharp': return CSHARP_STD_METHODS;
case 'vbnet': return VBNET_STD_METHODS;
case 'dart': return DART_STD_METHODS;
default: return 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 {
return stdMethodNames(language)?.has(language === 'vbnet' ? name.toLowerCase() : name) ?? false;
}
/** Methods of Dart's String, List, Iterable, Map and Set — names a project type rarely carries itself. */
const DART_STD_METHODS: ReadonlySet<string> = new Set([
'endsWith', 'startsWith', 'contains', 'split', 'substring', 'trim', 'trimLeft', 'trimRight', 'toLowerCase',
@@ -4657,7 +4667,13 @@ function vbReceiverOf(ref: UnresolvedRef, context: ResolutionContext): string |
const name = ref.referenceName.toLowerCase();
let start = lower.startsWith(name, ref.column) ? ref.column : -1;
if (start < 0) {
const m = new RegExp(`(?<![\\w])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w])`).exec(lower);
// The reference starts at its receiver or its `New`, so the name is the
// first one there or after: `st.Language = New Language(…)` constructs
// a Language through no receiver, and `Me.Size = New System.Drawing.Size(…)`
// through `System.Drawing`, not `Me`.
const at = new RegExp(`(?<![\\w])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w])`, 'g');
at.lastIndex = Math.max(0, ref.column);
const m = at.exec(lower) ?? (at.lastIndex = 0, at.exec(lower));
start = m ? m.index : -1;
}
if (start < 0) return null;
@@ -4688,6 +4704,22 @@ function isVbMemberReachable(n: Node, receiver: string): boolean {
return last !== '' && last === owner;
}
/**
* Whether a VB.NET call has no receiver, or one the extractor drops (`Me`,
* `MyClass`, `MyBase`) — a call isVbMemberInScope judges. A name its line
* doesn't show (a link of a chain continued from the line above) is not.
*/
function isVbScopedCall(ref: UnresolvedRef, receiver: string | null, context: ResolutionContext): boolean {
if (ref.language !== 'vbnet' || ref.referenceKind !== 'calls' || !/^\w+$/.test(ref.referenceName)) return false;
return receiver === null ? hasNoReceiverOnLine(ref, context) : /^(?:me|mybase|myclass)$/i.test(receiver);
}
/** Whether a VB.NET call or construction names its target with nothing before it (`New Point(4, 285)`). */
function isVbUnqualifiedName(ref: UnresolvedRef, receiver: string | null, context: ResolutionContext): boolean {
if (ref.language !== 'vbnet' || (ref.referenceKind !== 'calls' && ref.referenceKind !== 'instantiates')) return false;
return receiver === null && /^\w+$/.test(ref.referenceName) && hasNoReceiverOnLine(ref, context);
}
const CFML_CHAINS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
/**
@@ -5536,6 +5568,8 @@ export function matchByExactName(
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
? vbReceiverOf(ref, context) : null;
const vbScoped = isVbScopedCall(ref, vbReceiver, context);
const vbUnqualified = isVbUnqualifiedName(ref, vbReceiver, context);
const objcShape = ref.language === 'objc' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*:*(?:\w+:)*$/.test(ref.referenceName)
? objcCallShape(ref, context) : null;
const csharpBare = ref.language === 'csharp' && (ref.referenceKind === 'calls' || ref.referenceKind === 'references') && /^[A-Za-z_]\w*$/.test(ref.referenceName);
@@ -5557,6 +5591,9 @@ export function matchByExactName(
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(vbReceiver && !/^(?:me|mybase|myclass)$/i.test(vbReceiver) && !isVbTypeQualifiedBy(n, vbReceiver, ref.filePath, context)) &&
!(vbScoped && !isVbMemberInScope(n, ref, context)) &&
!(vbUnqualified && !isVbNestedTypeInScope(n, ref, context)) &&
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
@@ -7817,6 +7854,14 @@ export function matchMethodCall(
// shared source-based inferrer. resolveMethodOnType validates the method
// exists on the inferred type, so a mis-inference produces no edge.
if (inferableReceiver) {
// A VB.NET receiver's declared type decides the call, or that there is no
// project method to call: SCrawler's `ThumbnailFile.Delete(…)` on an
// external `SFile` went to a nested class's `Delete` by a shared word.
if (ref.language === 'vbnet' && dotMatch) {
const typed = nmTimedT('mc-vbtyped', ref, () =>
matchVbTypedCall(objectOrClass!, methodName!, ref, context, (name) => isStdMethodName('vbnet', name)));
if (typed !== undefined) return typed;
}
let inferredType = nmTimedT('mc-infer', ref, () =>
ref.language === 'cpp'
? inferCppReceiverType(objectOrClass!, ref, context)
@@ -8024,6 +8069,9 @@ export function matchMethodCall(
const visible = classCandidates.filter((c) => c.language !== 'csharp' || isCsharpTypeVisible(c, typeRef, context));
classCandidates = [...visible, ...classCandidates.filter((c) => !visible.includes(c))];
}
// A VB.NET type declared in two projects is the caller's own project's:
// staxrip's `FrameServerFactory.Create(…)` went to its AutoCrop tool's copy.
if (ref.language === 'vbnet') classCandidates = preferVbProject(classCandidates, ref, context);
for (const classNode of classCandidates) {
// Skip cross-language class matches
@@ -8213,10 +8261,10 @@ export function matchMethodCall(
// `json` of its `MockedResponse`.
!isUnnamedTestDouble(targetMethods[0]!, objectOrClass!, ref, context) &&
!((ref.language === 'lua' || ref.language === 'luau') && isLuaLibraryCall(objectOrClass!, methodName!, ref, targetMethods[0]!)) &&
// Rust / Go / Kotlin / C#: a standard-library method name on an
// untyped receiver (`sym.map(…)`, `w.Header().Get(…)`,
// Rust / Go / Kotlin / C# / VB.NET: a standard-library method name on
// an untyped receiver (`sym.map(…)`, `w.Header().Get(…)`,
// `reader.Value.ToString()`) is the library type's.
!(stdMethodNames(ref.language)?.has(methodName!) &&
!(isStdMethodName(ref.language, methodName!) &&
!/^(?:self|Self|this|base)$/.test(objectOrClass!) && !receiverNamesOwner(receiverLink(objectOrClass!), targetMethods[0]!, context)) &&
!(UNTYPED_RECEIVER_LANGUAGES.has(ref.language) && !/^(?:self|self\.class|this|super|weak_?self|strong_?self)$/i.test(objectOrClass!) &&
!sharesReceiverWord(objectOrClass!, targetMethods[0]!) &&
@@ -8238,11 +8286,12 @@ export function matchMethodCall(
const head = receiverWords[receiverWords.length - 1]?.toLowerCase();
let bestMatch: typeof targetMethods[0] | undefined;
let bestScore = 0;
let tied: typeof targetMethods = [];
// Same-file candidates first, so a score tie (`score > bestScore` keeps
// the first seen) resolves to the call site's own file rather than the
// first-indexed duplicate (#1079).
const std = stdMethodNames(ref.language)?.has(methodName!) && !/^(?:self|Self|this|base)$/.test(objectOrClass!);
const std = isStdMethodName(ref.language, methodName!) && !/^(?:self|Self|this|base)$/.test(objectOrClass!);
for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
if (std && !receiverNamesOwner(receiverLink(objectOrClass!), method, context)) continue;
// The owner type's own name — not its namespace (`eShop.ClientApp…`
@@ -8265,8 +8314,16 @@ export function matchMethodCall(
if (score > bestScore) {
bestScore = score;
bestMatch = method;
tied = [method];
} else if (score === bestScore) {
tied.push(method);
}
}
// VB.NET: between equally good guesses, the caller's own file, then its
// project, then the nearer directory — and no guess when none of them
// decides. staxrip's main app and its AutoCrop tool each declare a
// `ColorHSL`, and the first indexed took about 90 of the app's calls.
if (ref.language === 'vbnet' && tied.length > 1 && bestScore >= 2) bestMatch = breakVbTie(tied, ref, context) ?? undefined;
// A wrapper handing its call on — BookStack's `FileStorage::delete` doing
// `$storage->delete($path)`, `CommentRepo::delete` doing
@@ -9229,7 +9286,7 @@ function computePathProximity(filePath1: string, filePath2: string): number {
function findBestMatch(
ref: UnresolvedRef,
candidates: Node[],
_context: ResolutionContext
context: ResolutionContext
): Node | null {
// Prioritization rules:
// 1. Same file > different file
@@ -9269,6 +9326,13 @@ function findBestMatch(
// Directory proximity bonus — strongly prefer same module/package
score += pathProximityFromDirs(refDirs, candidate.filePath);
// A VB.NET project compiles its own files: the caller's project weighs as
// much as the nearest a directory can be. staxrip's `New ColorHSL(…)` went
// to its AutoCrop tool's copy.
if (ref.language === 'vbnet' && candidate.language === 'vbnet' && sameVbProject(candidate.filePath, ref.filePath, context)) {
score += 80;
}
// Language matching: strongly prefer same language, penalize cross-language
if (candidate.language === ref.language) {
score += 50;
@@ -9353,6 +9417,8 @@ export function matchFuzzy(
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
? vbReceiverOf(ref, context) : null;
const vbScoped = isVbScopedCall(ref, vbReceiver, context);
const vbUnqualified = isVbUnqualifiedName(ref, vbReceiver, context);
const objcShape = ref.language === 'objc' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*:*(?:\w+:)*$/.test(ref.referenceName)
? objcCallShape(ref, context) : null;
const csharpBare = ref.language === 'csharp' && (ref.referenceKind === 'calls' || ref.referenceKind === 'references') && /^[A-Za-z_]\w*$/.test(ref.referenceName);
@@ -9388,6 +9454,9 @@ export function matchFuzzy(
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(vbReceiver && !/^(?:me|mybase|myclass)$/i.test(vbReceiver) && !isVbTypeQualifiedBy(n, vbReceiver, ref.filePath, context)) &&
!(vbScoped && !isVbMemberInScope(n, ref, context)) &&
!(vbUnqualified && !isVbNestedTypeInScope(n, ref, context)) &&
!(objcShape === 'c-call' && OBJC_MEMBER_KINDS.has(n.kind)) &&
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
File diff suppressed because it is too large Load Diff