From d5dc923a7e52a441c73869fb9823bb7cd28b41d8 Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 15 Oct 2026 23:20:09 +0300 Subject: [PATCH 1/2] fix: index Visual Basic Catch statements that declare no exception variable A Visual Basic `Catch` without an exception variable (`Catch`, or `Catch When condition`) has no IdentifierName, and ScipVisualBasicSyntaxWalker.VisitCatchStatement read node.IdentifierName.Identifier unconditionally. The resulting NullReferenceException ended the whole index, so no project containing a bare `Catch` could be indexed. The definition occurrence is now recorded only when the Catch declares a variable. Statements.vb in the syntax snapshot gains a `Catch When` and a bare `Catch`; without the fix, the syntax snapshot test fails with the exception. --- ScipDotnet/ScipVisualBasicSyntaxWalker.cs | 7 +++++- snapshots/input/syntax/VBMain/Statements.vb | 10 ++++++++ .../syntax/VBMain/Statements.vb | 23 +++++++++++++++++++ .../output-net8.0/syntax/VBMain/Statements.vb | 23 +++++++++++++++++++ .../output-net9.0/syntax/VBMain/Statements.vb | 23 +++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/ScipDotnet/ScipVisualBasicSyntaxWalker.cs b/ScipDotnet/ScipVisualBasicSyntaxWalker.cs index e4c191e..f71f6e7 100644 --- a/ScipDotnet/ScipVisualBasicSyntaxWalker.cs +++ b/ScipDotnet/ScipVisualBasicSyntaxWalker.cs @@ -45,7 +45,12 @@ public override void VisitEnumStatement(EnumStatementSyntax node) public override void VisitCatchStatement(CatchStatementSyntax node) { - _scipDocumentIndexer.VisitOccurrence(_semanticModel.GetDeclaredSymbol(node), node.IdentifierName.Identifier.GetLocation(), true); + // `Catch` and `Catch When condition` declare no exception variable, so IdentifierName is null. + if (node.IdentifierName != null) + { + _scipDocumentIndexer.VisitOccurrence(_semanticModel.GetDeclaredSymbol(node), node.IdentifierName.Identifier.GetLocation(), true); + } + base.VisitCatchStatement(node); } diff --git a/snapshots/input/syntax/VBMain/Statements.vb b/snapshots/input/syntax/VBMain/Statements.vb index ed76d9a..8dc45a1 100644 --- a/snapshots/input/syntax/VBMain/Statements.vb +++ b/snapshots/input/syntax/VBMain/Statements.vb @@ -12,6 +12,16 @@ Namespace VBMain End Try End Sub + Private Sub TryWithoutExceptionVariable(fileName As String) + Try + File.ReadLines(fileName) + Catch When fileName.Length = 0 + Console.WriteLine("empty file name") + Catch + Console.WriteLine("failed") + End Try + End Sub + Private Function [Default]() As (A As String, B As Boolean) Dim C As (A As String, B As Boolean) = ("42", 42) Return C diff --git a/snapshots/output-net10.0/syntax/VBMain/Statements.vb b/snapshots/output-net10.0/syntax/VBMain/Statements.vb index 0f2b103..ff8e157 100644 --- a/snapshots/output-net10.0/syntax/VBMain/Statements.vb +++ b/snapshots/output-net10.0/syntax/VBMain/Statements.vb @@ -32,6 +32,29 @@ End Try End Sub + Private Sub TryWithoutExceptionVariable(fileName As String) +' ^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable(). +' documentation ```vb\nPrivate Sub Statements.TryWithoutExceptionVariable(fileName As String)\n``` +' ^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' documentation ```vb\nfileName As String\n``` + Try + File.ReadLines(fileName) +' ^^^^ reference scip-dotnet nuget System.Runtime 10.0.0.0 IO/File# +' ^^^^^^^^^ reference scip-dotnet nuget System.Runtime 10.0.0.0 IO/File#ReadLines(). +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) + Catch When fileName.Length = 0 +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' ^^^^^^ reference scip-dotnet nuget System.Runtime 10.0.0.0 System/String#Length. + Console.WriteLine("empty file name") +' ^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console#WriteLine(+11). + Catch + Console.WriteLine("failed") +' ^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console#WriteLine(+11). + End Try + End Sub + Private Function [Default]() As (A As String, B As Boolean) ' ^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#Default(). ' documentation ```vb\nPrivate Function Statements.Default() As (A As String, B As Boolean)\n``` diff --git a/snapshots/output-net8.0/syntax/VBMain/Statements.vb b/snapshots/output-net8.0/syntax/VBMain/Statements.vb index 0bfc6eb..229f83e 100644 --- a/snapshots/output-net8.0/syntax/VBMain/Statements.vb +++ b/snapshots/output-net8.0/syntax/VBMain/Statements.vb @@ -32,6 +32,29 @@ End Try End Sub + Private Sub TryWithoutExceptionVariable(fileName As String) +' ^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable(). +' documentation ```vb\nPrivate Sub Statements.TryWithoutExceptionVariable(fileName As String)\n``` +' ^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' documentation ```vb\nfileName As String\n``` + Try + File.ReadLines(fileName) +' ^^^^ reference scip-dotnet nuget System.Runtime 8.0.0.0 IO/File# +' ^^^^^^^^^ reference scip-dotnet nuget System.Runtime 8.0.0.0 IO/File#ReadLines(). +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) + Catch When fileName.Length = 0 +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' ^^^^^^ reference scip-dotnet nuget System.Runtime 8.0.0.0 System/String#Length. + Console.WriteLine("empty file name") +' ^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console#WriteLine(+11). + Catch + Console.WriteLine("failed") +' ^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console#WriteLine(+11). + End Try + End Sub + Private Function [Default]() As (A As String, B As Boolean) ' ^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#Default(). ' documentation ```vb\nPrivate Function Statements.Default() As (A As String, B As Boolean)\n``` diff --git a/snapshots/output-net9.0/syntax/VBMain/Statements.vb b/snapshots/output-net9.0/syntax/VBMain/Statements.vb index 9cc6c9b..52d4b11 100644 --- a/snapshots/output-net9.0/syntax/VBMain/Statements.vb +++ b/snapshots/output-net9.0/syntax/VBMain/Statements.vb @@ -32,6 +32,29 @@ End Try End Sub + Private Sub TryWithoutExceptionVariable(fileName As String) +' ^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable(). +' documentation ```vb\nPrivate Sub Statements.TryWithoutExceptionVariable(fileName As String)\n``` +' ^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' documentation ```vb\nfileName As String\n``` + Try + File.ReadLines(fileName) +' ^^^^ reference scip-dotnet nuget System.Runtime 9.0.0.0 IO/File# +' ^^^^^^^^^ reference scip-dotnet nuget System.Runtime 9.0.0.0 IO/File#ReadLines(). +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) + Catch When fileName.Length = 0 +' ^^^^^^^^ reference scip-dotnet nuget . . VBMain/Statements#TryWithoutExceptionVariable().(fileName) +' ^^^^^^ reference scip-dotnet nuget System.Runtime 9.0.0.0 System/String#Length. + Console.WriteLine("empty file name") +' ^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console#WriteLine(+11). + Catch + Console.WriteLine("failed") +' ^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console# +' ^^^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console#WriteLine(+11). + End Try + End Sub + Private Function [Default]() As (A As String, B As Boolean) ' ^^^^^^^^^ definition scip-dotnet nuget . . VBMain/Statements#Default(). ' documentation ```vb\nPrivate Function Statements.Default() As (A As String, B As Boolean)\n``` From b799950e0d9f8937b3e3e1432b096fb3454c7af2 Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 15 Oct 2026 23:24:57 +0300 Subject: [PATCH 2/2] fix: index a project file beside its solution exactly once With no path given, `scip-dotnet index` takes every solution and project file in the working directory as a root, in the order Directory.GetFiles returns them, which depends on the file system. A project file in the same directory as the solution that includes it was handled in one of two ways: - After the solution (the order seen on Linux): OpenProjectAsync threw "'' is already part of the workspace", which ended the whole index. - Before the solution (the order seen on macOS): opening the solution afterwards loaded the project again under a new ProjectId, which indexedProjects did not recognize, so every document of the project appeared twice in the index. FindSolutionOrProjectFile now lists solutions before project files, each in name order. IndexProject no longer opens a project file that the workspace already holds: it uses the loaded project, which indexedProjects then skips because the solution indexed it, and it skips `dotnet restore` for it. The new project-beside-solution snapshot holds ProjectBesideSolution.sln and ProjectBesideSolution.csproj in one directory. With the new order but without the IndexProject change, its test fails with "'ProjectBesideSolution(net10.0)' is already part of the workspace". --- ScipDotnet/IndexCommandHandler.cs | 7 ++++- ScipDotnet/ScipProjectIndexer.cs | 30 ++++++++++++------- .../input/project-beside-solution/Program.cs | 9 ++++++ .../ProjectBesideSolution.csproj | 10 +++++++ .../ProjectBesideSolution.sln | 22 ++++++++++++++ .../project-beside-solution/Program.cs | 16 ++++++++++ .../project-beside-solution/Program.cs | 16 ++++++++++ .../project-beside-solution/Program.cs | 16 ++++++++++ 8 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 snapshots/input/project-beside-solution/Program.cs create mode 100644 snapshots/input/project-beside-solution/ProjectBesideSolution.csproj create mode 100644 snapshots/input/project-beside-solution/ProjectBesideSolution.sln create mode 100644 snapshots/output-net10.0/project-beside-solution/Program.cs create mode 100644 snapshots/output-net8.0/project-beside-solution/Program.cs create mode 100644 snapshots/output-net9.0/project-beside-solution/Program.cs diff --git a/ScipDotnet/IndexCommandHandler.cs b/ScipDotnet/IndexCommandHandler.cs index f4d8e8c..363f0f4 100644 --- a/ScipDotnet/IndexCommandHandler.cs +++ b/ScipDotnet/IndexCommandHandler.cs @@ -126,7 +126,12 @@ private static List FindSolutionOrProjectFile(FileInfo workingDirector string.Equals(Path.GetExtension(file), ".slnx", StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(file), ".csproj", StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(file), ".vbproj", StringComparison.OrdinalIgnoreCase) - ).ToList(); + ) + // Solutions (.sln/.slnx) before project files, each in name order. Directory.GetFiles returns + // the order of the file system, and a project file must come after the solution that loads it. + .OrderBy(file => Path.GetExtension(file).StartsWith(".sln", StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(file => file, StringComparer.Ordinal) + .ToList(); if (paths.Count != 0) { diff --git a/ScipDotnet/ScipProjectIndexer.cs b/ScipDotnet/ScipProjectIndexer.cs index 1f1debf..d6c7e66 100644 --- a/ScipDotnet/ScipProjectIndexer.cs +++ b/ScipDotnet/ScipProjectIndexer.cs @@ -61,21 +61,29 @@ private void Restore(IndexCommandOptions options, FileInfo project) FileInfo rootProject, HashSet indexedProjects) { - if (!options.SkipDotnetRestore) - { - Restore(options, rootProject); - } - + var workspace = host.Services.GetRequiredService(); var isProjectFile = string.Equals(rootProject.Extension, ".csproj", StringComparison.OrdinalIgnoreCase) || string.Equals(rootProject.Extension, ".vbproj", StringComparison.OrdinalIgnoreCase); - var projects = (isProjectFile - ? new[] + + // A solution in the same directory may have loaded this project file already. Opening it a second time + // throws "'' is already part of the workspace", so the loaded project is used instead, and + // indexedProjects skips it because the solution indexed it. + var projects = isProjectFile + ? workspace.CurrentSolution.Projects + .Where(project => string.Equals(project.FilePath, rootProject.FullName, StringComparison.OrdinalIgnoreCase)) + .ToList() + : new List(); + if (projects.Count == 0) + { + if (!options.SkipDotnetRestore) { - await host.Services.GetRequiredService() - .OpenProjectAsync(rootProject.FullName) + Restore(options, rootProject); } - : (await host.Services.GetRequiredService() - .OpenSolutionAsync(rootProject.FullName)).Projects).ToList(); + + projects = (isProjectFile + ? new[] { await workspace.OpenProjectAsync(rootProject.FullName) } + : (await workspace.OpenSolutionAsync(rootProject.FullName)).Projects).ToList(); + } options.Logger.LogDebug($"Found {projects.Count()} projects"); diff --git a/snapshots/input/project-beside-solution/Program.cs b/snapshots/input/project-beside-solution/Program.cs new file mode 100644 index 0000000..9ca9fad --- /dev/null +++ b/snapshots/input/project-beside-solution/Program.cs @@ -0,0 +1,9 @@ +namespace ProjectBesideSolution; + +public static class Program +{ + public static void Main() + { + Console.WriteLine("Hello, World!"); + } +} diff --git a/snapshots/input/project-beside-solution/ProjectBesideSolution.csproj b/snapshots/input/project-beside-solution/ProjectBesideSolution.csproj new file mode 100644 index 0000000..3e3505f --- /dev/null +++ b/snapshots/input/project-beside-solution/ProjectBesideSolution.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0;net9.0;net8.0 + enable + enable + + + diff --git a/snapshots/input/project-beside-solution/ProjectBesideSolution.sln b/snapshots/input/project-beside-solution/ProjectBesideSolution.sln new file mode 100644 index 0000000..8794bf7 --- /dev/null +++ b/snapshots/input/project-beside-solution/ProjectBesideSolution.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectBesideSolution", "ProjectBesideSolution.csproj", "{96877E88-1D0A-45D0-9C35-E5A0B07783A0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {96877E88-1D0A-45D0-9C35-E5A0B07783A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96877E88-1D0A-45D0-9C35-E5A0B07783A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96877E88-1D0A-45D0-9C35-E5A0B07783A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96877E88-1D0A-45D0-9C35-E5A0B07783A0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/snapshots/output-net10.0/project-beside-solution/Program.cs b/snapshots/output-net10.0/project-beside-solution/Program.cs new file mode 100644 index 0000000..17179df --- /dev/null +++ b/snapshots/output-net10.0/project-beside-solution/Program.cs @@ -0,0 +1,16 @@ + namespace ProjectBesideSolution; +// ^^^^^^^^^^^^^^^^^^^^^ reference scip-dotnet nuget . . ProjectBesideSolution/ + + public static class Program +// ^^^^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program# +// documentation ```cs\nclass Program\n``` + { + public static void Main() +// ^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program#Main(). +// documentation ```cs\npublic static void Program.Main()\n``` + { + Console.WriteLine("Hello, World!"); +// ^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console# +// ^^^^^^^^^ reference scip-dotnet nuget System.Console 10.0.0.0 System/Console#WriteLine(+11). + } + } diff --git a/snapshots/output-net8.0/project-beside-solution/Program.cs b/snapshots/output-net8.0/project-beside-solution/Program.cs new file mode 100644 index 0000000..aa93a12 --- /dev/null +++ b/snapshots/output-net8.0/project-beside-solution/Program.cs @@ -0,0 +1,16 @@ + namespace ProjectBesideSolution; +// ^^^^^^^^^^^^^^^^^^^^^ reference scip-dotnet nuget . . ProjectBesideSolution/ + + public static class Program +// ^^^^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program# +// documentation ```cs\nclass Program\n``` + { + public static void Main() +// ^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program#Main(). +// documentation ```cs\npublic static void Program.Main()\n``` + { + Console.WriteLine("Hello, World!"); +// ^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console# +// ^^^^^^^^^ reference scip-dotnet nuget System.Console 8.0.0.0 System/Console#WriteLine(+11). + } + } diff --git a/snapshots/output-net9.0/project-beside-solution/Program.cs b/snapshots/output-net9.0/project-beside-solution/Program.cs new file mode 100644 index 0000000..191d232 --- /dev/null +++ b/snapshots/output-net9.0/project-beside-solution/Program.cs @@ -0,0 +1,16 @@ + namespace ProjectBesideSolution; +// ^^^^^^^^^^^^^^^^^^^^^ reference scip-dotnet nuget . . ProjectBesideSolution/ + + public static class Program +// ^^^^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program# +// documentation ```cs\nclass Program\n``` + { + public static void Main() +// ^^^^ definition scip-dotnet nuget . . ProjectBesideSolution/Program#Main(). +// documentation ```cs\npublic static void Program.Main()\n``` + { + Console.WriteLine("Hello, World!"); +// ^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console# +// ^^^^^^^^^ reference scip-dotnet nuget System.Console 9.0.0.0 System/Console#WriteLine(+11). + } + }