Files
codegraph/__tests__/vbnet-std-methods.test.ts
T
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

76 lines
2.6 KiB
TypeScript

/**
* VB.NET runs on the same .NET base library as C#, so a call such as
* `x.Contains(…)`, `x.Add(…)` or `x.Dispose()` on a receiver whose type is not
* known is far more likely the library's method than the one project method
* that happens to share the name. As in C#, such a guess is kept only when the
* receiver is named after the method's owner. staxrip's
* `SupportedInput.Contains(ret)` — a String array — went to `VideoScript`'s
* `Contains`.
*/
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-std-'));
const files: Record<string, string> = {
'Video/VideoScript.vb': `Public Class VideoScript
Public Function Contains(ByVal value As String) As Boolean
Return False
End Function
End Class
`,
'Audio/AudioProfile.vb': `Public Class AudioProfile
Public Function IsInputSupported(ByVal inputs As Object, ByVal ext As String) As Boolean
For Each supported In inputs
If supported.Contains(ext) Then Return True
Next
Return False
End Function
Public Function HasScript(ByVal scripts As Object, ByVal name As String) As Boolean
For Each script In scripts
If script.Contains(name) Then Return True
Next
Return False
End Function
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` of every call edge out of one method. */
function callsOf(qualifiedName: string): string[] {
const from = cg.getNodesInFile('Audio/AudioProfile.vb').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 standard-library method names', () => {
it('are not a project method on a receiver that does not name its owner', () => {
expect(callsOf('AudioProfile::IsInputSupported')).toEqual([]);
});
it('still reach the project method when the receiver names its owner', () => {
expect(callsOf('AudioProfile::HasScript')).toEqual(['VideoScript::Contains']);
});
});