Files
codegraph/__tests__/vbnet-bare-member-scope.test.ts
Colby Mchenry c9944147bb 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>
2026-10-05 17:32:53 +00:00

101 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A VB.NET call with no receiver — or with `Me` / `MyClass` / `MyBase`,
* which the extractor drops — is a member of the type it is written in, of a
* type around it, or of one they inherit; a `Module`'s members are reached
* from anywhere. Never another class's same-named member, however near its
* file: SCrawler's `{ToString()}` in the Instagram `UserData` (which
* `Inherits UserDataBase`) went to a nearby structure's `ToString`.
*/
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;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-bare-'));
const files: Record<string, string> = {
'API/Base/UserDataBase.vb': `Namespace API.Base
Friend MustInherit Class UserDataBase
Public Overrides Function ToString() As String
Return "user"
End Function
Protected Sub Refresh()
End Sub
End Class
End Namespace
`,
'API/Instagram/MediaItem.vb': `Namespace API.Instagram
Friend Class MediaItem
Public Overrides Function ToString() As String
Return "media"
End Function
Friend Sub Refresh()
End Sub
End Class
End Namespace
`,
'API/Instagram/UserData.vb': `Namespace API.Instagram
Friend Class UserData : Inherits API.Base.UserDataBase
Friend Sub SetTagsLimit()
Dim aStr$ = $"Enter the number of posts from user {ToString()}"
Me.Refresh()
Log(aStr)
End Sub
Private Class Counter
Friend Sub Tick()
Report()
End Sub
End Class
Private Shared Sub Report()
End Sub
End Class
End Namespace
`,
'Tools/Logger.vb': `Public Module Logger
Public Sub Log(ByVal Text As String)
End Sub
End Module
`,
};
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 });
});
/** `Owner::member` of every call edge out of one method. */
function callsOf(file: string, qualifiedName: string): string[] {
const from = cg.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName)!;
return cg
.getOutgoingEdgesFrom([from.id])
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('VB.NET receiver-less calls', () => {
it('reach the members the class inherits, not a nearer class’s, and a module’s', () => {
expect(callsOf('API/Instagram/UserData.vb', 'API.Instagram::UserData::SetTagsLimit')).toEqual([
'API.Base::UserDataBase::Refresh',
'API.Base::UserDataBase::ToString',
'Logger::Log',
]);
});
it('reach a member of the class around a nested one', () => {
expect(callsOf('API/Instagram/UserData.vb', 'API.Instagram::UserData::Counter::Tick')).toEqual([
'API.Instagram::UserData::Report',
]);
});
});