mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
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:
@@ -19,6 +19,11 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
|
||||
- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List<Foo> Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
|
||||
- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337)
|
||||
- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
|
||||
- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
|
||||
- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
|
||||
- In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name.
|
||||
- VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it.
|
||||
- Upgrading CodeGraph while an agent session is open no longer leaves the old version's background server in charge of your project: the first session started from the new install stops it and starts a current one in its place, even while sessions opened before the upgrade are still running. That old server could no longer load the language parsers the upgrade removed, so it saved every file it re-indexed with no symbols, while new sessions could only read the index beside it without keeping it up to date. A background server from a newer install is never stopped, and sessions opened before the upgrade keep the old version until you restart them. Thanks @lipchey for the report. (#2335)
|
||||
- A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335)
|
||||
- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,12 @@
|
||||
* (`GetService(Of Notifier).Notify()`). SCrawler's designer code sent
|
||||
* `New System.Drawing.Size(…)` to a nested enum's `Size` case 713 times and
|
||||
* `Controls.Add(…)` to a collection class's `Add` 547 times.
|
||||
*
|
||||
* The name read is the one the reference starts at (or after) — in
|
||||
* `Me.Size = New System.Drawing.Size(…)` that is `System.Drawing.Size`, not
|
||||
* `Me.Size` — and a type is one its written qualifier names: staxrip's
|
||||
* designer `New System.Drawing.Point(…)` went to a nested `Point` class 550
|
||||
* times. A type nested in a class is named bare only inside it.
|
||||
*/
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
@@ -56,6 +62,24 @@ End Module
|
||||
Private Sub Refresh()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'UI/ButtonEx.vb': `Public Class ButtonEx
|
||||
Public Class SymbolDrawer
|
||||
Public Class Point
|
||||
End Class
|
||||
Public Sub Draw()
|
||||
Dim p As New Point()
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
`,
|
||||
'Forms/AppsForm.vb': `Public Class AppsForm
|
||||
Private Sub InitializeComponent()
|
||||
Me.Size = New System.Drawing.Size(315, 205)
|
||||
Me.ToolStrip.Location = New System.Drawing.Point(473, 10)
|
||||
Me.lDescription.Location = New Point(4, 285)
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
@@ -84,4 +108,15 @@ describe('VB.NET member access', () => {
|
||||
expect(targets).toContain('Notifier::Notify');
|
||||
expect(targets).toContain('MainForm::Refresh');
|
||||
});
|
||||
|
||||
it('reads the name the reference starts at, and a type its qualifier names', () => {
|
||||
const targetsOf = (file: string) => cg
|
||||
.getOutgoingEdgesFrom(cg.getNodesInFile(file).map((n) => n.id))
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
|
||||
.map((e) => `${e.kind} ${cg.getNode(e.target)!.qualifiedName}`)
|
||||
.sort();
|
||||
// `Me.Size = New System.Drawing.Size(…)`, `New System.Drawing.Point(…)`, `New Point(…)` outside ButtonEx.
|
||||
expect(targetsOf('Forms/AppsForm.vb')).toEqual([]);
|
||||
expect(targetsOf('UI/ButtonEx.vb')).toEqual(['instantiates ButtonEx::SymbolDrawer::Point']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* A VB.NET call through a local, a parameter, a field or a property is a call
|
||||
* on the type it is declared with — `Dim x As T`, `ByVal x As T`,
|
||||
* `x As New T`, `Private File As SFile`, `Property File As SFile`, a type
|
||||
* character (`Dim name$`), or what `Dim x = obj.GetString(…)` returns —
|
||||
* never a guess at a same-named method of some project class. Keywords are
|
||||
* matched without regard to case. A declared type the project does not define
|
||||
* means nothing of the project's, except an extension method declared for it.
|
||||
*
|
||||
* - SCrawler's `ThumbnailFile.Delete(SFO.File, …)` on an `SFile` (from the
|
||||
* external PersonalUtilities library) went to a nested `TempFileConversion`
|
||||
* class's `Delete`, `GroupFile.Delete()` to a download group's,
|
||||
* `UserUpdatedEventHandlers.Add(e)` on a `List(Of …)` to `UserDataBind.Add`
|
||||
* and `TotalSize.CompareTo(…)` on a `Double` to a plugin's `VSize.CompareTo`;
|
||||
* - staxrip's `timestampFontColorValue.ToColor(…)`, a String from
|
||||
* `settings.GetString(…)`, went to a `ColorHSL.ToColor()` instead of the
|
||||
* String extension `StringExtensions.ToColor`.
|
||||
*
|
||||
* The type a name means is the one VB.NET's lookup finds where it is written:
|
||||
* the namespaces around it (SCrawler declares a `SiteSettings` and an `M3U8`
|
||||
* per site namespace), a base class's nested types, the file's `Imports`
|
||||
* aliases, and the type arguments a subclass gives its base. The declared
|
||||
* type wins over whatever is assigned later, a `For Each` variable is an
|
||||
* element of what it loops over, and an interface a class implements lends
|
||||
* it no members to read bare.
|
||||
*/
|
||||
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-receiver-'));
|
||||
const files: Record<string, string> = {
|
||||
'Download/Groups/DownloadGroup.vb': `Namespace DownloadObjects.Groups
|
||||
Friend Class DownloadGroup
|
||||
Friend Sub Delete()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'YouTube/Objects/YouTubeMediaContainerBase.vb': `Namespace API.YouTube.Objects
|
||||
Public MustInherit Class YouTubeMediaContainerBase
|
||||
Public ReadOnly Property ThumbnailFile As SFile
|
||||
Private Class TempFileConversion
|
||||
Friend Sub Delete(ByVal Mode As Integer)
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Download/Groups/DownloadGroupCollection.vb': `Namespace DownloadObjects.Groups
|
||||
Friend Class DownloadGroupCollection
|
||||
Private ReadOnly GroupFile As SFile = "Settings\\Groups.xml"
|
||||
Friend Sub Update()
|
||||
GroupFile.Delete()
|
||||
End Sub
|
||||
Friend Sub Reset(byval groupFile as SFile)
|
||||
groupFile.Delete()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Hosts/DownloadableMediaHost.vb': `Namespace Hosts
|
||||
Friend Class DownloadableMediaHost : Inherits API.YouTube.Objects.YouTubeMediaContainerBase
|
||||
Friend Sub Clean()
|
||||
ThumbnailFile.Delete(1)
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'API/UserDataBind.vb': `Namespace API
|
||||
Friend Class UserDataBind
|
||||
Friend Sub Add(ByVal User As Object)
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'API/Base/UserDataBase.vb': `Namespace API.Base
|
||||
Friend MustInherit Class UserDataBase
|
||||
Private ReadOnly UserUpdatedEventHandlers As List(Of UserUpdatedEventHandler)
|
||||
Friend Sub AddUpdateHandler(ByVal e As UserUpdatedEventHandler)
|
||||
UserUpdatedEventHandlers.Add(e)
|
||||
End Sub
|
||||
Friend Sub Download()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'API/Instagram/UserData.vb': `Namespace API.Instagram
|
||||
Friend Class UserData : Inherits API.Base.UserDataBase
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'API/Instagram/Downloader.vb': `Namespace API.Instagram
|
||||
Friend Class Downloader
|
||||
Friend Sub Run(ByVal u As UserData)
|
||||
u.Download()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Plugin/UserData.vb': `Friend Class VSize
|
||||
Public Function CompareTo(ByVal Other As VSize) As Integer
|
||||
Return 0
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'Editors/UsersInfoForm.vb': `Friend Class UsersInfoForm
|
||||
Private NotInheritable Class UserOpt
|
||||
Friend Property TotalSize As Double = 0
|
||||
Friend Function CompareTo(ByVal Other As UserOpt) As Integer
|
||||
Return TotalSize.CompareTo(Other.TotalSize) * -1
|
||||
End Function
|
||||
End Class
|
||||
End Class
|
||||
`,
|
||||
'UI/ColorHSL.vb': `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
|
||||
Public Function ToColor() As Integer
|
||||
Return 0
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'UI/BackColorAdjuster.vb': `Public Class BackColorAdjuster
|
||||
Public Function AddLuminance(ByVal offset As Single) As Integer
|
||||
Return 0
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'UI/ThemeManager.vb': `Public Class ThemeManager
|
||||
Public Sub Apply()
|
||||
Dim _backColor As ColorHSL = New ColorHSL(0, 0.01, 0.1, 1)
|
||||
Dim _controlBackColor As ColorHSL = _backColor.AddLuminance(0.025)
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'UI/Popup.vb': `Public Class Popup
|
||||
Public Sub Show()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'UI/MainForm.vb': `Public Class MainForm
|
||||
Inherits Form
|
||||
|
||||
Public Sub OpenSettings()
|
||||
Dim settingsForm As New MainForm
|
||||
settingsForm.Show()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'General/Extensions.vb': `Imports System.Runtime.CompilerServices
|
||||
|
||||
Module StringExtensions
|
||||
<Extension>
|
||||
Function ToColor(ByVal str As String, Optional ByVal defaultColor As Integer = 0) As Integer
|
||||
Return 0
|
||||
End Function
|
||||
<Extension>
|
||||
Function Join(ByVal instance As IEnumerable(Of String), ByVal delimiter As String) As String
|
||||
Return ""
|
||||
End Function
|
||||
<Extension>
|
||||
Function Sort(Of T)(ByVal instance As IEnumerable(Of T)) As IEnumerable(Of T)
|
||||
Return instance
|
||||
End Function
|
||||
End Module
|
||||
`,
|
||||
'General/ObjectStorage.vb': `Public Class ObjectStorage
|
||||
Public Function GetString(ByVal key As String, Optional ByVal defaultValue As String = Nothing) As String
|
||||
Return defaultValue
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'General/Thumbnailer.vb': `Public Class Thumbnailer
|
||||
Public Sub Run(ByVal settings As ObjectStorage)
|
||||
Dim timestampFontColorValue = settings.GetString("TimestampFontColor", "#fff")
|
||||
Dim timestampFontColor = timestampFontColorValue.ToColor(1)
|
||||
Dim colorText As String = "#000"
|
||||
Dim outline = colorText.ToColor()
|
||||
Dim name$ = "x"
|
||||
Dim named = name.ToColor()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'General/Lists.vb': `Public Class Lists
|
||||
Public Sub Run()
|
||||
Dim names As New List(Of String)
|
||||
names.Sort()
|
||||
Dim joined = names.Join(", ")
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'Base/DownDetector.vb': `Namespace API.Base
|
||||
Friend NotInheritable Class DownDetector
|
||||
Friend MustInherit Class Checker(Of T)
|
||||
Protected ReadOnly Property Source As T
|
||||
End Class
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Sites/Bluesky/SiteSettings.vb': `Namespace API.Bluesky
|
||||
Friend Class SiteSettings
|
||||
Friend Function IsMyUser(ByVal url As String) As Boolean
|
||||
Return False
|
||||
End Function
|
||||
Friend Function AvailableTrueValue() As Boolean
|
||||
Return False
|
||||
End Function
|
||||
End Class
|
||||
Friend NotInheritable Class M3U8
|
||||
Friend Shared Sub Download(ByVal url As String)
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Sites/Reddit/SiteSettings.vb': `Namespace API.Reddit
|
||||
Friend Class SiteSettings
|
||||
Friend Function IsMyUser(ByVal url As String) As Boolean
|
||||
Return True
|
||||
End Function
|
||||
Friend Function AvailableTrueValue() As Boolean
|
||||
Return True
|
||||
End Function
|
||||
Private Class MyDownDetector : Inherits API.Base.DownDetector.Checker(Of SiteSettings)
|
||||
Friend Sub Check()
|
||||
Source.AvailableTrueValue()
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
Friend NotInheritable Class M3U8
|
||||
Friend Shared Sub Download(ByVal url As String)
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Sites/Reddit/UserData.vb': `Namespace API.Reddit
|
||||
Friend Class UserData
|
||||
Private ReadOnly Property MySettings As SiteSettings
|
||||
Friend Sub Check(ByVal url As String)
|
||||
MySettings.IsMyUser(url)
|
||||
M3U8.Download(url)
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Encoding/VideoEncoder.vb': `Public MustInherit Class VideoEncoder
|
||||
Public Class MenuList
|
||||
Public Sub Add(ByVal text As String)
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
Public Class BatchEncoder
|
||||
Inherits VideoEncoder
|
||||
Public Function GetMenu() As Object
|
||||
Dim ret As New MenuList
|
||||
ret.Add("Codec Configuration")
|
||||
Return ret
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'YouTube/YouTubeFunctions.vb': `Friend Interface IContainer
|
||||
Sub Parse()
|
||||
End Interface
|
||||
Friend Class Channel : Implements IContainer
|
||||
Friend Sub Parse() Implements IContainer.Parse
|
||||
End Sub
|
||||
End Class
|
||||
Friend Module YouTubeFunctions
|
||||
Friend Sub Load()
|
||||
Dim item As IContainer
|
||||
item = New Channel
|
||||
item.Parse()
|
||||
End Sub
|
||||
End Module
|
||||
`,
|
||||
'UI/Labels.vb': `Public Class LabelUI
|
||||
Public Function AddLabel(ByVal text As String) As Object
|
||||
Return Nothing
|
||||
End Function
|
||||
End Class
|
||||
Public Class SimpleUI
|
||||
Public Function AddLabel(ByVal text As String) As Object
|
||||
Return Nothing
|
||||
End Function
|
||||
Public Function AddLabel(ByVal text As String, ByVal width As Integer) As Object
|
||||
Return Nothing
|
||||
End Function
|
||||
End Class
|
||||
Public Class SimpleSettingsForm
|
||||
Friend WithEvents SimpleUI As SimpleUI
|
||||
End Class
|
||||
`,
|
||||
'UI/AudioForm.vb': `Public Class AudioForm
|
||||
Public Sub ShowAdvanced()
|
||||
Using form As New SimpleSettingsForm()
|
||||
Dim ui = form.SimpleUI
|
||||
ui.AddLabel("EBU R128")
|
||||
End Using
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'UI/Theme.vb': `Public Class ButtonLabel
|
||||
Public Sub ApplyTheme()
|
||||
End Sub
|
||||
End Class
|
||||
Public Class ToggleButtonLabel
|
||||
Public Sub ApplyTheme()
|
||||
End Sub
|
||||
End Class
|
||||
Public Class ThemeApplier
|
||||
Public Sub Apply(ByVal controls As Object)
|
||||
For Each control In controls.OfType(Of ToggleButtonLabel)
|
||||
control.ApplyTheme()
|
||||
Next
|
||||
End Sub
|
||||
Public Sub ApplyAll(ByVal labels As List(Of ToggleButtonLabel))
|
||||
For Each label In labels
|
||||
label.ApplyTheme()
|
||||
Next
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'Download/TDownloader.vb': `Namespace App.Download
|
||||
Friend Class TDownloader
|
||||
Friend Class Job
|
||||
Friend Sub Start()
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
Friend Class Worker
|
||||
Friend Sub Start()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Download/DownloadProgress.vb': `Imports TDJob = App.Download.TDownloader.Job
|
||||
|
||||
Namespace App.Download
|
||||
Friend Class DownloadProgress
|
||||
Friend ReadOnly Property Worker As TDJob
|
||||
Friend Sub Run()
|
||||
Worker.Start()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Plugin/IPluginContentProvider.vb': `Namespace Plugin
|
||||
Public Interface ISiteSettings
|
||||
End Interface
|
||||
Public Interface IPluginContentProvider
|
||||
Property Settings As ISiteSettings
|
||||
End Interface
|
||||
End Namespace
|
||||
`,
|
||||
'MainMod.vb': `Friend Module MainMod
|
||||
Friend Settings As SettingsCLS
|
||||
End Module
|
||||
`,
|
||||
'SettingsCLS.vb': `Friend Class SettingsCLS
|
||||
Friend Sub UpdateUsersList()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'API/Base/UserDataProvider.vb': `Namespace API.Base
|
||||
Friend MustInherit Class UserDataProvider : Implements Plugin.IPluginContentProvider
|
||||
Friend Property MySettings As Plugin.ISiteSettings Implements Plugin.IPluginContentProvider.Settings
|
||||
Friend Sub Delete()
|
||||
Settings.UpdateUsersList()
|
||||
End Sub
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
};
|
||||
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 a file. */
|
||||
function callsFrom(file: string): string[] {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg
|
||||
.getOutgoingEdgesFrom(ids)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!.qualifiedName)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('VB.NET calls through declared receivers', () => {
|
||||
it('reach nothing of the project on a type it does not define', () => {
|
||||
expect(callsFrom('Download/Groups/DownloadGroupCollection.vb')).toEqual([]);
|
||||
expect(callsFrom('Hosts/DownloadableMediaHost.vb')).toEqual([]);
|
||||
expect(callsFrom('API/Base/UserDataBase.vb')).toEqual([]);
|
||||
});
|
||||
|
||||
it('reach nothing of the project on a VB.NET built-in type', () => {
|
||||
expect(callsFrom('Editors/UsersInfoForm.vb')).toEqual([]);
|
||||
});
|
||||
|
||||
it('reach the declared type’s own method, or the one it inherits', () => {
|
||||
expect(callsFrom('UI/ThemeManager.vb')).toEqual(['ColorHSL::AddLuminance']);
|
||||
expect(callsFrom('API/Instagram/Downloader.vb')).toEqual(['API.Base::UserDataBase::Download']);
|
||||
});
|
||||
|
||||
it('reach nothing of the project for a method the declared type inherits from outside it', () => {
|
||||
expect(callsFrom('UI/MainForm.vb')).toEqual([]);
|
||||
});
|
||||
|
||||
it('reach an extension method declared for the type, through what a call returns too', () => {
|
||||
expect(callsFrom('General/Thumbnailer.vb')).toEqual([
|
||||
'ObjectStorage::GetString',
|
||||
'StringExtensions::ToColor',
|
||||
'StringExtensions::ToColor',
|
||||
'StringExtensions::ToColor',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reach a .NET type’s own method before an extension of that name, and an extension it lacks', () => {
|
||||
// `List(Of T)` has its own `Sort()`, but no `Join`.
|
||||
expect(callsFrom('General/Lists.vb')).toEqual(['StringExtensions::Join']);
|
||||
});
|
||||
|
||||
it('name the type their declaration’s namespace sees, through a base class’s type argument too', () => {
|
||||
// SCrawler declares a `SiteSettings` and an `M3U8` in each site's namespace.
|
||||
expect(callsFrom('Sites/Reddit/UserData.vb')).toEqual([
|
||||
'API.Reddit::M3U8::Download',
|
||||
'API.Reddit::SiteSettings::IsMyUser',
|
||||
]);
|
||||
// `Inherits DownDetector.Checker(Of SiteSettings)` makes its `Source As T` a SiteSettings.
|
||||
expect(callsFrom('Sites/Reddit/SiteSettings.vb')).toEqual(['API.Reddit::SiteSettings::AvailableTrueValue']);
|
||||
// A type nested in a base class is named bare in a subclass: staxrip's `Dim ret As New MenuList`.
|
||||
expect(callsFrom('Encoding/VideoEncoder.vb')).toEqual(['VideoEncoder::MenuList::Add']);
|
||||
});
|
||||
|
||||
it('take the declared type over what is assigned, and an import alias for what it names', () => {
|
||||
expect(callsFrom('YouTube/YouTubeFunctions.vb')).toEqual(['IContainer::Parse']);
|
||||
expect(callsFrom('Download/DownloadProgress.vb')).toEqual(['App.Download::TDownloader::Job::Start']);
|
||||
});
|
||||
|
||||
it('read a field through what a call returns, and a loop variable through what it loops over', () => {
|
||||
expect(callsFrom('UI/AudioForm.vb')).toEqual(['SimpleUI::AddLabel']);
|
||||
// `For Each control In controls.OfType(Of ToggleButtonLabel)`, `For Each label In labels` over a `List(Of ToggleButtonLabel)`.
|
||||
expect(callsFrom('UI/Theme.vb')).toEqual(['ToggleButtonLabel::ApplyTheme', 'ToggleButtonLabel::ApplyTheme']);
|
||||
});
|
||||
|
||||
it('see a module’s field, not the property of an interface the class implements', () => {
|
||||
expect(callsFrom('API/Base/UserDataProvider.vb')).toEqual(['SettingsCLS::UpdateUsersList']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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']);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user