Files
codegraph/__tests__/vbnet-project-ties.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

144 lines
4.7 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 solution often carries the same type in two projects: staxrip's
* main app and its separate AutoCrop tool (its own AutoCrop.vbproj) each
* declare `ColorHSL`, `FrameServerFactory` and `DirectFrameServer`. A call
* in the main app means its own project's copy — about 90 calls such as
* `_backColor.AddLuminance(0.025)` went to the AutoCrop copy, whichever was
* indexed first. Between equally good guesses, the caller's project decides,
* then the nearer directory; when neither does, there is no guess.
*/
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 VBPROJ = `<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
</PropertyGroup>
</Project>
`;
const COLOR_HSL = `Public Class ColorHSL
Public Sub New(ByVal h As Double, ByVal s As Double, ByVal l As Double, ByVal a As Double)
End Sub
Public Function AddLuminance(ByVal offset As Single) As ColorHSL
Return Me
End Function
End Class
`;
const FRAME_SERVER_FACTORY = `Public Class FrameServerFactory
Public Shared Function Create(ByVal path As String) As Object
Return Nothing
End Function
End Class
`;
const HELPER = `Public Class Helper
Public Sub Run()
End Sub
End Class
`;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-ties-'));
const files: Record<string, string> = {
'Source/StaxRip.vbproj': VBPROJ,
'Source/Tools/AutoCrop/AutoCrop.vbproj': VBPROJ,
'Source/Tools/AutoCrop/Main.vb': COLOR_HSL + FRAME_SERVER_FACTORY,
'Source/UI/ColorHSL.vb': COLOR_HSL,
'Source/Video/FrameServer.vb': FRAME_SERVER_FACTORY,
'Source/General/ThemeManager.vb': `Public Class ThemeManager
Public Sub Apply(ByVal palette As Object)
Dim _backColor As ColorHSL = New ColorHSL(0, 0.01, 0.1, 1)
Dim _controlBackColor As ColorHSL = _backColor.AddLuminance(0.025)
For Each backgroundColor In palette
backgroundColor.AddLuminance(-0.1)
Next
Dim server = FrameServerFactory.Create("video.mkv")
End Sub
End Class
`,
'Source/UI/TipProvider.vb': `Public Class TipProvider
Public Sub SetTip(ByVal text As String)
End Sub
Public Sub SetTip(ByVal text As String, ByVal title As String)
End Sub
End Class
`,
'Source/General/Tips.vb': `Public Class Tips
Public Sub Apply(ByVal providers As Object)
For Each tipProvider In providers
tipProvider.SetTip("x")
Next
End Sub
End Class
`,
'Plugins/A/A.vbproj': VBPROJ,
'Plugins/A/Helper.vb': HELPER,
'Plugins/B/B.vbproj': VBPROJ,
'Plugins/B/Helper.vb': HELPER,
'App/App.vbproj': VBPROJ,
'App/Main.vb': `Public Class Main
Public Sub Start(ByVal helpers As Object)
For Each runHelper In helpers
runHelper.Run()
Next
End Sub
End Class
`,
};
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 @ file` of every call / instantiation edge out of a file. */
function targetsFrom(file: string, kind: 'calls' | 'instantiates'): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === kind)
.map((e) => {
const target = cg.getNode(e.target)!;
return `${target.qualifiedName} @ ${target.filePath}`;
})
.sort();
}
describe('VB.NET duplicate types across projects', () => {
it('resolve a call to the caller’s own project’s copy', () => {
expect(targetsFrom('Source/General/ThemeManager.vb', 'calls')).toEqual([
'ColorHSL::AddLuminance @ Source/UI/ColorHSL.vb',
'ColorHSL::AddLuminance @ Source/UI/ColorHSL.vb',
'FrameServerFactory::Create @ Source/Video/FrameServer.vb',
]);
});
it('resolve a construction to the caller’s own project’s copy', () => {
expect(targetsFrom('Source/General/ThemeManager.vb', 'instantiates')).toEqual([
'ColorHSL @ Source/UI/ColorHSL.vb',
]);
});
it('make no guess between copies nothing tells apart', () => {
expect(targetsFrom('App/Main.vb', 'calls')).toEqual([]);
});
it('take one type’s overloads as one guess, not a tie', () => {
expect(targetsFrom('Source/General/Tips.vb', 'calls')).toEqual(['TipProvider::SetTip @ Source/UI/TipProvider.vb']);
});
});