mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
fix(vbnet): link reads through a namespace or a bare member name, and run the parameterless overload (#2762)
Three kinds of VB.NET member read that #2355 and #2377 left unlinked: - A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`, a constructor's `Me.New` / `MyBase.New`) went to the first overload declared, so `Me.Load` reached `Load(path)` instead of `Load(Optional force = False)`, and a `Me.New` whose first overload was the calling constructor itself linked nothing. - A read whose first name is a namespace (`Billing.Invoice.MaxLines`, `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front, a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was looked up as a value or a type only. - A field, property or Module variable named bare before the dot (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was not counted as read: a bare name is no member access, so only what is read through it could link, and nothing when that is .NET's (`Text`). Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output unchanged): - A method a reference runs without parentheses resolves among its type's overloads to one that takes no arguments (none, or only Optional and ParamArray parameters, read from the declaration), never the member the reference is in. AddressOf / NameOf keep the first overload, as before. - A receiver name that is neither a value nor a type is looked up as a namespace the way VB.NET does: nested in a namespace around the read, outward to the root namespaces, then in the file's and project's Imports. Each further link is a type declared in it, a nested namespace, or a member of one of its Modules. The read links the type (`Billing.Invoice`) or the member (`Assets.Logo`), as #2355's `Outer.Mode` reads do. - A bare name before the dot that VB.NET binds to a field, property or Module variable (not a local or parameter, which hide it) is linked as a read alongside what is read through it. Not for a `With` block's `.Text`, a method run through the name (the call is the method's alone, as with parentheses), another project's Module, or a member declared with an outside type of its own name (`Responser As Responser`), whose Shared members and nested types VB.NET reads through the type; a project type of its own name follows #2377's Color Color rule. - membersNamed looked a type's members up in every project's same-named type and memoised the list by qualified name alone, so SCrawler's two plugin `UserData` classes shared one list ordered by whichever was asked first. It now keeps to the type's own project. Alone this leaves main's graph unchanged on SCrawler and staxrip; without it, 17 of the new bare-name reads in one plugin linked the other plugin's members. Dropped from the 2026-10-04 test set: a method named through its class also linking the class (#2355 treats it as a call, and its test pins that); `AppSession.SessionId` through a property typed OtherSession linking nothing (#2377 links OtherSession's field, pinned the same way); `SCrawler.My.Resources...` reads (#2355/#2377 send no path that starts with `My` or `Global`); and same-named classes picked by namespace, Imports and project, which main already does once the fixture declares its RootNamespace. Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on main and passes; a mutation that restores main's membersNamed fails the own-project test. The other VB.NET, C#, extraction, resolution, sync and framework suites pass; tsc and build clean. Validation, main vs this branch (indexes from each arm's own build; nodes unchanged; self-loops unchanged; serilog and gin dumps byte-identical): - SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet` re-pointed from the two-argument overload to `ColorsGet() As DataColor`. Added: 4,973 bare-name reads (3,731 own class, 957 Module variables, 285 inherited), 192 namespace reads, 12 reads through a Module member reached by namespace, and that 1 overload. - staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with the root namespace in front, and `box.Show` reaching `Function Show() As DialogResult` from its Shared overload. - Checked against source: every bare-name read's name starts the access, has no local of that name, and binds to its class, an ancestor (23 through a qualified `Inherits Instagram.UserData` the graph's own extends edge misses) or a Module of its project; every namespace read's path ends its target's full name and the target's project is the reader's or one it references; no edge crosses between staxrip and its AutoCrop tool; 48 sampled edges hand-checked, all correct. - Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall 5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall 7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the growth being the new edges (unresolved refs fall 47,074 -> 43,607 and 38,051 -> 36,165). Not covered: a call through a bare name (`Button1.Focus()`) or a function named before the dot, reads in a `With Button1` block, and `My.` / `Global.` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8b5d1d543d
commit
dbb6aa9ba9
@@ -69,6 +69,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, what a top-level variable's or a field's initializer calls, creates and reads is now linked. In Riverpod apps, where most wiring lives in provider closures like `final repoProvider = Provider((ref) => Repository(ref.watch(dioProvider)));`, `callers`, impact and `codegraph_explore` now follow a provider to the code it builds instead of stopping at the provider. A `final` or `const` initializer belongs to its constant, and a `var`'s or a field's to its file or class. A call through a type's static constant, like `FutureProvider.autoDispose(…)`, now links to that constant instead of an unrelated method that shares its name. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call further down a chain, like Riverpod's `FutureProvider.autoDispose.family(…)` or `events.map(mapper).transform(…)` on a stream, now links to the method of the type the chain before it produces, worked out from what each step is declared to return, and to nothing when that type can't be worked out. Before, it linked to whichever project method shared its name, so `X.autoDispose.family(…)` reached the same provider builder whatever `X` was, and a stream's `transform` reached an unrelated `transform` method. `super.` calls now reach the superclass's method, `context.read<CounterBloc>().add(…)` reaches the bloc's own `add`, and the members of an extension marked `@internal` are found too. Re-index Dart projects after upgrading. (#750)
|
||||
- In VB.NET, reading or setting a field or property through a variable, parameter, field, `Me` or `MyBase`, like `x.Normal = 3`, `Me._h.Title`, `.Value` inside a `With` block or `.Switch` in `New BoolParam With {…}`, now counts as a use of that member, and so do calls inside a `With` block, functions called without parentheses and reads through a class whose name starts in lower case. Before, `codegraph callers` on an instance field or property missed most of its uses. Only a member that the value's declared type has or inherits is linked, nothing when that type comes from outside your project, and a value named after its class, like `theme As Theme`, counts as a use of the class only when it reads a `Shared` member. Re-index VB.NET projects after upgrading.
|
||||
- In VB.NET, a type or member read through a namespace, like `Billing.Invoice.MaxLines`, `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the project's root namespace in front, or a Module's member read through its namespace like `RedGifs.RedGifsSiteKey`, now links to what it names, and a method called without parentheses, like `Me.Load` or a constructor's `Me.New`, now links to the overload that takes no arguments instead of the first one declared or nothing. The namespace is looked up as VB.NET does, so a namespace of the same name in another project is not mistaken for it. Thanks @serkanince for the report. (#2305)
|
||||
- In VB.NET, a field, property or Module variable written by its bare name in front of a member, like `Button1` in `Button1.Text = "x"` or `Settings` in `Settings.Plugins.Count`, now counts as used by that code, so `codegraph callers` on a form's control or a shared settings object lists the code that reads through it. Nothing is linked for a local or parameter of that name, for the member of a same-named class in another project, or for a member declared with an outside type of its own name, like `Responser As Responser`, whose `Shared` members VB.NET reads through the type; a call through the name, like `Button1.Focus()`, still links only the method. Re-index VB.NET projects after upgrading. (#2305)
|
||||
- In JavaScript and TypeScript, an import whose name contains a `$`, like `import items$ from './store'`, `import * as api$ from './api'` or `import { load as load$ } from './data'`, now links the calls made through it, such as `items$.getState().inc()`, to the code it imports, and so does a `$` name in a `require(…)` destructuring or an `export { … } from` barrel. Before, such an import was skipped or bound under a cut-off name (`load` for `load$`), so those calls reached nothing and the functions they call could look unused. A local variable or parameter that reuses an imported `$` name, like `const items$ = …` inside a function, is no longer mistaken for the import. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
- In Dart, calling a parameter or a local variable, like `create(ref, arg)` inside `overrideWith(… create)`, a closure's parameter, a loop variable or a test's mock `listener(0, 1)`, no longer links the call to an unrelated function or constant of the same name in another file, or to a method the class inherits: a Bloc event handler's `emit(…)` is the handler's own `emit` parameter, not `Bloc.emit`. So `codegraph callers` and impact no longer list code that never calls them, while calls the parameter or variable doesn't cover, and calls to a local function, link as before. Re-index Dart projects after upgrading.
|
||||
- In Dart, `const` constructors and redirecting factories, like flutter_bloc's `const BlocProvider.value(…)` or freezed's `const factory LoginState.loading() = _Loading;`, are now indexed, so a call such as `BlocProvider.value(…)` or `BlocProvider<CounterCubit>.value(…)` links to that constructor instead of another class's member with the same name. A call through a class name now reaches only that class's own constructors and static members, never a look-alike class, a parent class, an instance getter of the same name, or a project method that merely shares its name with an SDK call like `Uri.parse(…)`, and a constructor is no longer linked from a bare name, the end of a call chain or an annotation like `@internal`. Re-index Dart projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* VB.NET member reads that #2355 and #2377 left unlinked (#2305 follow-up):
|
||||
*
|
||||
* - A method named without parentheses runs: VB.NET calls the overload that
|
||||
* takes no arguments (`Me.Load` is `Load(Optional force = False)`, not
|
||||
* `Load(path)`), and a constructor's `Me.New` / `MyBase.New` is another
|
||||
* constructor, never itself.
|
||||
* - A name before the dot can be a namespace: `Billing.Invoice.MaxLines`,
|
||||
* the project's root namespace in front (`Acme.Billing.Invoice.MaxLines`),
|
||||
* and a Module's member through its namespace (`Assets.Logo`). The namespace
|
||||
* is looked up as VB.NET does — in the namespaces around the read, outward to
|
||||
* the root namespaces, then in the file's `Imports` — so another project's
|
||||
* namespace of that name is not read.
|
||||
* - A bare name before the dot that is a field or property of the class around
|
||||
* the read (or of one it inherits, or a Module's variable) is a read of that
|
||||
* member too: `Button1.Text` reads the form's own `Button1` — never another
|
||||
* form's, nor a same-named class's in another project — although `Text` is
|
||||
* .NET's. A parameter or local of that name hides it; a value named like its
|
||||
* type (`Property Mode As Mode`) is read only when the member read through it
|
||||
* is an instance one, and not at all when that type is from outside
|
||||
* (`Responser As Responser`); a method run through the name is the call's
|
||||
* alone.
|
||||
*/
|
||||
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 files: Record<string, string> = {
|
||||
'App.vbproj': '<Project Sdk="Microsoft.NET.Sdk">\n <PropertyGroup>\n <RootNamespace>Acme</RootNamespace>\n </PropertyGroup>\n</Project>\n',
|
||||
'Plugin/Plugin.vbproj': '<Project Sdk="Microsoft.NET.Sdk">\n <PropertyGroup>\n <RootNamespace>Plugin</RootNamespace>\n </PropertyGroup>\n</Project>\n',
|
||||
'AppSession.vb': `Public Class AppSession
|
||||
Public Shared Property CurrentUser As String
|
||||
End Class
|
||||
`,
|
||||
'OtherSession.vb': `Public Class OtherSession
|
||||
Public Shared SessionId As Guid
|
||||
End Class
|
||||
`,
|
||||
// Overloads of a method VB.NET runs without parentheses.
|
||||
'Chain.vb': `Public Class Chain
|
||||
Public Sub New(ByVal name As String)
|
||||
Me.New
|
||||
Me.Load
|
||||
Dim n = Me.Count
|
||||
End Sub
|
||||
|
||||
Public Sub New()
|
||||
End Sub
|
||||
|
||||
Public Sub Load(ByVal path As String)
|
||||
End Sub
|
||||
|
||||
Public Sub Load(Optional ByVal force As Boolean = False)
|
||||
End Sub
|
||||
|
||||
Public Function Count(ByVal items As Integer()) As Integer
|
||||
Return 0
|
||||
End Function
|
||||
|
||||
Public Function Count(ParamArray items As Integer()) As Integer
|
||||
Return 0
|
||||
End Function
|
||||
End Class
|
||||
|
||||
Public Class LinkedChain
|
||||
Inherits Chain
|
||||
|
||||
Public Sub New()
|
||||
MyBase.New
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class Calc
|
||||
Public Shared Function GetTargetDAR(ByVal width As Integer) As Double
|
||||
Return 0
|
||||
End Function
|
||||
|
||||
Public Shared Function GetTargetDAR() As Double
|
||||
Return 0
|
||||
End Function
|
||||
End Class
|
||||
|
||||
Public Class Measurer
|
||||
Private Tracker As Chain
|
||||
|
||||
Public Function Ratio(ByVal c As Chain) As Double
|
||||
Dim n = c.Count
|
||||
Return Calc.GetTargetDAR
|
||||
End Function
|
||||
|
||||
Public Sub Track()
|
||||
Tracker.Load
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
// Types and a Module in namespaces of the Acme project.
|
||||
'Billing/Invoice.vb': `Namespace Billing
|
||||
Public Class Invoice
|
||||
Public Const MaxLines As Integer = 100
|
||||
Public Shared Function Create() As Invoice
|
||||
Return Nothing
|
||||
End Function
|
||||
End Class
|
||||
|
||||
Public Enum Status
|
||||
Open
|
||||
Paid
|
||||
End Enum
|
||||
End Namespace
|
||||
`,
|
||||
'Assets/Images.vb': `Namespace Assets
|
||||
Public Module Images
|
||||
Public Logo As String = "logo.png"
|
||||
Public Function Pick() As String
|
||||
Return Logo
|
||||
End Function
|
||||
End Module
|
||||
End Namespace
|
||||
`,
|
||||
'Forms/MainForm.Designer.vb': `<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
|
||||
Partial Class MainForm
|
||||
Inherits System.Windows.Forms.Form
|
||||
|
||||
Private Sub InitializeComponent()
|
||||
Me.Button1 = New System.Windows.Forms.Button()
|
||||
Me.Button1.Size = New System.Drawing.Size(75, 23)
|
||||
Me.Size = New System.Drawing.Size(300, 200)
|
||||
End Sub
|
||||
|
||||
Friend WithEvents Button1 As System.Windows.Forms.Button
|
||||
End Class
|
||||
`,
|
||||
'Forms/MainForm.vb': `Public Class MainForm
|
||||
Private Clicks As Integer
|
||||
|
||||
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
|
||||
Me.Clicks = Me.Clicks + 1
|
||||
Button1.Text = AppSession.CurrentUser
|
||||
With Button1
|
||||
.Enabled = False
|
||||
End With
|
||||
Me.Text = Console.Out.ToString()
|
||||
Dim n = Billing.Invoice.MaxLines
|
||||
Dim m = Acme.Billing.Invoice.MaxLines
|
||||
Dim s = Billing.Status.Paid
|
||||
Dim l = Assets.Logo
|
||||
Dim p = Assets.Pick
|
||||
Dim i = Billing.Invoice.Create
|
||||
Dim r = System.Drawing.Color.Red
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'Forms/OtherForm.Designer.vb': `Partial Class OtherForm
|
||||
Inherits System.Windows.Forms.Form
|
||||
|
||||
Friend WithEvents Button1 As System.Windows.Forms.Button
|
||||
End Class
|
||||
`,
|
||||
'Forms/OtherForm.vb': `Public Class OtherForm
|
||||
Private Sub Reload()
|
||||
Button1.Text = OtherSession.SessionId.ToString()
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
// The Plugin project sees no \`Billing\` namespace: Acme's is another project's.
|
||||
'Plugin/Report.vb': `Public Class Report
|
||||
Public Sub Run()
|
||||
Dim n = Billing.Invoice.MaxLines
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
'Model.vb': `Public Enum Mode
|
||||
Fast
|
||||
Slow
|
||||
End Enum
|
||||
|
||||
Public Class Settings
|
||||
Public Property Level As Integer
|
||||
End Class
|
||||
|
||||
Public Class Widget
|
||||
Public Property Length As Integer
|
||||
End Class
|
||||
`,
|
||||
'Globals.vb': `Public Module Globals
|
||||
Public Current As Settings
|
||||
End Module
|
||||
`,
|
||||
// What the name before the dot binds to: a parameter or local, then a member
|
||||
// of the class around it (or one it inherits, or a Module's), then a type.
|
||||
'Scopes.vb': `Public Class Reporter
|
||||
Public Sub Report(ByVal Mode As String)
|
||||
Dim Widget = "x"
|
||||
Dim n = Mode.Length + Widget.Length
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class SessionView
|
||||
Private ReadOnly Property AppSession As OtherSession
|
||||
|
||||
Public Sub Show()
|
||||
Dim id = AppSession.SessionId
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class SettingsUser
|
||||
Public Property Settings As Settings
|
||||
Public Property Mode As Mode
|
||||
|
||||
Public Sub Apply()
|
||||
Dim t = Settings.Level
|
||||
Dim f = Mode.Fast
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class BaseView
|
||||
Protected Title As String
|
||||
End Class
|
||||
|
||||
Public Class DerivedView
|
||||
Inherits BaseView
|
||||
|
||||
Public Sub Render()
|
||||
Dim n = Title.Length
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class Viewer
|
||||
Private Button1 As Settings
|
||||
|
||||
Public Sub Show()
|
||||
Dim lv = Current.Level
|
||||
End Sub
|
||||
|
||||
Public Sub Hide()
|
||||
Dim Button1 As New Settings()
|
||||
Dim x = Button1.Level
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Public Class Detector
|
||||
Public Class Data
|
||||
Public Shared Empty As Integer
|
||||
End Class
|
||||
|
||||
Public Function Probe() As Integer
|
||||
Return Data.Empty
|
||||
End Function
|
||||
End Class
|
||||
|
||||
Public Class Failure
|
||||
Inherits System.Exception
|
||||
|
||||
Public Function Describe() As Integer
|
||||
Return Data.Empty
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
// CRLF line endings, and multibyte text — naming the field — before the read.
|
||||
'Weird.vb': [
|
||||
'Public Class Weird',
|
||||
' Private Label1 As Settings',
|
||||
' Public Sub Go()',
|
||||
' Dim s = "Label1 éééé 😀" & Label1.Level & Billing.Invoice.MaxLines',
|
||||
' End Sub',
|
||||
'End Class',
|
||||
'',
|
||||
].join('\r\n'),
|
||||
// One class name in two namespaces and in another project: the file's
|
||||
// \`Imports\` says which \`UserData\` it means. A type in the file's own
|
||||
// namespace (the root one) comes before an imported one: \`Helper.Name\`.
|
||||
'Plugin/UserData.vb': `Public Class UserData
|
||||
Public Shared ChannelsLabel As String
|
||||
Private Label As String
|
||||
|
||||
Public Function Size() As Integer
|
||||
Return Label.Length
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
// Another plugin project declares a \`UserData\` of its own: its members are not Plugin's.
|
||||
'Plugin2/Plugin2.vbproj': '<Project Sdk="Microsoft.NET.Sdk">\n <PropertyGroup>\n <RootNamespace>Plugin2</RootNamespace>\n </PropertyGroup>\n</Project>\n',
|
||||
'Plugin2/UserData.vb': `Public Class UserData
|
||||
Private Label As String
|
||||
|
||||
Public Function Size() As Integer
|
||||
Return Label.Length
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
// A member declared As an outside type of its own name: VB.NET reads that
|
||||
// type's Shared members and nested types through the type ("Color Color").
|
||||
'Client.vb': `Public Class Client
|
||||
Protected Responser As Responser
|
||||
|
||||
Public Function Code() As Integer
|
||||
Dim m = Responser.Modes.Curl
|
||||
Return Responser.StatusCode
|
||||
End Function
|
||||
End Class
|
||||
`,
|
||||
'Helper.vb': `Public Class Helper
|
||||
Public Shared Name As String
|
||||
End Class
|
||||
`,
|
||||
'Lib/Helper.vb': `Namespace Lib
|
||||
Public Class Helper
|
||||
Public Shared Name As String
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Reddit/UserData.vb': `Namespace API.Reddit
|
||||
Public Class UserData
|
||||
Public Const ChannelsLabel As String = "Channels"
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'Twitter/UserData.vb': `Namespace API.Twitter
|
||||
Public Class UserData
|
||||
Public Const ChannelsLabel As String = "Lists"
|
||||
End Class
|
||||
End Namespace
|
||||
`,
|
||||
'ChannelView.vb': `Imports Acme.API.Reddit
|
||||
Imports Acme.Lib
|
||||
|
||||
Public Class ChannelView
|
||||
Public Sub Add()
|
||||
Dim l = UserData.ChannelsLabel
|
||||
Dim h = Helper.Name
|
||||
End Sub
|
||||
End Class
|
||||
`,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-scope-'));
|
||||
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 });
|
||||
});
|
||||
|
||||
/** The node named `qualifiedName` — at `line`, or in `file`, when the name alone leaves several. */
|
||||
function nodeId(qualifiedName: string, at?: number | string): string {
|
||||
const found = cg.getNodesInFiles(Object.keys(files)).filter((n) => n.qualifiedName === qualifiedName &&
|
||||
(at === undefined || (typeof at === 'number' ? n.startLine === at : n.filePath === at)));
|
||||
if (found.length !== 1) throw new Error(`${found.length} nodes named ${qualifiedName}${at === undefined ? '' : ` at ${at}`}`);
|
||||
return found[0]!.id;
|
||||
}
|
||||
|
||||
/** What links to `qualifiedName` with an edge of `kind`, one per edge: `file:qualifiedName` when `withFiles`. */
|
||||
function linkedFrom(qualifiedName: string, kind: 'references' | 'calls', at?: number | string, withFiles = false): string[] {
|
||||
return cg
|
||||
.getIncomingEdgesTo([nodeId(qualifiedName, at)], [kind])
|
||||
.map((e) => cg.getNode(e.source)!)
|
||||
.map((n) => (withFiles ? `${n.filePath}:${n.qualifiedName}` : n.qualifiedName))
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('VB.NET member reads through a namespace, a member of the class around them, or a method without parentheses (#2305)', () => {
|
||||
it('calls the overload that takes no arguments, and a constructor never itself', () => {
|
||||
// Chain.vb: New(name) at 2, New() at 8, Load(path) at 11, Load(Optional) at 14,
|
||||
// Count(items) at 17, Count(ParamArray) at 21.
|
||||
expect(linkedFrom('Chain::New', 'calls', 8)).toEqual(['Chain::New', 'LinkedChain::New']);
|
||||
expect(linkedFrom('Chain::New', 'calls', 2)).toEqual([]);
|
||||
// `Me.Load`, and `Tracker.Load` through a field, whose read is the call's alone.
|
||||
expect(linkedFrom('Chain::Load', 'calls', 14)).toEqual(['Chain::New', 'Measurer::Track']);
|
||||
expect(linkedFrom('Chain::Load', 'calls', 11)).toEqual([]);
|
||||
expect(linkedFrom('Measurer::Tracker', 'calls')).toEqual([]);
|
||||
expect(linkedFrom('Chain::Count', 'calls', 21)).toEqual(['Chain::New', 'Measurer::Ratio']);
|
||||
expect(linkedFrom('Chain::Count', 'calls', 17)).toEqual([]);
|
||||
// Calc's GetTargetDAR(width) at 35, GetTargetDAR() at 39.
|
||||
expect(linkedFrom('Calc::GetTargetDAR', 'calls', 39)).toEqual(['Measurer::Ratio']);
|
||||
expect(linkedFrom('Calc::GetTargetDAR', 'calls', 35)).toEqual([]);
|
||||
});
|
||||
|
||||
it('reads a type, a constant, an Enum case and a Module member through their namespace, the root namespace in front or not', () => {
|
||||
// `Billing.Invoice.MaxLines` and `Acme.Billing.Invoice.MaxLines`.
|
||||
expect(linkedFrom('Billing::Invoice::MaxLines', 'references'))
|
||||
.toEqual(['MainForm::Button1_Click', 'MainForm::Button1_Click', 'Weird::Go']);
|
||||
// The type each of them, and `Billing.Invoice.Create`, names.
|
||||
expect(linkedFrom('Billing::Invoice', 'references'))
|
||||
.toEqual(['MainForm::Button1_Click', 'MainForm::Button1_Click', 'MainForm::Button1_Click', 'Weird::Go']);
|
||||
expect(linkedFrom('Billing::Invoice::Create', 'calls')).toEqual(['MainForm::Button1_Click']);
|
||||
expect(linkedFrom('Billing::Status::Paid', 'references')).toEqual(['MainForm::Button1_Click']);
|
||||
expect(linkedFrom('Billing::Status', 'references')).toEqual(['MainForm::Button1_Click']);
|
||||
// A Module's members are its namespace's; the Module itself is not written.
|
||||
expect(linkedFrom('Assets::Images::Logo', 'references')).toEqual(['MainForm::Button1_Click']);
|
||||
expect(linkedFrom('Assets::Images::Pick', 'calls')).toEqual(['MainForm::Button1_Click']);
|
||||
expect(linkedFrom('Assets::Images', 'references')).toEqual([]);
|
||||
});
|
||||
|
||||
it('reads no namespace another project declares', () => {
|
||||
for (const qn of ['Billing::Invoice::MaxLines', 'Billing::Invoice']) {
|
||||
expect(linkedFrom(qn, 'references')).not.toContain('Report::Run');
|
||||
}
|
||||
});
|
||||
|
||||
it('reads a field or property of the class around the read through its bare name', () => {
|
||||
// `Button1.Text` in Button1_Click and the designer's two `Me.Button1`; the
|
||||
// `With Button1` block's `.Enabled` is no second read of it.
|
||||
expect(linkedFrom('MainForm::Button1', 'references'))
|
||||
.toEqual(['MainForm::Button1_Click', 'MainForm::InitializeComponent', 'MainForm::InitializeComponent']);
|
||||
expect(linkedFrom('OtherForm::Button1', 'references')).toEqual(['OtherForm::Reload']);
|
||||
expect(linkedFrom('MainForm::Clicks', 'references')).toEqual(['MainForm::Button1_Click', 'MainForm::Button1_Click']);
|
||||
// An inherited field, a Module's variable, and a field read after multibyte text.
|
||||
expect(linkedFrom('BaseView::Title', 'references')).toEqual(['DerivedView::Render']);
|
||||
expect(linkedFrom('Globals::Current', 'references')).toEqual(['Viewer::Show']);
|
||||
expect(linkedFrom('Weird::Label1', 'references')).toEqual(['Weird::Go']);
|
||||
expect(linkedFrom('Settings::Level', 'references')).toEqual(['SettingsUser::Apply', 'Viewer::Hide', 'Viewer::Show', 'Weird::Go']);
|
||||
});
|
||||
|
||||
it('binds the name before the dot as VB does: parameter or local, own member, then type', () => {
|
||||
// `Mode` is a parameter and `Widget` a local of Report; `Button1` a local of Hide.
|
||||
expect(linkedFrom('Mode', 'references')).toEqual(['SettingsUser::Apply']);
|
||||
expect(linkedFrom('Widget', 'references')).toEqual([]);
|
||||
expect(linkedFrom('Widget::Length', 'references')).toEqual([]);
|
||||
expect(linkedFrom('Viewer::Button1', 'references')).toEqual([]);
|
||||
// SessionView's own `AppSession` property holds an OtherSession.
|
||||
expect(linkedFrom('SessionView::AppSession', 'references')).toEqual(['SessionView::Show']);
|
||||
expect(linkedFrom('OtherSession::SessionId', 'references')).toEqual(['OtherForm::Reload', 'SessionView::Show']);
|
||||
// "Color Color": `Settings.Level` reads the property, and an instance
|
||||
// member of its type; `Mode.Fast` reads an Enum case through the type.
|
||||
expect(linkedFrom('SettingsUser::Settings', 'references')).toEqual(['SettingsUser::Apply']);
|
||||
expect(linkedFrom('SettingsUser::Mode', 'references')).toEqual([]);
|
||||
expect(linkedFrom('Mode::Fast', 'references')).toEqual(['SettingsUser::Apply']);
|
||||
// A type nested in a class is a bare name only inside it: Failure's `Data` is Exception's.
|
||||
expect(linkedFrom('Detector::Data::Empty', 'references')).toEqual(['Detector::Probe']);
|
||||
expect(linkedFrom('Detector::Data', 'references')).toEqual(['Detector::Probe']);
|
||||
// `Responser As Responser`, a type from outside: `Responser.Modes` may name
|
||||
// the type's nested enum, which nothing here can tell, so no read is linked.
|
||||
expect(linkedFrom('Client::Responser', 'references')).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads a member of the class's own project, not of a same-named class in another", () => {
|
||||
expect(linkedFrom('UserData::Label', 'references', 'Plugin/UserData.vb', true)).toEqual(['Plugin/UserData.vb:UserData::Size']);
|
||||
expect(linkedFrom('UserData::Label', 'references', 'Plugin2/UserData.vb', true)).toEqual(['Plugin2/UserData.vb:UserData::Size']);
|
||||
});
|
||||
|
||||
it('picks among same-named classes the one VB finds first, for the member and the class', () => {
|
||||
expect(linkedFrom('API.Reddit::UserData::ChannelsLabel', 'references')).toEqual(['ChannelView::Add']);
|
||||
expect(linkedFrom('API.Reddit::UserData', 'references')).toEqual(['ChannelView::Add']);
|
||||
expect(linkedFrom('API.Twitter::UserData::ChannelsLabel', 'references')).toEqual([]);
|
||||
expect(linkedFrom('UserData::ChannelsLabel', 'references')).toEqual([]);
|
||||
expect(linkedFrom('Helper::Name', 'references')).toEqual(['ChannelView::Add']);
|
||||
expect(linkedFrom('Lib::Helper::Name', 'references')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not bind a member of another class that only shares the name', () => {
|
||||
// `Me.Size` / `Me.Text` on a Form, `Console.Out`, `System.Drawing.Color.Red`.
|
||||
expect(linkedFrom('MainForm::Button1', 'references')).not.toContain('OtherForm::Reload');
|
||||
expect(linkedFrom('OtherForm::Button1', 'references')).not.toContain('MainForm::Button1_Click');
|
||||
expect(linkedFrom('Widget::Length', 'references')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the edges through an incremental sync', async () => {
|
||||
for (const rel of ['Forms/MainForm.vb', 'Billing/Invoice.vb', 'Chain.vb', 'Scopes.vb']) {
|
||||
fs.writeFileSync(path.join(root, rel), `' touched\n${files[rel]}`);
|
||||
}
|
||||
await cg.sync();
|
||||
expect(linkedFrom('Billing::Invoice::MaxLines', 'references'))
|
||||
.toEqual(['MainForm::Button1_Click', 'MainForm::Button1_Click', 'Weird::Go']);
|
||||
expect(linkedFrom('Assets::Images::Logo', 'references')).toEqual(['MainForm::Button1_Click']);
|
||||
expect(linkedFrom('MainForm::Button1', 'references'))
|
||||
.toEqual(['MainForm::Button1_Click', 'MainForm::InitializeComponent', 'MainForm::InitializeComponent']);
|
||||
expect(linkedFrom('SessionView::AppSession', 'references')).toEqual(['SessionView::Show']);
|
||||
// The touched Chain.vb moved down a line.
|
||||
expect(linkedFrom('Chain::Load', 'calls', 15)).toEqual(['Chain::New', 'Measurer::Track']);
|
||||
expect(linkedFrom('Chain::New', 'calls', 9)).toEqual(['Chain::New', 'LinkedChain::New']);
|
||||
});
|
||||
});
|
||||
@@ -466,21 +466,27 @@ const VB_MEMBERS = new WeakMap<ResolutionContext, Map<string, Node[]>>();
|
||||
* Members named `name` declared directly in `type` (any partial part of it),
|
||||
* case aside. A member is declared inside a part of its type, so it is looked
|
||||
* up in those parts' files: one lookup per type name, not one per member name.
|
||||
* The parts are in the type's own project: SCrawler's plugins each declare a
|
||||
* `UserData`, whose members are not one another's.
|
||||
*/
|
||||
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 project = projectOf(type.filePath, context);
|
||||
const qn = `${type.qualifiedName}::${name}`.toLowerCase();
|
||||
const hit = memo.get(qn);
|
||||
const key = `${project}|${qn}`;
|
||||
const hit = memo.get(key);
|
||||
if (hit) return hit;
|
||||
const own = type.qualifiedName.toLowerCase();
|
||||
const files = new Set([type.filePath]);
|
||||
for (const part of context.getNodesByLowerName(type.name.toLowerCase())) {
|
||||
if (part.language === 'vbnet' && part.qualifiedName.toLowerCase() === own) files.add(part.filePath);
|
||||
if (part.language === 'vbnet' && part.qualifiedName.toLowerCase() === own && projectOf(part.filePath, context) === project) {
|
||||
files.add(part.filePath);
|
||||
}
|
||||
}
|
||||
const members = [...files].flatMap((file) => nodesByQualifiedName(file, context).get(qn) ?? []);
|
||||
if (memo.size >= 65536) memo.delete(memo.keys().next().value!);
|
||||
memo.set(qn, members);
|
||||
memo.set(key, members);
|
||||
return members;
|
||||
}
|
||||
|
||||
@@ -848,6 +854,83 @@ function typesNamedAt(
|
||||
return { owners: [...tier.filter((c) => c.filePath === file), ...tier.filter((c) => c.filePath !== file)], ambiguous: distinct.size > 1 };
|
||||
}
|
||||
|
||||
const VB_NAMESPACES = new WeakMap<ResolutionContext, Set<string>>();
|
||||
|
||||
/**
|
||||
* Every namespace the project's VB.NET code is declared in, by its full name
|
||||
* (lowercased, dot-joined, root namespace first), and each namespace around
|
||||
* one: `acme`, `acme.billing`, `scrawler.api.reddit`.
|
||||
*/
|
||||
function knownNamespaces(context: ResolutionContext): Set<string> {
|
||||
const hit = VB_NAMESPACES.get(context);
|
||||
if (hit) return hit;
|
||||
const known = new Set<string>();
|
||||
const add = (segments: string[]) => {
|
||||
for (let i = 1; i <= segments.length; i++) known.add(segments.slice(0, i).join('.'));
|
||||
};
|
||||
for (const n of context.getNodesByKind('namespace')) if (n.language === 'vbnet') add(fullSegments(n, context));
|
||||
for (const file of context.getAllFiles()) if (/\.vb$/i.test(file)) add(projectInfo(file, context).root);
|
||||
VB_NAMESPACES.set(context, known);
|
||||
return known;
|
||||
}
|
||||
|
||||
/** The namespace a line of a file is in, as full segments: its project's root namespace, then the `Namespace` block around it. */
|
||||
function namespaceAround(file: string, line: number, context: ResolutionContext): string[] {
|
||||
const root = projectInfo(file, context).root;
|
||||
const around = typesAround(file, line, context);
|
||||
const outer = around[around.length - 1];
|
||||
const cut = outer ? outer.qualifiedName.indexOf('::') : -1;
|
||||
if (cut < 0) return root;
|
||||
const head = outer!.qualifiedName.slice(0, cut).toLowerCase();
|
||||
return nodesByQualifiedName(file, context).get(head)?.some((n) => n.kind === 'namespace') ? [...root, ...head.split('.')] : root;
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace a name written first in a path means where it is written,
|
||||
* as full segments — as VB.NET looks one up: nested in a namespace around the
|
||||
* read, the innermost first and the root namespaces last, else in one the
|
||||
* file or its project imports. Null when no namespace of that name is there:
|
||||
* `Billing` is Acme's `Billing` in Acme's code, and nothing in a project
|
||||
* whose own namespaces don't have one.
|
||||
*/
|
||||
function namespaceNamed(name: string, ref: UnresolvedRef, context: ResolutionContext): string[] | null {
|
||||
const known = knownNamespaces(context);
|
||||
const key = name.toLowerCase();
|
||||
const around = namespaceAround(ref.filePath, ref.line, context);
|
||||
for (let i = around.length; i >= 0; i--) {
|
||||
const candidate = [...around.slice(0, i), key];
|
||||
if (known.has(candidate.join('.'))) return candidate;
|
||||
}
|
||||
for (const imported of importedNamespaces(ref.filePath, context)) {
|
||||
const candidate = [...imported.split('.'), key];
|
||||
if (known.has(candidate.join('.'))) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The project types named `name` declared directly in namespace `ns` (full
|
||||
* segments): the caller's project's, if it has one — SCrawler's projects
|
||||
* share a root namespace — and null when that leaves two different types.
|
||||
*/
|
||||
function typesInNamespace(ns: string[], name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] | null {
|
||||
const key = ns.join('.');
|
||||
let found = projectTypesNamed(name, context, VB_READ_TYPE_KINDS)
|
||||
.filter((c) => fullSegments(c, context).slice(0, -1).join('.') === key);
|
||||
const own = found.filter((c) => sameVbProject(c.filePath, ref.filePath, context));
|
||||
if (own.length > 0) found = own;
|
||||
const distinct = new Set(found.map((c) => `${projectOf(c.filePath, context)}|${c.qualifiedName.toLowerCase()}`));
|
||||
return distinct.size > 1 ? null
|
||||
: [...found.filter((c) => c.filePath === ref.filePath), ...found.filter((c) => c.filePath !== ref.filePath)];
|
||||
}
|
||||
|
||||
/** A member named `name` of a `Module` declared directly in namespace `ns`, which VB.NET reads through the namespace: `Assets.Logo`. */
|
||||
function moduleMemberIn(ns: string[], name: string, ref: UnresolvedRef, context: ResolutionContext): Node | undefined {
|
||||
const key = ns.join('.');
|
||||
return moduleMembersNamed(name, ref, context).find((n) =>
|
||||
(VB_VALUE_KINDS.has(n.kind) || n.kind === 'method') && fullSegments(n, context).slice(0, -2).join('.') === key);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
@@ -941,20 +1024,30 @@ function valueMemberType(member: Node, context: ResolutionContext, args: Readonl
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The member a receiver-less `name` reads where a call is: a field, property
|
||||
* or function of a class around the call or of one it inherits (with what
|
||||
* that class's type parameters stand for there), else of a `Module`.
|
||||
*/
|
||||
function memberType(name: string, ref: UnresolvedRef, context: ResolutionContext): VbType | null | undefined {
|
||||
function memberAround(name: string, ref: UnresolvedRef, context: ResolutionContext): { node: Node; args: Map<string, VbType> } | 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);
|
||||
if (member) return { node: member, args };
|
||||
}
|
||||
}
|
||||
const global = moduleMembersNamed(name, ref, context).find(fits);
|
||||
return global ? valueMemberType(global, context) : undefined;
|
||||
return global ? { node: global, args: new Map() } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of what a receiver-less `name` reads where a call is (see
|
||||
* memberAround). 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 member = memberAround(name, ref, context);
|
||||
return member ? valueMemberType(member.node, context, member.args) : undefined;
|
||||
}
|
||||
|
||||
/** The type a method, function or property returns, read from its declaration; null for a `Sub` or none written. */
|
||||
@@ -1378,6 +1471,11 @@ interface VbReceiver {
|
||||
throughType: boolean;
|
||||
/** The name is a value whose type has the same name (`theme As Theme`): VB.NET's "Color Color" rule. */
|
||||
colorColor?: boolean;
|
||||
/**
|
||||
* The receiver names a namespace (`Billing`, `Acme.Billing`), whose types and
|
||||
* Modules' members are read through it: its full segments. No owners.
|
||||
*/
|
||||
namespace?: string[];
|
||||
}
|
||||
|
||||
/** `Name` for an escaped `[Name]`. */
|
||||
@@ -1398,7 +1496,8 @@ function valueReceiver(t: VbType | null | undefined, context: ResolutionContext)
|
||||
* inherits, `{T}` a value of `T`. A name holds a value of its declared type
|
||||
* when it is a local, a parameter, or a member of a type around the read or
|
||||
* of a Module; any other name means the class, module, structure, interface
|
||||
* or enum it names there. A value whose type has its name (`Public Property
|
||||
* or enum it names there, else a namespace (`Billing`, the root namespace
|
||||
* `Acme`). A value whose type has its name (`Public Property
|
||||
* Settings As Settings`, `theme As Theme`) may mean either, by VB.NET's
|
||||
* "Color Color" rule: the member read through it decides (matchVbMemberRead).
|
||||
*/
|
||||
@@ -1419,17 +1518,29 @@ function receiverAt(head: string, ref: UnresolvedRef, context: ResolutionContext
|
||||
: bound && !bound.array && bound.name.toLowerCase() === name.toLowerCase() ? bound : null;
|
||||
if (!written) return valueReceiver(bound, context);
|
||||
const found = typesNamedAt(written, context, VB_READ_TYPE_KINDS);
|
||||
return found.ambiguous || found.owners.length === 0 ? null
|
||||
: { owners: found.owners, throughType: true, ...(bound ? { colorColor: true } : {}) };
|
||||
if (found.ambiguous) return null;
|
||||
if (found.owners.length > 0) return { owners: found.owners, throughType: true, ...(bound ? { colorColor: true } : {}) };
|
||||
const namespace = bound === undefined ? namespaceNamed(name, ref, context) : null;
|
||||
return namespace ? { owners: [], throughType: false, namespace } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `name`, read through `at`, is as a receiver in turn: a value of the
|
||||
* type a field, property or function declares, or a type nested in a type
|
||||
* named for it (`Outer.Mode`). Null where the member, or its type, is not
|
||||
* known here.
|
||||
* named for it (`Outer.Mode`); through a namespace, a type declared in it, a
|
||||
* namespace nested in it, or the value one of its Modules' members holds.
|
||||
* Null where the member, or its type, is not known here.
|
||||
*/
|
||||
function memberAt(at: VbReceiver, name: string, ref: UnresolvedRef, context: ResolutionContext): VbReceiver | null {
|
||||
if (at.namespace) {
|
||||
const types = typesInNamespace(at.namespace, name, ref, context);
|
||||
if (types === null) return null;
|
||||
if (types.length > 0) return { owners: types, throughType: true };
|
||||
const nested = [...at.namespace, name.toLowerCase()];
|
||||
if (knownNamespaces(context).has(nested.join('.'))) return { owners: [], throughType: false, namespace: nested };
|
||||
const member = moduleMemberIn(at.namespace, name, ref, context);
|
||||
return member ? valueReceiver(valueMemberType(member, context), context) : null;
|
||||
}
|
||||
if (at.owners.length === 0) return null;
|
||||
const fits = (n: Node) => VB_INSTANCE_READ_KINDS.has(n.kind) || (at.throughType && VB_READ_TYPE_KINDS.has(n.kind));
|
||||
const found = memberOn(at.owners, name, ref, context, true, at.typed, fits) ??
|
||||
@@ -1482,6 +1593,116 @@ function namesWithoutRunning(ref: UnresolvedRef, context: ResolutionContext): bo
|
||||
return /\b(?:AddressOf\s+|NameOf\s*\(\s*)$/i.test(line.slice(0, Math.max(0, ref.column)));
|
||||
}
|
||||
|
||||
const VB_NO_ARGUMENTS = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/**
|
||||
* Whether VB.NET can call a method with no arguments: it declares no
|
||||
* parameter, or only `Optional` ones and a `ParamArray`. Read from its
|
||||
* declaration; true when that can't be read.
|
||||
*/
|
||||
function takesNoArguments(n: Node, context: ResolutionContext): boolean {
|
||||
let memo = VB_NO_ARGUMENTS.get(context);
|
||||
if (!memo) VB_NO_ARGUMENTS.set(context, (memo = new Map()));
|
||||
const hit = memo.get(n.id);
|
||||
if (hit !== undefined) return hit;
|
||||
const code = codeLines(n.filePath, context).slice(n.startLine - 1, n.startLine + 6).join(' ');
|
||||
const head = new RegExp(`\\b(?:Sub|Function)\\s+\\[?${escapeRegex(unescaped(n.name))}\\]?(?![\\w$%&!#@])`, 'i').exec(code);
|
||||
let none = true;
|
||||
if (head) {
|
||||
let at = head.index + head[0].length;
|
||||
// `Function Parse(Of T)(text As String)`: the type parameters come first.
|
||||
if (/^\s*\(\s*Of\b/i.test(code.slice(at))) at = closeParen(code, code.indexOf('(', at)) + 1;
|
||||
const open = at > 0 ? /^\s*\(/.exec(code.slice(at)) : null;
|
||||
const params = open ? splitArgs(code, at + open[0].length - 1) : null;
|
||||
none = !params || params.every((p) => {
|
||||
const param = p.replace(/<[^<>]*>/g, '').replace(/^[\s_]+/, '');
|
||||
return param === '' || /^(?:(?:ByVal|ByRef)\s+)*(?:Optional|ParamArray)\b/i.test(param);
|
||||
});
|
||||
}
|
||||
memo.set(n.id, none);
|
||||
return none;
|
||||
}
|
||||
|
||||
/**
|
||||
* The overload of `member` (a method) that VB.NET runs where a reference
|
||||
* names it without parentheses: one that takes no arguments — `Me.Load` is
|
||||
* `Load(Optional force = False)`, not `Load(path)` — and never the member
|
||||
* the reference is in: a constructor's `Me.New` is another constructor.
|
||||
* `member` itself when its overloads don't tell; null when the only one is
|
||||
* the member the reference is in.
|
||||
*/
|
||||
function runnableOverload(member: Node, ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
||||
const cut = member.qualifiedName.lastIndexOf('::');
|
||||
const owner = cut > 0 ? context.getNodesByQualifiedName(member.qualifiedName.slice(0, cut))
|
||||
.find((n) => n.language === 'vbnet' && VB_TYPE_KINDS.has(n.kind)) : undefined;
|
||||
const project = projectOf(member.filePath, context);
|
||||
const overloads = owner ? membersNamed(owner, member.name, context)
|
||||
.filter((n) => n.kind === 'method' && projectOf(n.filePath, context) === project) : [];
|
||||
const others = overloads.filter((n) => n.id !== ref.fromNodeId);
|
||||
if (others.length === 0) return member.id === ref.fromNodeId ? null : member;
|
||||
const runnable = others.filter((n) => takesNoArguments(n, context));
|
||||
const pool = runnable.length > 0 ? runnable : others;
|
||||
return pool.find((n) => n.id === member.id) ?? pool[0]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* The field, property or Module variable a bare name before the dot reads as
|
||||
* well, where VB.NET binds the name to one: `Button1` in `Button1.Text` is
|
||||
* the form's own control, `Title` a field its class inherits, `Current` a
|
||||
* variable of one of its project's Modules — whatever the member read through
|
||||
* it. Null for a parameter or local of that name, which hides the member;
|
||||
* for `Me`, `MyBase` and a `{T}` value; for a `With` block's `.Text`, whose
|
||||
* `With` names the value; and for a member declared as a type of its own name
|
||||
* from outside the project (`Responser As Responser`): VB.NET reads that
|
||||
* type's Shared members and nested types through the type, not the member,
|
||||
* and nothing here shows which these are.
|
||||
*/
|
||||
function headMemberOf(head: string, ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
||||
if (head.startsWith('{') || /^(?:Me|MyClass|MyBase)$/i.test(head)) return null;
|
||||
const name = unescaped(head);
|
||||
// The name itself starts the access where the reference is: `Button1` or `[Button1]`, then the dot.
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
|
||||
const bracketed = line[ref.column] === '[';
|
||||
let at = ref.column + (bracketed ? 1 : 0);
|
||||
if (line.slice(at, at + name.length).toLowerCase() !== name.toLowerCase()) return null;
|
||||
at += name.length;
|
||||
if (bracketed && line[at++] !== ']') return null;
|
||||
while (line[at] === ' ' || line[at] === '\t') at++;
|
||||
if (line[at] !== '.') return null;
|
||||
if (localBinding(name, ref, context)) return null;
|
||||
const found = memberAround(name, ref, context);
|
||||
const member = found?.node;
|
||||
if (!member || !VB_VALUE_KINDS.has(member.kind) || member.id === ref.fromNodeId) return null;
|
||||
// Another project's Module is one this code can't name without a reference to it.
|
||||
const cut = member.qualifiedName.lastIndexOf('::');
|
||||
const foreign = projectOf(member.filePath, context) !== projectOf(ref.filePath, context);
|
||||
if (foreign && cut > 0 && isModule(member.qualifiedName.slice(0, cut), context)) return null;
|
||||
return isOutsideNamesake(member, found!.args, context) ? null : member;
|
||||
}
|
||||
|
||||
const VB_NAMESAKES = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/**
|
||||
* Whether a member is declared as a type of its own name that isn't one of
|
||||
* the project's (`Responser As Responser`), or names two of them: a read
|
||||
* through its name may be one through that type, which nothing here can tell.
|
||||
*/
|
||||
function isOutsideNamesake(member: Node, args: ReadonlyMap<string, VbType>, context: ResolutionContext): boolean {
|
||||
let memo = VB_NAMESAKES.get(context);
|
||||
if (!memo) VB_NAMESAKES.set(context, (memo = new Map()));
|
||||
// What a type parameter stands for depends on where the member is reached from.
|
||||
const hit = args.size === 0 ? memo.get(member.id) : undefined;
|
||||
if (hit !== undefined) return hit;
|
||||
const declared = valueMemberType(member, context, args);
|
||||
let namesake = false;
|
||||
if (declared && !declared.array && declared.name.toLowerCase() === unescaped(member.name).toLowerCase()) {
|
||||
const types = typesNamedAt(declared, context, VB_READ_TYPE_KINDS);
|
||||
namesake = types.ambiguous || types.owners.length === 0;
|
||||
}
|
||||
if (args.size === 0) memo.set(member.id, namesake);
|
||||
return namesake;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a VB.NET member read or write to the member the receiver's type
|
||||
* declares or inherits. Through a type's name (#2305) — `AppSession.SessionId`,
|
||||
@@ -1494,7 +1715,12 @@ function namesWithoutRunning(ref: UnresolvedRef, context: ResolutionContext): bo
|
||||
* mean. A value named like its type (`theme As Theme`) reads an instance
|
||||
* member as a value does and a Shared one as the type does, by VB.NET's
|
||||
* "Color Color" rule. A method named either way is called, as without
|
||||
* parentheses VB.NET runs it, unless `AddressOf` or `NameOf` only names it.
|
||||
* parentheses VB.NET runs it — the overload that takes no arguments — unless
|
||||
* `AddressOf` or `NameOf` only names it. Through a namespace — `Billing.Invoice`,
|
||||
* `Acme.Billing.Invoice.MaxLines`, a Module's `Assets.Logo` — it links the type
|
||||
* or the Module's member the namespace declares. A bare name before the dot
|
||||
* that is a field, property or Module variable (`Button1` of `Button1.Text`)
|
||||
* is read too, whatever the member read through it.
|
||||
* Null when a link of the path is not typed here (an outside type:
|
||||
* `Color.Red`, `str.Length`), is late-bound (`Object`), or names two of the
|
||||
* project's types.
|
||||
@@ -1504,10 +1730,11 @@ export function matchVbMemberRead(ref: UnresolvedRef, context: ResolutionContext
|
||||
if (!m) return null;
|
||||
const links = m[2] ? m[2].slice(1).split('.') : [];
|
||||
const at = pathReceiver(m[1]!, links, ref, context);
|
||||
if (!at || at.owners.length === 0) return null;
|
||||
// `MySettings.Default` reads the property declared `[Default]`, and back.
|
||||
const name = unescaped(m[3]!);
|
||||
const calls = (member: Node) => member.kind === 'method' && !namesWithoutRunning(ref, context);
|
||||
// A method VB.NET runs here is the overload it can call without arguments.
|
||||
const run = (member: Node | undefined): Node | null => (!member ? null : calls(member) ? runnableOverload(member, ref, context) : member);
|
||||
const throughValue = (member: Node): ResolvedRef => ({
|
||||
original: ref,
|
||||
targetNodeId: member.id,
|
||||
@@ -1515,13 +1742,36 @@ export function matchVbMemberRead(ref: UnresolvedRef, context: ResolutionContext
|
||||
resolvedBy: 'instance-method',
|
||||
...(calls(member) ? { edgeKind: 'calls' as const } : {}),
|
||||
});
|
||||
// The member a bare name before the dot reads, beside what is read through
|
||||
// it. A method run through the name (`Button1.PerformClick`) is linked alone,
|
||||
// as a call written with parentheses is.
|
||||
const withHead = (read: ResolvedRef | null): ResolvedRef | null => {
|
||||
const head = links.length === 0 ? headMemberOf(m[1]!, ref, context) : null;
|
||||
if (!head) return read;
|
||||
if (!read) return { original: ref, targetNodeId: head.id, confidence: 0.9, resolvedBy: 'instance-method' };
|
||||
return read.edgeKind === 'calls' ? read : { ...read, alsoTargets: [...(read.alsoTargets ?? []), { targetNodeId: head.id }] };
|
||||
};
|
||||
if (at?.namespace) {
|
||||
// A type, or a Module's member, read through its namespace.
|
||||
const types = typesInNamespace(at.namespace, name, ref, context);
|
||||
const target = types?.[0] ?? (types ? run(moduleMemberIn(at.namespace, name, ref, context)) : null);
|
||||
if (!target || target.id === ref.fromNodeId) return null;
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: target.id,
|
||||
confidence: target.kind === 'method' ? 0.85 : 0.9,
|
||||
resolvedBy: 'qualified-name',
|
||||
...(calls(target) ? { edgeKind: 'calls' as const } : {}),
|
||||
};
|
||||
}
|
||||
if (!at || at.owners.length === 0) return withHead(null);
|
||||
if (!at.throughType) {
|
||||
// The member the name means: the nearest the type declares or inherits.
|
||||
// The read's own member (`Return Me.Count` in `Count`) runs itself.
|
||||
const fits = (n: Node) => VB_INSTANCE_READ_KINDS.has(n.kind);
|
||||
const member = (memberOn(at.owners, name, ref, context, true, at.typed, fits) ??
|
||||
memberOn(at.owners, `[${name}]`, ref, context, true, at.typed, fits))?.node;
|
||||
return member && member.id !== ref.fromNodeId ? throughValue(member) : null;
|
||||
const member = run((memberOn(at.owners, name, ref, context, true, at.typed, fits) ??
|
||||
memberOn(at.owners, `[${name}]`, ref, context, true, at.typed, fits))?.node);
|
||||
return withHead(member && member.id !== ref.fromNodeId ? throughValue(member) : null);
|
||||
}
|
||||
// Not the member the read is written in: staxrip's `Overrides ReadOnly
|
||||
// Property Package` returns the class's `Shared ReadOnly Property Package`.
|
||||
@@ -1534,9 +1784,10 @@ export function matchVbMemberRead(ref: UnresolvedRef, context: ResolutionContext
|
||||
const found = member;
|
||||
member = context.getNodesByQualifiedName(found.qualifiedName).find((n) => n.isStatic && n.filePath === found.filePath && fits(n)) ?? found;
|
||||
}
|
||||
member = run(member) ?? member;
|
||||
// "Color Color": a value named like its type (`theme As Theme`) reads an
|
||||
// instance member through itself, and only a Shared one through the type.
|
||||
if (at.colorColor && !isSharedMember(member, context)) return throughValue(member);
|
||||
if (at.colorColor && !isSharedMember(member, context)) return withHead(throughValue(member));
|
||||
if (member.kind === 'method') {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -1649,7 +1900,7 @@ export function isVbMemberInScope(n: Node, ref: UnresolvedRef, context: Resoluti
|
||||
export function clearVbnetReceiverMemos(context: ResolutionContext): void {
|
||||
for (const memo of [VB_CODE_LINES, VB_WORD_LINES, VB_BINDINGS, VB_FILE_TYPES, VB_FILE_QNS, 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<WeakMap<ResolutionContext, unknown>>) {
|
||||
VB_SCOPES, VB_NAMESPACES, VB_NO_ARGUMENTS, VB_NAMESAKES] as Array<WeakMap<ResolutionContext, unknown>>) {
|
||||
memo.delete(context);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user