mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
c9944147bb
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>
144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
/**
|
||
* 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']);
|
||
});
|
||
});
|