Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List<Foo> Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337)
- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
- In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name.
- VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it.
- Upgrading CodeGraph while an agent session is open no longer leaves the old version's background server in charge of your project: the first session started from the new install stops it and starts a current one in its place, even while sessions opened before the upgrade are still running. That old server could no longer load the language parsers the upgrade removed, so it saved every file it re-indexed with no symbols, while new sessions could only read the index beside it without keeping it up to date. A background server from a newer install is never stopped, and sessions opened before the upgrade keep the old version until you restart them. Thanks @lipchey for the report. (#2335)
- A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335)
- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
Expand Down
100 changes: 100 additions & 0 deletions __tests__/vbnet-bare-member-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* A VB.NET call with no receiver — or with `Me` / `MyClass` / `MyBase`,
* which the extractor drops — is a member of the type it is written in, of a
* type around it, or of one they inherit; a `Module`'s members are reached
* from anywhere. Never another class's same-named member, however near its
* file: SCrawler's `{ToString()}` in the Instagram `UserData` (which
* `Inherits UserDataBase`) went to a nearby structure's `ToString`.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-bare-'));
const files: Record<string, string> = {
'API/Base/UserDataBase.vb': `Namespace API.Base
Friend MustInherit Class UserDataBase
Public Overrides Function ToString() As String
Return "user"
End Function
Protected Sub Refresh()
End Sub
End Class
End Namespace
`,
'API/Instagram/MediaItem.vb': `Namespace API.Instagram
Friend Class MediaItem
Public Overrides Function ToString() As String
Return "media"
End Function
Friend Sub Refresh()
End Sub
End Class
End Namespace
`,
'API/Instagram/UserData.vb': `Namespace API.Instagram
Friend Class UserData : Inherits API.Base.UserDataBase
Friend Sub SetTagsLimit()
Dim aStr$ = $"Enter the number of posts from user {ToString()}"
Me.Refresh()
Log(aStr)
End Sub
Private Class Counter
Friend Sub Tick()
Report()
End Sub
End Class
Private Shared Sub Report()
End Sub
End Class
End Namespace
`,
'Tools/Logger.vb': `Public Module Logger
Public Sub Log(ByVal Text As String)
End Sub
End Module
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `Owner::member` of every call edge out of one method. */
function callsOf(file: string, qualifiedName: string): string[] {
const from = cg.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName)!;
return cg
.getOutgoingEdgesFrom([from.id])
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}

describe('VB.NET receiver-less calls', () => {
it('reach the members the class inherits, not a nearer class’s, and a module’s', () => {
expect(callsOf('API/Instagram/UserData.vb', 'API.Instagram::UserData::SetTagsLimit')).toEqual([
'API.Base::UserDataBase::Refresh',
'API.Base::UserDataBase::ToString',
'Logger::Log',
]);
});

it('reach a member of the class around a nested one', () => {
expect(callsOf('API/Instagram/UserData.vb', 'API.Instagram::UserData::Counter::Tick')).toEqual([
'API.Instagram::UserData::Report',
]);
});
});
35 changes: 35 additions & 0 deletions __tests__/vbnet-member-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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']);
});
});
143 changes: 143 additions & 0 deletions __tests__/vbnet-project-ties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* A VB.NET solution often carries the same type in two projects: staxrip's
* main app and its separate AutoCrop tool (its own AutoCrop.vbproj) each
* declare `ColorHSL`, `FrameServerFactory` and `DirectFrameServer`. A call
* in the main app means its own project's copy — about 90 calls such as
* `_backColor.AddLuminance(0.025)` went to the AutoCrop copy, whichever was
* indexed first. Between equally good guesses, the caller's project decides,
* then the nearer directory; when neither does, there is no guess.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

const VBPROJ = `<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
</PropertyGroup>
</Project>
`;

const COLOR_HSL = `Public Class ColorHSL
Public Sub New(ByVal h As Double, ByVal s As Double, ByVal l As Double, ByVal a As Double)
End Sub
Public Function AddLuminance(ByVal offset As Single) As ColorHSL
Return Me
End Function
End Class
`;

const FRAME_SERVER_FACTORY = `Public Class FrameServerFactory
Public Shared Function Create(ByVal path As String) As Object
Return Nothing
End Function
End Class
`;

const HELPER = `Public Class Helper
Public Sub Run()
End Sub
End Class
`;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-ties-'));
const files: Record<string, string> = {
'Source/StaxRip.vbproj': VBPROJ,
'Source/Tools/AutoCrop/AutoCrop.vbproj': VBPROJ,
'Source/Tools/AutoCrop/Main.vb': COLOR_HSL + FRAME_SERVER_FACTORY,
'Source/UI/ColorHSL.vb': COLOR_HSL,
'Source/Video/FrameServer.vb': FRAME_SERVER_FACTORY,
'Source/General/ThemeManager.vb': `Public Class ThemeManager
Public Sub Apply(ByVal palette As Object)
Dim _backColor As ColorHSL = New ColorHSL(0, 0.01, 0.1, 1)
Dim _controlBackColor As ColorHSL = _backColor.AddLuminance(0.025)
For Each backgroundColor In palette
backgroundColor.AddLuminance(-0.1)
Next
Dim server = FrameServerFactory.Create("video.mkv")
End Sub
End Class
`,
'Source/UI/TipProvider.vb': `Public Class TipProvider
Public Sub SetTip(ByVal text As String)
End Sub
Public Sub SetTip(ByVal text As String, ByVal title As String)
End Sub
End Class
`,
'Source/General/Tips.vb': `Public Class Tips
Public Sub Apply(ByVal providers As Object)
For Each tipProvider In providers
tipProvider.SetTip("x")
Next
End Sub
End Class
`,
'Plugins/A/A.vbproj': VBPROJ,
'Plugins/A/Helper.vb': HELPER,
'Plugins/B/B.vbproj': VBPROJ,
'Plugins/B/Helper.vb': HELPER,
'App/App.vbproj': VBPROJ,
'App/Main.vb': `Public Class Main
Public Sub Start(ByVal helpers As Object)
For Each runHelper In helpers
runHelper.Run()
Next
End Sub
End Class
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `Owner::member @ file` of every call / instantiation edge out of a file. */
function targetsFrom(file: string, kind: 'calls' | 'instantiates'): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === kind)
.map((e) => {
const target = cg.getNode(e.target)!;
return `${target.qualifiedName} @ ${target.filePath}`;
})
.sort();
}

describe('VB.NET duplicate types across projects', () => {
it('resolve a call to the caller’s own project’s copy', () => {
expect(targetsFrom('Source/General/ThemeManager.vb', 'calls')).toEqual([
'ColorHSL::AddLuminance @ Source/UI/ColorHSL.vb',
'ColorHSL::AddLuminance @ Source/UI/ColorHSL.vb',
'FrameServerFactory::Create @ Source/Video/FrameServer.vb',
]);
});

it('resolve a construction to the caller’s own project’s copy', () => {
expect(targetsFrom('Source/General/ThemeManager.vb', 'instantiates')).toEqual([
'ColorHSL @ Source/UI/ColorHSL.vb',
]);
});

it('make no guess between copies nothing tells apart', () => {
expect(targetsFrom('App/Main.vb', 'calls')).toEqual([]);
});

it('take one type’s overloads as one guess, not a tie', () => {
expect(targetsFrom('Source/General/Tips.vb', 'calls')).toEqual(['TipProvider::SetTip @ Source/UI/TipProvider.vb']);
});
});
Loading