diff --git a/CHANGELOG.md b/CHANGELOG.md index da460b5b7..b5476b128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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) diff --git a/__tests__/vbnet-bare-member-scope.test.ts b/__tests__/vbnet-bare-member-scope.test.ts new file mode 100644 index 000000000..a2601671b --- /dev/null +++ b/__tests__/vbnet-bare-member-scope.test.ts @@ -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 = { + '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', + ]); + }); +}); diff --git a/__tests__/vbnet-member-access.test.ts b/__tests__/vbnet-member-access.test.ts index d8643ecc5..668cc18aa 100644 --- a/__tests__/vbnet-member-access.test.ts +++ b/__tests__/vbnet-member-access.test.ts @@ -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']); + }); }); diff --git a/__tests__/vbnet-project-ties.test.ts b/__tests__/vbnet-project-ties.test.ts new file mode 100644 index 000000000..9d8e2fcc0 --- /dev/null +++ b/__tests__/vbnet-project-ties.test.ts @@ -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 = ` + + WinExe + + +`; + +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 = { + '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']); + }); +}); diff --git a/__tests__/vbnet-receiver-types.test.ts b/__tests__/vbnet-receiver-types.test.ts new file mode 100644 index 000000000..0a8130974 --- /dev/null +++ b/__tests__/vbnet-receiver-types.test.ts @@ -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 = { + '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 + + Function ToColor(ByVal str As String, Optional ByVal defaultColor As Integer = 0) As Integer + Return 0 + End Function + + Function Join(ByVal instance As IEnumerable(Of String), ByVal delimiter As String) As String + Return "" + End Function + + 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']); + }); +}); diff --git a/__tests__/vbnet-std-methods.test.ts b/__tests__/vbnet-std-methods.test.ts new file mode 100644 index 000000000..8c29b1e77 --- /dev/null +++ b/__tests__/vbnet-std-methods.test.ts @@ -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 = { + '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']); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 1eaf14b14..4c9fb45f2 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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); } } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index f721a2cc4..0ba9c946d 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 = 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 = 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 | 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 = 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(`(?>>(); /** @@ -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)) && diff --git a/src/resolution/vbnet-receivers.ts b/src/resolution/vbnet-receivers.ts new file mode 100644 index 000000000..07732927e --- /dev/null +++ b/src/resolution/vbnet-receivers.ts @@ -0,0 +1,1335 @@ +/** + * VB.NET receivers, projects and receiver-less calls. + * + * The name matcher guesses a `receiver.Method()` call it cannot type by the + * method's name alone (matchMethodCall's Strategy 3: a unique name, or the + * receiver sharing words with the owner), and a receiver-less call by the + * nearest same-named member. VB.NET brought none of the evidence C# does to + * those guesses, so a call landed on whichever project method shared its + * name: SCrawler's `ThumbnailFile.Delete(…)` on an external `SFile` went to a + * nested `TempFileConversion.Delete`, staxrip's main app called its AutoCrop + * tool's copy of `ColorHSL`. What this module reads, from the source: + * + * 1. A receiver's declared type — `Dim x As T`, `ByVal x As T`, `x As New T`, + * a type character (`Dim name$`), `Dim x = New T(…)`, a `For Each` over a + * typed collection, a field or property of the class, of one it inherits + * (with the type arguments the subclass gives) or of a `Module`, or what + * `Dim x = obj.GetString(…)` returns. A typed receiver's call is the + * type's own method or one it inherits, else an extension method declared + * for the type, else nothing of the project's: a type the project does + * not define (`SFile`, `String`, `List(Of T)`) has none of its methods. + * 2. Which type a name means where it is written: the namespaces around it, + * outward, then its file's and project's `Imports` (aliases included), + * then the project a file belongs to — the `.vbproj` above it, whose + * `RootNamespace` its namespaces are inside. Between same-named types, and + * between guesses nothing else tells apart, the caller's own project's + * comes first; a written qualifier (`System.Drawing.Point`) must match. + * 3. A receiver-less call (or one on `Me` / `MyClass` / `MyBase`, which the + * extractor drops) is a member of a type around it, of what those inherit, + * or of a `Module`; a type nested in a class is named bare only inside it. + * + * VB.NET's keywords and names are matched without regard to case. Nothing + * here runs for another language. + */ +import * as fs from 'fs'; +import * as path from 'path'; +import type { Node } from '../types'; +import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types'; + +/** A VB.NET declared type: its simple name (no namespace, no type arguments) and whether it is an array. */ +interface VbType { + name: string; + array: boolean; + /** The namespaces or types written before the name (`API.Base` in `API.Base.UserDataBase`), lowercased. */ + qualifier?: string[]; + /** Where the name is written, which decides the namespaces it is looked up in. */ + file?: string; + line?: number; + /** A supertype named in an `Implements` statement: its members are not the type's own. */ + implemented?: boolean; + /** The type arguments written with it: `SiteSettings` in `Checker(Of SiteSettings)`. */ + args?: VbType[]; +} + +/** What a statement says a local is: its type, the call whose result it holds, or a binding that names no type. */ +type VbBinding = + | { kind: 'type'; type: VbType } + | { kind: 'call'; receiver: string | null; member: string } + | { kind: 'each'; collection: string } + | { kind: 'unknown' }; + +/** .NET's collections of one element type: `For Each x In list` over a `List(Of T)` binds a `T`. */ +const VB_ELEMENT_COLLECTIONS = /^(?:List|IList|IEnumerable|ICollection|IReadOnlyList|IReadOnlyCollection|HashSet|SortedSet|Queue|Stack|LinkedList|ObservableCollection|Collection|ReadOnlyCollection|BindingList|ConcurrentBag|ConcurrentQueue|ConcurrentStack|BlockingCollection)$/i; + +/** The type of what a `For Each` over a value of type `t` binds: an array's or a .NET collection's element type. */ +function elementType(t: VbType): VbType | null { + if (t.array) return { ...t, array: false }; + return t.args?.length === 1 && VB_ELEMENT_COLLECTIONS.test(t.name) && t.args[0]!.name !== '?' ? t.args[0]! : null; +} + +/** VB.NET's built-in types, by keyword and by .NET name, keyed to the keyword. */ +const VB_BUILTIN_TYPES: ReadonlyMap = new Map([ + ...['boolean', 'byte', 'char', 'date', 'decimal', 'double', 'integer', 'long', 'object', 'sbyte', 'short', 'single', + 'string', 'uinteger', 'ulong', 'ushort'].map((k): [string, string] => [k, k]), + ['int16', 'short'], ['int32', 'integer'], ['int64', 'long'], ['uint16', 'ushort'], ['uint32', 'uinteger'], + ['uint64', 'ulong'], ['datetime', 'date'], +]); + +/** The type a type character declares: `Dim aStr$`, `For i% = 0 …`. */ +const VB_TYPE_CHARS: Readonly> = { + $: 'String', '%': 'Integer', '&': 'Long', '!': 'Single', '#': 'Double', '@': 'Decimal', +}; + +/** VB.NET's conversion functions and the built-in type each returns. */ +const VB_CONVERSIONS: Readonly> = { + cbool: 'Boolean', cbyte: 'Byte', cchar: 'Char', cdate: 'Date', cdbl: 'Double', cdec: 'Decimal', cint: 'Integer', + clng: 'Long', cobj: 'Object', csbyte: 'SByte', cshort: 'Short', csng: 'Single', cstr: 'String', cuint: 'UInteger', + culng: 'ULong', cushort: 'UShort', +}; + +/** VB.NET type nodes a member can belong to (a `Module` is indexed as a class). */ +const VB_TYPE_KINDS: ReadonlySet = new Set(['class', 'struct', 'interface']); +const VB_VALUE_KINDS: ReadonlySet = new Set(['field', 'property', 'constant', 'variable']); +const VB_MEMBER_KINDS: ReadonlySet = new Set(['method', 'property', 'field', 'enum_member', 'constant', 'variable']); + +/** A declaration keyword right before a name: the name is a member being declared, not a variable. */ +const VB_MEMBER_HEAD = /\b(?:Function|Sub|Property|Event|Operator|Declare|Delegate|Class|Structure|Module|Interface|Enum|Namespace)\s+$/i; + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** A type's key for comparing two of them: the built-in keyword or the lowercased name, `[]` for an array. */ +function typeKey(t: VbType): string { + const lower = t.name.toLowerCase(); + return `${VB_BUILTIN_TYPES.get(lower) ?? lower}${t.array ? '[]' : ''}`; +} + +function isBuiltin(t: VbType): boolean { + return !t.array && VB_BUILTIN_TYPES.has(t.name.toLowerCase()); +} + +/** The index of the `)` closing the `(` at `open`, or -1. */ +function closeParen(text: string, open: number): number { + let depth = 0; + for (let i = open; i < text.length; i++) { + if (text[i] === '(') depth++; + else if (text[i] === ')' && --depth === 0) return i; + } + return -1; +} + +/** The top-level items of the parenthesized list opening at `open`, or null when it does not close. */ +function splitArgs(text: string, open: number): string[] | null { + const close = closeParen(text, open); + if (close < 0) return null; + const args: string[] = []; + let depth = 0; + let cur = ''; + for (const ch of text.slice(open + 1, close)) { + if (ch === '(' || ch === '{') depth++; + else if (ch === ')' || ch === '}') depth--; + if (ch === ',' && depth === 0) { + args.push(cur); + cur = ''; + } else cur += ch; + } + args.push(cur); + return args.map((a) => a.trim()); +} + +/** + * The type written at `text[at]` — `List(Of Foo)`, `String()`, `Integer?`, + * `Global.A.B` — by its last name; null when no type starts there. + */ +function readType(text: string, at: number): VbType | null { + const head = /^\s*([A-Za-z_][\w.]*)/.exec(text.slice(at)); + if (!head) return null; + let i = at + head[0].length; + let args: VbType[] | undefined; + if (/^\s*\(\s*Of\b/i.test(text.slice(i))) { + const open = text.indexOf('(', i); + const close = closeParen(text, open); + if (close < 0) return null; + args = (splitArgs(text, open) ?? []).map((a) => readType(a.replace(/^Of\s+/i, ''), 0) ?? { name: '?', array: false }); + i = close + 1; + } + const segments = head[1]!.split('.'); + const name = segments.pop()!; + if (!/^[A-Za-z_]\w*$/.test(name) || /^(?:New|Of|As|In|Out|From|With)$/i.test(name)) return null; + const qualifier = segments.map((s) => s.toLowerCase()).filter((s, i) => !(i === 0 && s === 'global')); + return { + name, + array: /^\s*\??\s*\(\s*,*\s*\)/.test(text.slice(i)), + ...(qualifier.length > 0 ? { qualifier } : {}), + ...(args ? { args } : {}), + }; +} + +/** `t` — and the type arguments written inside it — written at `file:line`. */ +function sited(t: VbType | null, file: string, line: number): VbType | null { + return t ? { ...t, file, line, ...(t.args ? { args: t.args.map((a) => sited(a, file, line)!) } : {}) } : null; +} + +/** A VB.NET line's code: its comment dropped and every string literal emptied to `""`. */ +function vbCode(line: string): string { + // Most lines have neither: the line itself, without a copy. + if (!/["'‘’\r]/.test(line)) return line; + let out = ''; + for (let i = 0; i < line.length; i++) { + const ch = line[i]!; + if (ch === '"') { + let j = i + 1; + for (; j < line.length; j++) { + if (line[j] !== '"') continue; + if (line[j + 1] === '"') j++; + else break; + } + out += '""'; + i = j; + continue; + } + if (ch === "'" || ch === '\u2018' || ch === '\u2019' || ch === '\r') break; + out += ch; + } + return out; +} + +const VB_CODE_LINES = new WeakMap>(); + +/** A file's lines as code (see vbCode), read once per file. */ +function codeLines(file: string, context: ResolutionContext): string[] { + let memo = VB_CODE_LINES.get(context); + if (!memo) VB_CODE_LINES.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const lines = (context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? []).map(vbCode); + if (memo.size >= 1024) memo.delete(memo.keys().next().value!); + memo.set(file, lines); + return lines; +} + +interface VbPatterns { + /** The name standing where a binding names it — not as `name.Member` nor `name(args)`, which only use it. */ + declared: RegExp; + /** `name As T`, `name() As T`, `name As New T`: a variable, parameter, loop or catch variable. */ + asClause: RegExp; + /** `Dim name, other As T`: a declarator sharing the next one's type. */ + listed: RegExp; + /** `Dim name$`, `ByVal name%`, `For i% = …`. */ + typeChar: RegExp; + /** `Dim name = …`: the value's type. */ + inferred: RegExp; + /** `For Each name In …`: the collection's element type, when it says it. */ + forEach: RegExp; + /** A binding that names no type: `For name = …`, `Function(name)`, `From name In`, `Catch name`. */ + loose: RegExp; +} + +const VB_PATTERNS = new Map(); + +function patternsFor(name: string): VbPatterns { + const key = name.toLowerCase(); + const hit = VB_PATTERNS.get(key); + if (hit) return hit; + const r = escapeRegex(key); + const patterns: VbPatterns = { + declared: new RegExp(`(?= 4096) VB_PATTERNS.delete(VB_PATTERNS.keys().next().value!); + VB_PATTERNS.set(key, patterns); + return patterns; +} + +/** + * What every binding of a name has on its line: a declaring keyword, or a + * type character. Most lines naming a variable just use it (`sb.Append(…)`). + */ +const VB_MAY_BIND = /\b(?:As|Dim|Static|Const|For|Each|Function|Sub|Catch|Using|From|Let|Aggregate)\b|\w[$%&!#@]/i; + +/** What one line of code says `name` is, or undefined when it doesn't bind the name. */ +function bindingOn(code: string, name: string): VbBinding | undefined { + if (!VB_MAY_BIND.test(code)) return undefined; + const p = patternsFor(name); + if (!p.declared.test(code)) return undefined; + p.asClause.lastIndex = 0; + for (let m = p.asClause.exec(code); m; m = p.asClause.exec(code)) { + if (VB_MEMBER_HEAD.test(code.slice(0, m.index))) continue; + const type = readType(code, m.index + m[0].length); + if (!type) return { kind: 'unknown' }; + // `As New T(…)`: the parentheses are the constructor's. + return { kind: 'type', type: m[2] ? { ...type, array: false } : { ...type, array: type.array || !!m[1] } }; + } + const listed = p.listed.exec(code); + if (listed) { + const type = readType(code, listed.index + listed[0].length); + return type ? { kind: 'type', type } : { kind: 'unknown' }; + } + const typeChar = p.typeChar.exec(code); + if (typeChar) return { kind: 'type', type: { name: VB_TYPE_CHARS[typeChar[1]!]!, array: !!typeChar[2] } }; + // An assignment (`x = New Channel`) says nothing: the declared type + // (`Dim x As IYouTubeMediaContainer`) is what the call binds to. + const inferred = p.inferred.exec(code); + if (inferred) return valueBinding(code.slice(inferred.index + inferred[0].length)); + // `For Each c In controls.OfType(Of ButtonLabel)`: the loop's elements are + // that type; `For Each user In users`: the elements of what `users` is. + const loop = p.forEach.exec(code); + if (loop) { + const collection = loop[1]!.trim(); + const elements = /\.\s*(?:OfType|Cast)\s*\(\s*Of\s+/i.exec(collection); + const type = elements && /^[^()]*\)\s*(?:\(\s*\))?\s*$/.test(collection.slice(elements.index + elements[0].length)) + ? readType(collection, elements.index + elements[0].length) : null; + if (type) return { kind: 'type', type }; + return /^[A-Za-z_]\w*$/.test(collection) ? { kind: 'each', collection } : { kind: 'unknown' }; + } + return p.loose.test(code) ? { kind: 'unknown' } : undefined; +} + +/** What a local initialized with `expr` (`Dim x = expr`) is. */ +function valueBinding(expr: string): VbBinding { + const e = expr.split(/\s:(?!=)/)[0]!.trim(); + const unknown: VbBinding = { kind: 'unknown' }; + const created = /^New\s+/i.exec(e); + if (created) { + const type = readType(e, created[0].length); + return type ? { kind: 'type', type: { ...type, array: false } } : unknown; + } + const cast = /^(?:DirectCast|TryCast|CType)\s*\(/i.exec(e); + if (cast) { + const args = splitArgs(e, cast[0].length - 1); + const type = args?.length === 2 ? readType(args[1]!, 0) : null; + return type ? { kind: 'type', type } : unknown; + } + if (/^\$?""/.test(e)) return { kind: 'type', type: { name: 'String', array: false } }; + const conversion = /^(C[A-Za-z]+)\s*\(/.exec(e); + const converted = conversion ? VB_CONVERSIONS[conversion[1]!.toLowerCase()] : undefined; + if (converted) return { kind: 'type', type: { name: converted, array: false } }; + // `obj.Member(…)`, `Member(…)`, `Me.Member`: a call or a read as the whole value. + const call = /^(?:([A-Za-z_]\w*)\s*\.\s*)?([A-Za-z_]\w*)\s*/.exec(e); + if (call) { + let rest = e.slice(call[0].length); + if (rest.startsWith('(')) { + const close = closeParen(rest, 0); + rest = close < 0 ? 'unclosed' : rest.slice(close + 1).trim(); + } + if (rest === '') return { kind: 'call', receiver: call[1] ?? null, member: call[2]! }; + } + return unknown; +} + +/** The 0-based line a call's own declarations start on: its member's first line, which holds a method's parameters. */ +function scopeStartLine(ref: UnresolvedRef, context: ResolutionContext): number { + const from = context.getNodeById?.(ref.fromNodeId); + if (from && from.filePath === ref.filePath && from.startLine <= ref.line && from.endLine >= ref.line && + (from.kind === 'method' || from.kind === 'function' || from.kind === 'property' || from.kind === 'field')) { + return from.startLine - 1; + } + let start = -1; + for (const n of context.getNodesInFile(ref.filePath)) { + if (n.kind !== 'method' && n.kind !== 'function' && n.kind !== 'property') continue; + if (n.startLine <= ref.line && n.endLine >= ref.line && n.startLine - 1 > start) start = n.startLine - 1; + } + return start < 0 ? ref.line - 1 : start; +} + +const VB_WORD_LINES = new WeakMap>>(); + +/** + * The 0-based lines of a file's code each lowercased word appears on, in + * order: a call's receiver is looked for only on the lines that name it, + * not on every line back to its method's start (a long method's calls each + * rescanned it). + */ +function wordLines(file: string, context: ResolutionContext): Map { + let memo = VB_WORD_LINES.get(context); + if (!memo) VB_WORD_LINES.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const index = new Map(); + const words = /[A-Za-z_]\w*/g; + const lines = codeLines(file, context); + for (let i = 0; i < lines.length; i++) { + const code = lines[i]!.toLowerCase(); + words.lastIndex = 0; + for (let m = words.exec(code); m; m = words.exec(code)) { + let at = index.get(m[0]); + if (!at) index.set(m[0], (at = [])); + if (at[at.length - 1] !== i) at.push(i); + } + } + if (memo.size >= 256) memo.delete(memo.keys().next().value!); + memo.set(file, index); + return index; +} + +const VB_BINDINGS = new WeakMap>>(); + +/** + * Every statement of a file that binds `name`, by 0-based line — read once: + * staxrip's `GetArgs` methods call `sb.Append(…)` on hundreds of lines, each + * of which would otherwise walk back over all the others to the `Dim sb`. + */ +function bindingsOf(name: string, file: string, context: ResolutionContext): Array<{ line: number; binding: VbBinding }> { + let memo = VB_BINDINGS.get(context); + if (!memo) VB_BINDINGS.set(context, (memo = new Map())); + const key = `${file}|${name.toLowerCase()}`; + const hit = memo.get(key); + if (hit) return hit; + const lines = codeLines(file, context); + const found: Array<{ line: number; binding: VbBinding }> = []; + for (const i of wordLines(file, context).get(name.toLowerCase()) ?? []) { + const binding = bindingOn(lines[i]!, name); + if (binding) found.push({ line: i, binding }); + } + if (memo.size >= 65536) memo.delete(memo.keys().next().value!); + memo.set(key, found); + return found; +} + +/** The nearest statement above a call (in its member) that binds `name`, and its 1-based line. */ +function localBinding(name: string, ref: UnresolvedRef, context: ResolutionContext): { binding: VbBinding; line: number } | undefined { + const found = bindingsOf(name, ref.filePath, context); + // The last binding at or above the call's line. + let lo = 0; + let hi = found.length; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (found[mid]!.line <= ref.line - 1) lo = mid + 1; + else hi = mid; + } + const nearest = found[lo - 1]; + return nearest && nearest.line >= scopeStartLine(ref, context) ? { binding: nearest.binding, line: nearest.line + 1 } : undefined; +} + +const VB_FILE_TYPES = new WeakMap>(); + +/** The VB.NET types a line is written inside, innermost first. */ +function typesAround(file: string, line: number, context: ResolutionContext): Node[] { + let memo = VB_FILE_TYPES.get(context); + if (!memo) VB_FILE_TYPES.set(context, (memo = new Map())); + let types = memo.get(file); + if (!types) { + types = context.getNodesInFile(file) + .filter((n) => n.language === 'vbnet' && VB_TYPE_KINDS.has(n.kind)) + .sort((a, b) => b.startLine - a.startLine); + if (memo.size >= 1024) memo.delete(memo.keys().next().value!); + memo.set(file, types); + } + return types.filter((n) => n.startLine <= line && n.endLine >= line); +} + +/** The project's VB.NET types (classes, modules, structures, interfaces) named `name`, case aside. */ +function projectTypesNamed(name: string, context: ResolutionContext): Node[] { + return context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && VB_TYPE_KINDS.has(n.kind)); +} + +const VB_MEMBERS = new WeakMap>(); + +/** Members named `name` declared directly in `type` (any partial part of it), case aside. */ +function membersNamed(type: Node, name: string, context: ResolutionContext): Node[] { + let memo = VB_MEMBERS.get(context); + if (!memo) VB_MEMBERS.set(context, (memo = new Map())); + const qn = `${type.qualifiedName}::${name}`.toLowerCase(); + const hit = memo.get(qn); + if (hit) return hit; + const members = context.getNodesByLowerName(name.toLowerCase()).filter((n) => n.language === 'vbnet' && n.qualifiedName.toLowerCase() === qn); + if (memo.size >= 65536) memo.delete(memo.keys().next().value!); + memo.set(qn, members); + return members; +} + +const VB_MODULES = new WeakMap>(); + +/** Whether a VB.NET owner (by qualified name) is a `Module`, whose members are reached without a qualifier. */ +function isModule(ownerQn: string, context: ResolutionContext): boolean { + let memo = VB_MODULES.get(context); + if (!memo) VB_MODULES.set(context, (memo = new Map())); + const hit = memo.get(ownerQn); + if (hit !== undefined) return hit; + // Its own head line or the two after it (attributes may come first); the raw + // lines, as the whole file need not be read as code for this. + const module = context.getNodesByQualifiedName(ownerQn).some((n) => n.language === 'vbnet' && n.kind === 'class' && + (context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? []).slice(n.startLine - 1, n.startLine + 2) + .some((l) => /^\s*(?:<[^>]*>\s*)*(?:(?:Public|Friend|Private|Partial)\s+)*Module\s/i.test(l))); + memo.set(ownerQn, module); + return module; +} + +/** The members named `name` of the project's Modules, which code reaches without a qualifier — the caller's project's first. */ +function moduleMembersNamed(name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] { + const members = context.getNodesByLowerName(name.toLowerCase()).filter((n) => { + const cut = n.qualifiedName.lastIndexOf('::'); + return n.language === 'vbnet' && cut > 0 && isModule(n.qualifiedName.slice(0, cut), context); + }); + return preferVbProject(members, ref, context); +} + +const VB_SUPERS = new WeakMap>(); + +/** + * The types a VB.NET type's declarations (every `Partial` part) inherit or + * implement: the `Inherits` / `Implements` statements that open its body, + * including the `Class X : Inherits Y` form. + */ +function supertypesOf(type: Node, context: ResolutionContext): VbType[] { + let memo = VB_SUPERS.get(context); + if (!memo) VB_SUPERS.set(context, (memo = new Map())); + const key = type.qualifiedName.toLowerCase(); + const hit = memo.get(key); + if (hit) return hit; + const supers: VbType[] = []; + const parts = context.getNodesByQualifiedName(type.qualifiedName).filter((n) => n.language === 'vbnet' && VB_TYPE_KINDS.has(n.kind)); + for (const part of parts.length > 0 ? parts : [type]) { + const lines = codeLines(part.filePath, context); + scan: for (let i = part.startLine - 1; i < Math.min(lines.length, part.startLine + 12, part.endLine); i++) { + for (const statement of lines[i]!.split(/:(?!=)/)) { + const s = statement.trim(); + // Blank, an attribute, a directive, or the declaration's own head. + if (s === '' || /^<.*>$/.test(s) || s.startsWith('#') || /\b(?:Class|Structure|Interface|Module)\s+[A-Za-z_]/i.test(s) && !/^(?:Inherits|Implements)\b/i.test(s)) continue; + const clause = /^(Inherits|Implements)\s+(.+)$/i.exec(s); + if (!clause) break scan; + for (const sup of splitArgs(`(${clause[2]})`, 0) ?? []) { + const t = sited(readType(sup, 0), part.filePath, part.startLine); + if (t) supers.push(/^Implements$/i.test(clause[1]!) ? { ...t, implemented: true } : t); + } + } + } + } + memo.set(key, supers); + return supers; +} + +/** A type a VB.NET type inherits, with what its type parameters stand for there (`T` → `SiteSettings`). */ +interface VbAncestor { + node: Node; + args: Map; +} + +const VB_ANCESTRIES = new WeakMap>(); + +/** `t` with a type parameter replaced by what `args` says it stands for. */ +function substitute(t: VbType, args: ReadonlyMap): VbType { + return (!t.qualifier && !t.array && args.get(t.name.toLowerCase())) || t; +} + +/** + * A VB.NET type and the project types it inherits, nearest first: a class's + * base classes, an interface's base interfaces — whose members are the + * type's own. Each carries the type arguments `Inherits Checker(Of + * SiteSettings)` gives its parameters. An interface a class implements lends + * it no members: they are reached through the class's own `… Implements + * IFoo.Bar` ones. + */ +function ancestry(type: Node, context: ResolutionContext): VbAncestor[] { + let memo = VB_ANCESTRIES.get(context); + if (!memo) VB_ANCESTRIES.set(context, (memo = new Map())); + const hit = memo.get(type.id); + if (hit) return hit; + const out: VbAncestor[] = []; + const seen = new Set(); + const queue: VbAncestor[] = [{ node: type, args: new Map() }]; + while (queue.length > 0 && out.length < 16) { + const current = queue.shift()!; + const key = current.node.qualifiedName.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + out.push(current); + for (const sup of supertypesOf(current.node, context)) { + if (sup.implemented) continue; + const found = typesNamedAt(sup, context); + if (found.ambiguous) continue; + const given = (sup.args ?? []).map((a) => substitute(a, current.args)); + for (const base of found.owners) { + const params = [...typeParametersOf(base, context).keys()]; + queue.push({ node: base, args: new Map(params.flatMap((p, i): Array<[string, VbType]> => (given[i] ? [[p, given[i]!]] : []))) }); + } + } + } + memo.set(type.id, out); + return out; +} + +/** A VB.NET type and the project types it inherits, nearest first (see ancestry). */ +function hierarchy(type: Node, context: ResolutionContext): Node[] { + return ancestry(type, context).map((a) => a.node); +} + +const VB_PROJECTS = new WeakMap>(); + +/** The directory of the nearest `.vbproj` at or above `dir` (`''` for the root), or null outside any project. */ +function projectOfDir(dir: string, context: ResolutionContext, memo: Map): string | null { + const hit = memo.get(dir); + if (hit !== undefined) return hit; + let found: string | null = null; + try { + if (fs.readdirSync(path.join(context.getProjectRoot(), dir)).some((e) => /\.vbproj$/i.test(e))) found = dir; + } catch { + // Unreadable: no project file here. + } + if (found === null && dir !== '') { + const cut = dir.lastIndexOf('/'); + found = projectOfDir(cut < 0 ? '' : dir.slice(0, cut), context, memo); + } + memo.set(dir, found); + return found; +} + +/** The project (the directory of the nearest `.vbproj` above it) a file belongs to, or null. */ +function projectOf(file: string, context: ResolutionContext): string | null { + let memo = VB_PROJECTS.get(context); + if (!memo) VB_PROJECTS.set(context, (memo = new Map())); + const cut = file.lastIndexOf('/'); + return projectOfDir(cut < 0 ? '' : file.slice(0, cut), context, memo); +} + +/** Whether two files are in the same VB.NET project — false when either is in none. */ +export function sameVbProject(a: string, b: string, context: ResolutionContext): boolean { + const project = projectOf(a, context); + return project !== null && project === projectOf(b, context); +} + +interface VbProjectInfo { + /** The root namespace every file of the project declares its namespaces inside, as segments. */ + root: string[]; + /** The namespaces the project imports into every file (``). */ + imports: string[]; +} + +const VB_PROJECT_INFO = new WeakMap>(); + +/** A file's project's root namespace (``, else the project's name) and project-wide imports, lowercased. */ +function projectInfo(file: string, context: ResolutionContext): VbProjectInfo { + const dir = projectOf(file, context); + if (dir === null) return { root: [], imports: [] }; + let memo = VB_PROJECT_INFO.get(context); + if (!memo) VB_PROJECT_INFO.set(context, (memo = new Map())); + const hit = memo.get(dir); + if (hit) return hit; + const info: VbProjectInfo = { root: [], imports: [] }; + try { + const abs = path.join(context.getProjectRoot(), dir); + const project = fs.readdirSync(abs).find((e) => /\.vbproj$/i.test(e)); + if (project) { + const text = fs.readFileSync(path.join(abs, project), 'utf8'); + const root = /\s*([\w.]*)\s*<\/RootNamespace>/i.exec(text)?.[1] ?? project.replace(/\.vbproj$/i, ''); + info.root = root.toLowerCase().split('.').filter((s) => s !== ''); + for (const m of text.matchAll(/ s.split('.'))]; +} + +/** A file's lines before its first declaration: where its `Option` and `Imports` statements are. */ +function headerLines(file: string, context: ResolutionContext): string[] { + const lines = codeLines(file, context); + const end = lines.findIndex((l) => + /^\s*(?:Namespace|Module|Class|Structure|Interface|Enum|Delegate|Public|Friend|Private|Protected|Partial|NotInheritable|MustInherit)\b/i.test(l)); + return end < 0 ? lines : lines.slice(0, end); +} + +const VB_FILE_IMPORTS = new WeakMap>(); + +/** The namespaces a file imports — its own `Imports` and its project's — lowercased. */ +function importedNamespaces(file: string, context: ResolutionContext): string[] { + let memo = VB_FILE_IMPORTS.get(context); + if (!memo) VB_FILE_IMPORTS.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const imports = [...projectInfo(file, context).imports]; + for (const line of headerLines(file, context)) { + const m = /^\s*Imports\s+([\w.]+)\s*$/i.exec(line); + if (m) imports.push(m[1]!.toLowerCase().replace(/^global\./, '')); + } + memo.set(file, imports); + return imports; +} + +const VB_ALIASES = new WeakMap>>(); + +/** A file's import aliases: `Imports TDJob = SCrawler.DownloadObjects.TDownloader.Job`, by lowercased alias. */ +function importAliases(file: string, context: ResolutionContext): Map { + let memo = VB_ALIASES.get(context); + if (!memo) VB_ALIASES.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const aliases = new Map(); + for (const line of headerLines(file, context)) { + const m = /^\s*Imports\s+([A-Za-z_]\w*)\s*=\s*(.+)$/i.exec(line); + const target = m ? readType(m[2]!, 0) : null; + if (target) aliases.set(m![1]!.toLowerCase(), target); + } + memo.set(file, aliases); + return aliases; +} + +/** `t` with an import alias it is written with — as itself or as its first qualifier — spelled out. */ +function unalias(t: VbType, file: string, context: ResolutionContext): VbType { + const aliases = importAliases(file, context); + if (aliases.size === 0) return t; + if (!t.qualifier) { + const target = aliases.get(t.name.toLowerCase()); + return target ? { ...t, name: target.name, ...(target.qualifier ? { qualifier: target.qualifier } : {}) } : t; + } + const target = aliases.get(t.qualifier[0]!); + return target ? { ...t, qualifier: [...(target.qualifier ?? []), target.name.toLowerCase(), ...t.qualifier.slice(1)] } : t; +} + +/** Whether `tail` is the end of `full`. */ +function endsWith(full: string[], tail: string[]): boolean { + return tail.length <= full.length && tail.every((s, i) => full[full.length - tail.length + i] === s); +} + +const VB_TYPE_LIKE_KINDS: ReadonlySet = new Set(['class', 'struct', 'interface', 'enum', 'type_alias']); + +/** The class, structure or interface (not a `Module`) a VB.NET type is nested in, or null. */ +function enclosingType(n: Node, context: ResolutionContext): Node | null { + const cut = n.qualifiedName.lastIndexOf('::'); + if (cut < 0) return null; + const parentQn = n.qualifiedName.slice(0, cut); + const parent = context.getNodesByQualifiedName(parentQn).find((p) => p.language === 'vbnet' && VB_TYPE_KINDS.has(p.kind)); + return parent && !isModule(parentQn, context) ? parent : null; +} + +function isNestedInType(n: Node, context: ResolutionContext): boolean { + return enclosingType(n, context) !== null; +} + +const VB_BASE_NAMES = new WeakMap>>(); + +/** + * The lowercased names of the types around a line and of the classes they + * inherit, walked by name (a few levels): the types whose nested types code + * there names bare — `Dim ret As New MenuList` in a `VideoEncoder` subclass. + */ +function baseTypeNamesAround(file: string, line: number, context: ResolutionContext): Set { + const around = typesAround(file, line, context); + let memo = VB_BASE_NAMES.get(context); + if (!memo) VB_BASE_NAMES.set(context, (memo = new Map())); + const key = around.map((t) => t.id).join('|'); + const hit = memo.get(key); + if (hit) return hit; + const names = new Set(); + let frontier = around; + for (let depth = 0; depth < 5 && frontier.length > 0; depth++) { + const next: Node[] = []; + for (const t of frontier) { + if (names.has(t.name.toLowerCase())) continue; + names.add(t.name.toLowerCase()); + for (const sup of supertypesOf(t, context)) if (!sup.implemented) next.push(...projectTypesNamed(sup.name, context)); + } + frontier = next; + } + memo.set(key, names); + return names; +} + +/** + * Whether a type is one a name written with this qualifier can mean: its + * namespaces and outer types end with what is written before the name. + * Designer code's `New System.Drawing.Point(3, 3)` is not staxrip's nested + * `ButtonEx.SymbolDrawer.Point`; `New API.Base.UserDataBase(…)` is that one. + */ +export function isVbTypeQualifiedBy(n: Node, qualifier: string, file: string, context: ResolutionContext): boolean { + if (n.language !== 'vbnet' || !VB_TYPE_LIKE_KINDS.has(n.kind)) return true; + const written = readType(`${qualifier}.${n.name}`, 0); + if (!written?.qualifier) return true; + const t = unalias(written, file, context); + return !t.qualifier || endsWith(fullSegments(n, context).slice(0, -1), t.qualifier); +} + +/** + * The project types a type name written at a site means, as VB.NET looks it + * up: one its written qualifier names, nested in or declared in the namespace + * of a type around the site — the nearest first — else nested in a class + * those inherit, else in a namespace the file or project imports, else in + * the site's own project (a class's nested type only where it is in scope). + * SCrawler declares + * a `SiteSettings` in each site's namespace (`API.Pinterest`, `API.Bluesky`, + * …); a member typed `SiteSettings` in `API.Pinterest.UserData` is + * Pinterest's. `ambiguous` when what is left are different types. + */ +function typesNamedAt(written: VbType, context: ResolutionContext): { owners: Node[]; ambiguous: boolean } { + const t = written.file ? unalias(written, written.file, context) : written; + let candidates = projectTypesNamed(t.name, context); + // `System.Drawing.Color` is not the project's `Color`; `API.Base.UserDataBase` is that one. + if (t.qualifier) candidates = candidates.filter((c) => endsWith(fullSegments(c, context).slice(0, -1), t.qualifier!)); + if (candidates.length === 0 || !t.file) return { owners: candidates, ambiguous: false }; + const file = t.file; + const site = typesAround(file, t.line ?? 0, context)[0]; + const sitePath = site ? fullSegments(site, context) : projectInfo(file, context).root; + let tier: Node[] = []; + let best = -1; + for (const c of candidates) { + const container = fullSegments(c, context).slice(0, -1); + if (container.length > sitePath.length || container.some((s, i) => s !== sitePath[i])) continue; + if (container.length > best) { + best = container.length; + tier = [c]; + } else if (container.length === best) tier.push(c); + } + // A type nested in a class the site's types inherit. + if (tier.length === 0 && !t.qualifier) { + const bases = baseTypeNamesAround(file, t.line ?? 0, context); + tier = candidates.filter((c) => bases.has(enclosingType(c, context)?.name.toLowerCase() ?? '')); + } + if (tier.length === 0) { + const imports = importedNamespaces(file, context); + tier = candidates.filter((c) => imports.includes(fullSegments(c, context).slice(0, -1).join('.'))); + } + // Else any the project declares in a namespace (the root namespaces and + // project imports this cannot see) — but not a class's nested type, which + // is named bare only inside it or a class deriving from it. + if (tier.length === 0) tier = t.qualifier ? candidates : candidates.filter((c) => !isNestedInType(c, context)); + // A project's own declaration over another project's of the same name + // (staxrip's main app and its AutoCrop tool each declare a `ColorHSL`). + const own = tier.filter((c) => sameVbProject(c.filePath, file, context)); + if (own.length > 0) tier = own; + // One type's partial parts are one type. + const distinct = new Set(tier.map((c) => `${projectOf(c.filePath, context)}|${c.qualifiedName.toLowerCase()}`)); + return { owners: [...tier.filter((c) => c.filePath === file), ...tier.filter((c) => c.filePath !== file)], ambiguous: distinct.size > 1 }; +} + +/** The call site's own file first, then its project's, the rest after, each in its given order. */ +export function preferVbProject(nodes: Node[], ref: UnresolvedRef, context: ResolutionContext): Node[] { + if (nodes.length < 2) return nodes; + const file: Node[] = []; + const project: Node[] = []; + const rest: Node[] = []; + for (const n of nodes) { + if (n.filePath === ref.filePath) file.push(n); + else if (sameVbProject(n.filePath, ref.filePath, context)) project.push(n); + else rest.push(n); + } + return [...file, ...project, ...rest]; +} + +/** How many leading directories two files share. */ +function sharedDirs(a: string, b: string): number { + const da = a.split('/').slice(0, -1); + const db = b.split('/').slice(0, -1); + let i = 0; + while (i < da.length && i < db.length && da[i] === db[i]) i++; + return i; +} + +/** + * The one of several equally good guesses a call means: the one in its own + * file, else in its own project, else in the nearest directory — or null + * when none of these tells them apart. + */ +export function breakVbTie(tied: Node[], ref: UnresolvedRef, context: ResolutionContext): Node | null { + // One type's overloads (and partial parts) are one guess, not a tie: the first stands for them. + const seen = new Set(); + const distinct = tied.filter((n) => { + const key = `${projectOf(n.filePath, context)}|${n.qualifiedName.toLowerCase()}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + if (distinct.length === 1) return distinct[0]!; + let pool = distinct.filter((n) => n.filePath === ref.filePath); + if (pool.length === 0) { + const own = distinct.filter((n) => sameVbProject(n.filePath, ref.filePath, context)); + pool = own.length > 0 ? own : distinct; + } + if (pool.length === 1) return pool[0]!; + let best = -1; + let winners: Node[] = []; + for (const n of pool) { + const shared = sharedDirs(ref.filePath, n.filePath); + if (shared > best) { + best = shared; + winners = [n]; + } else if (shared === best) winners.push(n); + } + return winners.length === 1 ? winners[0]! : null; +} + +/** The type a field or property declares itself with, read from its declaration. */ +function memberDeclaredType(member: Node, context: ResolutionContext): VbType | null { + const code = codeLines(member.filePath, context).slice(member.startLine - 1, member.startLine + 2).join(' '); + const r = escapeRegex(member.name); + const property = new RegExp(`\\bProperty\\s+${r}\\b\\s*`, 'i').exec(code); + if (property) { + let at = property.index + property[0].length; + if (code[at] === '(') { + const close = closeParen(code, at); + if (close < 0) return null; + at = close + 1; + } + const as = /^\s*As\s+(New\s+)?/i.exec(code.slice(at)); + const type = as ? readType(code, at + as[0].length) : null; + return type && as?.[1] ? { ...type, array: false } : type; + } + const binding = bindingOn(code, member.name); + if (binding?.kind === 'type') return binding.type; + // `Private x = New Foo()`: a field's initializer is what it holds. + const init = new RegExp(`(? = new Map()): VbType | null { + const type = sited(member.kind === 'method' ? declaredReturnType(member, context) : memberDeclaredType(member, context), + member.filePath, member.startLine); + const given = type ? substitute(type, args) : null; + return given !== type ? given : resolveTypeParameter(type, member, context); +} + +/** + * The type of what a receiver-less `name` reads where a call is: a member of + * a class around the call or of one it inherits, else of a `Module`. Null when + * it declares no type the source shows, undefined when nothing has the name. + */ +function memberType(name: string, ref: UnresolvedRef, context: ResolutionContext): VbType | null | undefined { + const fits = (n: Node) => VB_VALUE_KINDS.has(n.kind) || n.kind === 'method'; + for (const around of typesAround(ref.filePath, ref.line, context)) { + for (const { node, args } of ancestry(around, context)) { + const member = membersNamed(node, name, context).find(fits); + if (member) return valueMemberType(member, context, args); + } + } + const global = moduleMembersNamed(name, ref, context).find(fits); + return global ? valueMemberType(global, context) : undefined; +} + +/** The type a method, function or property returns, read from its declaration; null for a `Sub` or none written. */ +function declaredReturnType(n: Node, context: ResolutionContext): VbType | null { + const code = codeLines(n.filePath, context).slice(n.startLine - 1, n.startLine + 6).join(' '); + const head = new RegExp(`\\b(Function|Property|Sub)\\s+${escapeRegex(n.name)}\\b`, 'i').exec(code); + if (!head || /^Sub$/i.test(head[1]!)) return null; + let at = head.index + head[0].length; + // `(Of T)`, then the parameter list. + for (let group = 0; group < 2; group++) { + const open = /^\s*\(/.exec(code.slice(at)); + if (!open) break; + const close = closeParen(code, at + open[0].length - 1); + if (close < 0) return null; + at = close + 1; + } + const as = /^\s*As\s+/i.exec(code.slice(at)); + return as ? readType(code, at + as[0].length) : null; +} + +const VB_TYPE_PARAMS = new WeakMap>>(); + +/** The type parameters a declaration's head names (`Class Repo(Of T As Entity)`), each with its constraint or null. */ +function typeParametersOf(n: Node, context: ResolutionContext): Map { + let memo = VB_TYPE_PARAMS.get(context); + if (!memo) VB_TYPE_PARAMS.set(context, (memo = new Map())); + const hit = memo.get(n.id); + if (hit) return hit; + const params = readTypeParameters(n, context); + memo.set(n.id, params); + return params; +} + +function readTypeParameters(n: Node, context: ResolutionContext): Map { + const params = new Map(); + const code = codeLines(n.filePath, context).slice(n.startLine - 1, n.startLine + 2).join(' '); + const head = new RegExp(`\\b(?:Class|Structure|Interface|Module|Function|Sub)\\s+${escapeRegex(n.name)}\\s*(?=\\(\\s*Of\\b)`, 'i').exec(code); + if (!head) return params; + for (const p of splitArgs(code, head.index + head[0].length) ?? []) { + const m = /^(?:Of\s+)?(?:In\s+|Out\s+)?([A-Za-z_]\w*)(?:\s+As\s+(.+))?$/i.exec(p); + if (!m) continue; + const constraints = (m[2] ?? '').replace(/^\{|\}$/g, '').split(',').map((c) => c.trim()) + .filter((c) => c !== '' && !/^(?:New|Class|Structure)$/i.test(c)); + params.set(m[1]!.toLowerCase(), constraints.length > 0 ? sited(readType(constraints[0]!, 0), n.filePath, n.startLine) : null); + } + return params; +} + +/** + * What a type means where it is written: a type parameter of the method + * (`owner`) or of a type around it stands for its constraint — null when it + * has none, as its value's type is then unknown — and any other name for itself. + */ +function resolveTypeParameter(t: VbType | null, owner: Node | null, context: ResolutionContext): VbType | null { + if (!t || t.array || isBuiltin(t) || t.qualifier || !t.file) return t; + const key = t.name.toLowerCase(); + for (const decl of [...(owner && owner.kind === 'method' ? [owner] : []), ...typesAround(t.file, t.line ?? 0, context)]) { + const params = typeParametersOf(decl, context); + if (params.has(key)) return params.get(key) ?? null; + } + return t; +} + +/** + * The project types `t` names where it is written, null when the name leaves + * different types (a guess between them would be no better than none), or + * none for an array or a built-in type. + */ +function ownersOf(t: VbType, context: ResolutionContext): Node[] | null { + if (t.array || isBuiltin(t)) return []; + const found = typesNamedAt(t, context); + return found.ambiguous ? null : found.owners; +} + +/** + * A method — or, `withValues`, a field or property — named `name` on one of + * `owners` (the type `typed` names) or a project type they inherit, with what + * the type parameters of the type declaring it stand for. + */ +function memberOn( + owners: Node[], + name: string, + ref: UnresolvedRef, + context: ResolutionContext, + withValues: boolean, + typed?: VbType, +): { node: Node; args: Map } | null { + const fits = (n: Node) => n.kind === 'method' || (withValues && VB_VALUE_KINDS.has(n.kind)); + for (const level of [0, 1]) { + for (const owner of preferVbProject(owners, ref, context)) { + // What the owner's own parameters are, as the receiver's type writes them (`Repo(Of Foo)`). + const given = new Map([...typeParametersOf(owner, context).keys()].flatMap((p, i): Array<[string, VbType]> => + (typed?.args?.[i] ? [[p, typed.args[i]!]] : []))); + const ancestors = ancestry(owner, context); + for (const { node, args } of level === 0 ? ancestors.slice(0, 1) : ancestors.slice(1)) { + const found = preferVbProject(membersNamed(node, name, context).filter(fits), ref, context)[0]; + if (found) return { node: found, args: level === 0 ? given : new Map([...args].map(([p, t]) => [p, substitute(t, given)])) }; + } + } + } + return null; +} + +/** The type of the value `Member(…)` / `obj.Member(…)` gives, from the member's declaration. */ +function callResultType( + call: { receiver: string | null; member: string }, + ref: UnresolvedRef, + context: ResolutionContext, + depth: number, +): VbType | null { + let member: { node: Node; args: Map } | null = null; + if (call.receiver === null || /^(?:Me|MyClass|MyBase)$/i.test(call.receiver)) { + const owners = typesAround(ref.filePath, ref.line, context).slice(0, 1); + member = owners.length > 0 ? memberOn(owners, call.member, ref, context, true) : null; + if (!member && call.receiver === null) { + const global = moduleMembersNamed(call.member, ref, context).find((n) => n.kind === 'method' || VB_VALUE_KINDS.has(n.kind)); + member = global ? { node: global, args: new Map() } : null; + } + } else { + const type = receiverType(call.receiver, ref, context, depth + 1); + // Not a variable: a type or module named for a shared call (`ObjectStorage.Load()`). + const owners = type === undefined ? ownersOf({ name: call.receiver, array: false, file: ref.filePath, line: ref.line }, context) + : type ? ownersOf(type, context) : null; + member = owners && owners.length > 0 ? memberOn(owners, call.member, ref, context, true, type ?? undefined) : null; + } + return member ? valueMemberType(member.node, context, member.args) : null; +} + +/** + * What a receiver named `name` is declared as where the call is: a local or + * parameter of its member, else a field or property of its class, of one the + * class inherits, or of a `Module`. Null when the receiver is bound but its + * type isn't known; undefined when nothing binds the name (a type or module). + */ +function receiverType(name: string, ref: UnresolvedRef, context: ResolutionContext, depth: number): VbType | null | undefined { + if (depth > 2) return null; + const local = localBinding(name, ref, context); + if (local) { + const { binding, line } = local; + const owner = context.getNodeById?.(ref.fromNodeId) ?? null; + if (binding.kind === 'type') return resolveTypeParameter(sited(binding.type, ref.filePath, line), owner, context); + if (binding.kind === 'call') return callResultType(binding, { ...ref, line }, context, depth); + if (binding.kind === 'each') { + const collection = receiverType(binding.collection, { ...ref, line }, context, depth + 1); + return collection ? elementType(collection) : null; + } + return null; + } + return memberType(name, ref, context); +} + +interface VbExtension { + node: Node; + /** The type it extends; null when that is one of its own type parameters (it extends anything). */ + param: VbType | null; +} + +const VB_EXTENSIONS = new WeakMap>(); + +/** A method's extension-method declaration (` Function F(s As String, …)`), or null when it isn't one. */ +function extensionOf(n: Node, context: ResolutionContext): VbExtension | null { + if (n.kind !== 'method' || n.language !== 'vbnet') return null; + let memo = VB_EXTENSIONS.get(context); + if (!memo) VB_EXTENSIONS.set(context, (memo = new Map())); + const hit = memo.get(n.id); + if (hit !== undefined) return hit; + const extension = readExtension(n, context); + memo.set(n.id, extension); + return extension; +} + +function readExtension(n: Node, context: ResolutionContext): VbExtension | null { + const code = codeLines(n.filePath, context).slice(Math.max(0, n.startLine - 2), n.startLine + 6).join(' '); + const decl = new RegExp(`\\b(?:Function|Sub)\\s+${escapeRegex(n.name)}\\b`, 'i').exec(code); + if (!decl) return null; + const attributes = /((?:<[^<>]*>\s*)+)(?:(?:Public|Friend|Private|Protected|Shared|Overloads|Async|Iterator)\s+)*$/i + .exec(code.slice(0, decl.index))?.[1]; + if (!attributes || !/\bExtension(?:Attribute)?\b/i.test(attributes)) return null; + let at = decl.index + decl[0].length; + const typeParams = new Set(); + if (/^\s*\(\s*Of\b/i.test(code.slice(at))) { + const open = code.indexOf('(', at); + for (const p of splitArgs(code, open) ?? []) { + const tp = /^(?:Of\s+)?(?:In\s+|Out\s+)?([A-Za-z_]\w*)/i.exec(p); + if (tp) typeParams.add(tp[1]!.toLowerCase()); + } + const close = closeParen(code, open); + if (close < 0) return null; + at = close + 1; + } + const open = /^\s*\(/.exec(code.slice(at)); + const first = open ? splitArgs(code, at + open[0].length - 1)?.[0]?.replace(/<[^<>]*>/g, '') : undefined; + const as = first ? /\bAs\s+/i.exec(first) : null; + const type = as ? readType(first!, as.index + as[0].length) : null; + if (!type) return null; + return { node: n, param: typeParams.has(type.name.toLowerCase()) && !type.array ? null : type }; +} + +/** `Object`'s instance methods, which every type has. */ +const OBJECT_METHODS = ['tostring', 'equals', 'gethashcode', 'gettype']; + +/** + * The instance methods of .NET's everyday types: a call one of them answers + * is the type's own, whatever extension of that name the project declares + * (`list.Sort()` on a `List(Of T)`), and a name one of them lacks is no + * instance method there (`list.Join(", ")` is an extension's). + */ +const BCL_INSTANCE_METHODS: ReadonlyMap> = new Map(Object.entries({ + string: ['clone', 'compareto', 'contains', 'copyto', 'endswith', 'getenumerator', 'gettypecode', 'indexof', 'indexofany', + 'insert', 'isnormalized', 'lastindexof', 'lastindexofany', 'normalize', 'padleft', 'padright', 'remove', 'replace', 'split', + 'startswith', 'substring', 'tochararray', 'tolower', 'tolowerinvariant', 'toupper', 'toupperinvariant', 'trim', 'trimend', + 'trimstart'], + stringbuilder: ['append', 'appendformat', 'appendjoin', 'appendline', 'clear', 'copyto', 'ensurecapacity', 'getchunks', 'insert', + 'remove', 'replace'], + list: ['add', 'addrange', 'asreadonly', 'binarysearch', 'clear', 'contains', 'convertall', 'copyto', 'exists', 'find', 'findall', + 'findindex', 'findlast', 'findlastindex', 'foreach', 'getenumerator', 'getrange', 'indexof', 'insert', 'insertrange', + 'lastindexof', 'remove', 'removeall', 'removeat', 'removerange', 'reverse', 'sort', 'toarray', 'trimexcess', 'trueforall'], + dictionary: ['add', 'clear', 'containskey', 'containsvalue', 'ensurecapacity', 'getenumerator', 'remove', 'trimexcess', 'tryadd', + 'trygetvalue'], + hashset: ['add', 'clear', 'contains', 'copyto', 'exceptwith', 'getenumerator', 'intersectwith', 'ispropersubsetof', + 'ispropersupersetof', 'issubsetof', 'issupersetof', 'overlaps', 'remove', 'removewhere', 'setequals', 'symmetricexceptwith', + 'trimexcess', 'trygetvalue', 'unionwith'], + '[]': ['clone', 'copyto', 'getenumerator', 'getlength', 'getlonglength', 'getlowerbound', 'getupperbound', 'getvalue', + 'initialize', 'setvalue'], +}).map(([type, methods]) => [type, new Set([...methods, ...OBJECT_METHODS])])); + +/** The instance methods of a .NET type the table above knows; undefined for any other. */ +function bclInstanceMethods(t: VbType): ReadonlySet | undefined { + return BCL_INSTANCE_METHODS.get(t.array ? '[]' : typeKey(t)); +} + +/** + * Whether a value of type `t` might reach an extension declared for `param` + * through a conversion nothing here can check: `Object`, an array's + * collection interfaces, an interface a built-in type implements, or any + * type an outside base type may lead to. + */ +function mayExtend(t: VbType, open: boolean, param: VbType): boolean { + if (typeKey(param) === 'object') return true; + if (param.array !== t.array) { + return t.array && /^(?:IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection|Array)$/i.test(param.name); + } + if (isBuiltin(param)) return false; + if (isBuiltin(t)) return /^I[A-Z]/.test(param.name); + return open; +} + +/** + * The extension method a call on a value of type `t` reaches: one declared + * for `t` or for a type it inherits — else, when exactly one other extension + * of that name could apply and the name is no instance method of `t`'s + * (one of .NET's own names, for a type nothing here describes), that one. + */ +function extensionFor( + t: VbType, + owners: Node[], + method: string, + ref: UnresolvedRef, + context: ResolutionContext, + isStdMethod: (name: string) => boolean, +): ResolvedRef | null { + const instanceMethods = owners.length === 0 ? bclInstanceMethods(t) : undefined; + if (instanceMethods?.has(method.toLowerCase())) return null; + const extensions = context.getNodesByLowerName(method.toLowerCase()) + .map((n) => extensionOf(n, context)) + .filter((e): e is VbExtension => e !== null); + if (extensions.length === 0) return null; + // Every name the type goes by — its own, its base types', the interfaces + // they implement — and whether one of them is an outside type, whose own + // ancestry nothing here shows. + const names = new Set([typeKey({ name: t.name, array: false })]); + let open = owners.length === 0 && !isBuiltin(t) && !t.array; + for (const owner of owners) { + for (const type of hierarchy(owner, context)) { + names.add(type.name.toLowerCase()); + for (const sup of supertypesOf(type, context)) { + names.add(typeKey(sup)); + const found = typesNamedAt(sup, context).owners; + if (found.length === 0) open = true; + else if (sup.implemented) for (const i of found) for (const h of hierarchy(i, context)) names.add(h.name.toLowerCase()); + } + } + } + const exact = extensions.filter((e) => e.param !== null && + (typeKey(e.param) === typeKey(t) || (!t.array && !e.param.array && names.has(typeKey(e.param))))); + if (exact.length > 0) { + const target = preferVbProject(exact.map((e) => e.node), ref, context)[0]!; + return { original: ref, targetNodeId: target.id, confidence: 0.85, resolvedBy: 'instance-method' }; + } + const loose = extensions.filter((e) => e.param === null || mayExtend(t, open, e.param)); + if (loose.length === 1 && (instanceMethods !== undefined || !isStdMethod(method))) { + return { original: ref, targetNodeId: loose[0]!.node.id, confidence: 0.7, resolvedBy: 'instance-method' }; + } + return null; +} + +/** + * Resolve `receiver.method()` through the receiver's declared type: the + * type's own method or one it inherits, else an extension method for it, + * else null — a typed receiver is never a guess. A receiver naming one of the + * project's types or modules is a shared call on it. Undefined when the type + * isn't known, or is `Object` (a late-bound call), for the name strategies. + */ +export function matchVbTypedCall( + receiver: string, + method: string, + ref: UnresolvedRef, + context: ResolutionContext, + isStdMethod: (name: string) => boolean, +): ResolvedRef | null | undefined { + if (!/^[A-Za-z_]\w*$/.test(receiver)) return undefined; + const type = receiverType(receiver, ref, context, 0); + if (type === undefined) { + // A type or module named for a shared call — `M3U8.Download(…)` in SCrawler's + // `API.Reddit.UserData` is `API.Reddit.M3U8`'s, not another site's: its own + // member or one it inherits. + const named = typesNamedAt({ name: receiver, array: false, file: ref.filePath, line: ref.line }, context); + if (named.owners.length === 0) return undefined; + if (named.ambiguous) return null; + const shared = memberOn(named.owners, method, ref, context, false); + return shared ? { original: ref, targetNodeId: shared.node.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null; + } + if (!type || typeKey(type) === 'object') return undefined; + const owners = ownersOf(type, context); + // A type name that leaves two of the project's types: no guess between them. + if (owners === null) return null; + if (owners.length > 0) { + const own = memberOn(owners, method, ref, context, false, type); + if (own) return { original: ref, targetNodeId: own.node.id, confidence: 0.9, resolvedBy: 'instance-method' }; + } + return extensionFor(type, owners, method, ref, context, isStdMethod); +} + +const VB_TYPE_IMPORTS = new WeakMap>>(); + +/** The last names of a file's `Imports` (a type imported this way lends its shared members; an alias lends none). */ +function importedNames(file: string, context: ResolutionContext): Set { + let memo = VB_TYPE_IMPORTS.get(context); + if (!memo) VB_TYPE_IMPORTS.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const names = new Set(); + for (const line of headerLines(file, context)) { + const m = /^\s*Imports\s+([\w.]+)\s*$/i.exec(line); + if (m) names.add(m[1]!.split('.').pop()!.toLowerCase()); + } + memo.set(file, names); + return names; +} + +const VB_SCOPES = new WeakMap | null>>(); + +/** The lowercased names of the types around a call and of the project types they inherit; null outside any type. */ +function scopeOwners(ref: UnresolvedRef, context: ResolutionContext): Set | null { + let memo = VB_SCOPES.get(context); + if (!memo) VB_SCOPES.set(context, (memo = new WeakMap())); + const hit = memo.get(ref); + if (hit !== undefined) return hit; + const around = typesAround(ref.filePath, ref.line, context); + const owners = around.length === 0 ? null + : new Set(around.flatMap((t) => hierarchy(t, context)).map((t) => t.name.toLowerCase())); + memo.set(ref, owners); + return owners; +} + +/** + * Whether an unqualified VB.NET type name can mean `n`: a type nested in a + * class, structure or interface is named bare only inside it or a type + * deriving from it — designer code's `New Point(4, 285)` is not staxrip's + * nested `ButtonEx.SymbolDrawer.Point`. A `Module`'s types belong to its + * namespace; a file can import the outer type, or alias the nested one + * (`Imports UserMediaD = SCrawler.DownloadObjects.TDownloader.UserMediaD`). + * Not judged when no type is around the name. + */ +export function isVbNestedTypeInScope(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { + if (n.language !== 'vbnet' || !VB_TYPE_LIKE_KINDS.has(n.kind)) return true; + const parent = enclosingType(n, context); + if (!parent) return true; + const owners = scopeOwners(ref, context); + if (owners === null || owners.has(parent.name.toLowerCase())) return true; + const alias = importAliases(ref.filePath, context).get(ref.referenceName.toLowerCase()); + if (alias && alias.name.toLowerCase() === n.name.toLowerCase() && + (!alias.qualifier || endsWith(fullSegments(n, context).slice(0, -1), alias.qualifier))) return true; + return importedNames(ref.filePath, context).has(parent.name.toLowerCase()); +} + +/** + * Whether a receiver-less VB.NET call (or one through `Me` / `MyClass` / + * `MyBase`) can mean member `n`: one of a type around the call or of a type + * those inherit, of a `Module`, or of a type the file imports. Not judged + * when no type is around the call, or `n` is not a member of a type. + */ +export function isVbMemberInScope(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { + if (n.language !== 'vbnet' || !VB_MEMBER_KINDS.has(n.kind)) return true; + const cut = n.qualifiedName.lastIndexOf('::'); + if (cut < 0) return true; + const owners = scopeOwners(ref, context); + if (owners === null) return true; + const ownerQn = n.qualifiedName.slice(0, cut); + const owner = ownerQn.split(/::|\./).pop()!.toLowerCase(); + return owners.has(owner) || isModule(ownerQn, context) || importedNames(ref.filePath, context).has(owner); +} + +/** + * Drop this module's per-context memos — file lines and what was read from + * them — with the resolver's own caches (ReferenceResolver.clearCaches), so a + * sync never reads a changed file's old declarations. + */ +export function clearVbnetReceiverMemos(context: ResolutionContext): void { + for (const memo of [VB_CODE_LINES, VB_WORD_LINES, VB_BINDINGS, VB_FILE_TYPES, VB_MEMBERS, VB_MODULES, VB_SUPERS, VB_ANCESTRIES, + VB_PROJECTS, VB_PROJECT_INFO, VB_FILE_IMPORTS, VB_ALIASES, VB_BASE_NAMES, VB_TYPE_PARAMS, VB_EXTENSIONS, VB_TYPE_IMPORTS, + VB_SCOPES] as Array>) { + memo.delete(context); + } +}