From f204a946b2dab36d74b2b8a34a46f3b5c08e9ade Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Fri, 31 Oct 2025 14:25:44 +0100 Subject: [PATCH 1/9] Add SDK analyzer assembly redirector --- .../SdkAnalyzerAssemblyRedirectorTests.cs | 86 ++++++++ .../SdkAnalyzerAssemblyRedirector.cs | 193 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs create mode 100644 src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs new file mode 100644 index 0000000000000..93a8ce2e88862 --- /dev/null +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using Microsoft.VisualStudio.LanguageServices.ProjectSystem; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Roslyn.VisualStudio.CSharp.UnitTests.ProjectSystemShim; + +public sealed class SdkAnalyzerAssemblyRedirectorTests : TestBase +{ + [Theory] + [InlineData("9.0.0-preview.5.24306.11", "9.0.0-preview.7.24406.2")] + [InlineData("9.0.0-preview.5.24306.11", "9.0.1-preview.7.24406.2")] + [InlineData("9.0.100", "9.0.0-preview.7.24406.2")] + [InlineData("9.0.100", "9.0.200")] + [InlineData("9.0.100", "9.0.101")] + public void SameMajorMinorVersion(string a, string b) + { + var testDir = Temp.CreateDirectory(); + + var vsDir = Path.Combine(testDir.Path, "vs"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + + var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var redirected = resolver.RedirectPath(sdkAnalyzerPath); + AssertEx.Equal(vsAnalyzerPath, redirected); + } + + [Fact] + public void DifferentPathSuffix() + { + var testDir = Temp.CreateDirectory(); + + var vsDir = Path.Combine(testDir.Path, "vs"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); + FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = FakeDll(testDir.Path, @"sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb", "Microsoft.AspNetCore.App.Analyzers"); + + var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var redirected = resolver.RedirectPath(sdkAnalyzerPath); + Assert.Null(redirected); + } + + [Theory] + [InlineData("8.0.100", "9.0.0-preview.7.24406.2")] + [InlineData("9.1.100", "9.0.0-preview.7.24406.2")] + [InlineData("9.1.0-preview.5.24306.11", "9.0.0-preview.7.24406.2")] + [InlineData("9.0.100", "9.1.100")] + [InlineData("9.0.100", "10.0.100")] + [InlineData("9.9.100", "9.10.100")] + public void DifferentMajorMinorVersion(string a, string b) + { + var testDir = Temp.CreateDirectory(); + + var vsDir = Path.Combine(testDir.Path, "vs"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + + var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var redirected = resolver.RedirectPath(sdkAnalyzerPath); + Assert.Null(redirected); + } + + private static string FakeDll(string root, string subdir, string name) + { + var dllPath = Path.Combine(root, subdir, $"{name}.dll"); + Directory.CreateDirectory(Path.GetDirectoryName(dllPath)); + File.WriteAllText(dllPath, ""); + return dllPath; + } + + private static void Metadata(string root, Dictionary versions) + { + var metadataFilePath = Path.Combine(root, "metadata.json"); + Directory.CreateDirectory(Path.GetDirectoryName(metadataFilePath)); + File.WriteAllText(metadataFilePath, JsonSerializer.Serialize(versions)); + } +} diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs new file mode 100644 index 0000000000000..c11a1d961b6ca --- /dev/null +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.Json; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +// Example: +// FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll" +// ProductVersion: "8.0.8" +// PathSuffix: "analyzers\dotnet" +using AnalyzerInfo = (string FullPath, string ProductVersion, string PathSuffix); + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; + +/// +/// See . +/// +[Export(typeof(IAnalyzerAssemblyRedirector))] +internal sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector +{ + private readonly IVsActivityLog? _log; + + private readonly bool _enabled; + + private readonly string? _insertedAnalyzersDirectory; + + /// + /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers. + /// + private readonly ImmutableDictionary> _analyzerMap; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this( + Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), + serviceProvider.GetServiceOnMainThread()) + { + } + + // Internal for testing. + [SuppressMessage("RoslynDiagnosticsReliability", "RS0034: Exported parts should have a public constructor marked with 'ImportingConstructorAttribute'", + Justification = "This is an internal constructor exposed for testing and delegated to by the public importing constructor")] + internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) + { + _log = log; + var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); + _enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase); + _insertedAnalyzersDirectory = insertedAnalyzersDirectory; + _analyzerMap = CreateAnalyzerMap(); + } + + private ImmutableDictionary> CreateAnalyzerMap() + { + if (!_enabled) + { + Log("Analyzer redirecting is disabled."); + return ImmutableDictionary>.Empty; + } + + var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json"); + if (!File.Exists(metadataFilePath)) + { + Log($"File does not exist: {metadataFilePath}"); + return ImmutableDictionary>.Empty; + } + + var versions = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); + if (versions is null || versions.Count == 0) + { + Log($"Versions are empty: {metadataFilePath}"); + return ImmutableDictionary>.Empty; + } + + var builder = ImmutableDictionary.CreateBuilder>(StringComparer.OrdinalIgnoreCase); + + // Expects layout like: + // VsInstallDir\DotNetRuntimeAnalyzers\WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll + // ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath + + foreach (var topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory)) + { + foreach (var analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories)) + { + if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var subsetName = Path.GetFileName(topLevelDirectory); + if (!versions.TryGetValue(subsetName, out var version)) + { + continue; + } + + var analyzerName = Path.GetFileNameWithoutExtension(analyzerPath); + var pathSuffix = analyzerPath.Substring(topLevelDirectory.Length + 1 /* slash */); + pathSuffix = Path.GetDirectoryName(pathSuffix); + + AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix }; + + if (builder.TryGetValue(analyzerName, out var existing)) + { + existing.Add(analyzer); + } + else + { + builder.Add(analyzerName, [analyzer]); + } + } + } + + Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}"); + + return builder.ToImmutable(); + } + + public string? RedirectPath(string fullPath) + { + if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) + { + foreach (var analyzer in analyzers) + { + var directoryPath = Path.GetDirectoryName(fullPath); + + // Note that both paths we compare here are normalized via netfx's Path.GetDirectoryName. + if (directoryPath.EndsWith(analyzer.PathSuffix, StringComparison.OrdinalIgnoreCase) && + MajorAndMinorVersionsMatch(directoryPath, analyzer.PathSuffix, analyzer.ProductVersion)) + { + return analyzer.FullPath; + } + } + } + + return null; + + static bool MajorAndMinorVersionsMatch(string directoryPath, string pathSuffix, string version) + { + // Find the version number in the directory path - it is in the directory name before the path suffix. + // Example: + // "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\" = directoryPath + // ~~~~~~~~~~~~~~~~ = pathSuffix + // ~~~~~ = directoryPathVersion + // This can match also a NuGet package because the version number is at the same position: + // "C:\.nuget\packages\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\" + + var index = directoryPath.LastIndexOf(pathSuffix, StringComparison.OrdinalIgnoreCase); + if (index < 0) + { + return false; + } + + var directoryPathVersion = Path.GetFileName(Path.GetDirectoryName(directoryPath.Substring(0, index))); + + return AreVersionMajorMinorPartEqual(directoryPathVersion, version); + } + + static bool AreVersionMajorMinorPartEqual(string version1, string version2) + { + var firstDotIndex = version1.IndexOf('.'); + if (firstDotIndex < 0) + { + return false; + } + + var secondDotIndex = version1.IndexOf('.', firstDotIndex + 1); + if (secondDotIndex < 0) + { + return false; + } + + return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase); + } + } + + private void Log(string message) + { + _log?.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, + "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), + message); + } +} From ada02f56e1811db1c6368ae3c038294dc1f15935 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 10 Nov 2025 14:52:56 +0100 Subject: [PATCH 2/9] Extract class for testing --- .../SdkAnalyzerAssemblyRedirectorTests.cs | 6 ++--- .../SdkAnalyzerAssemblyRedirector.cs | 25 ++++++++----------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs index 93a8ce2e88862..f82db4de040a5 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -28,7 +28,7 @@ public void SameMajorMinorVersion(string a, string b) var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); - var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); AssertEx.Equal(vsAnalyzerPath, redirected); } @@ -43,7 +43,7 @@ public void DifferentPathSuffix() FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @"sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb", "Microsoft.AspNetCore.App.Analyzers"); - var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); Assert.Null(redirected); } @@ -64,7 +64,7 @@ public void DifferentMajorMinorVersion(string a, string b) FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); - var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); + var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); Assert.Null(redirected); } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index c11a1d961b6ca..106fdc7d029c6 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json; using Microsoft.CodeAnalysis.Host.Mef; @@ -26,7 +25,16 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; /// See . /// [Export(typeof(IAnalyzerAssemblyRedirector))] -internal sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore( + Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), + serviceProvider.GetServiceOnMainThread()); + +/// +/// Core functionality of extracted for testing. +/// +internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector { private readonly IVsActivityLog? _log; @@ -39,18 +47,7 @@ internal sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirecto /// private readonly ImmutableDictionary> _analyzerMap; - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this( - Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), - serviceProvider.GetServiceOnMainThread()) - { - } - - // Internal for testing. - [SuppressMessage("RoslynDiagnosticsReliability", "RS0034: Exported parts should have a public constructor marked with 'ImportingConstructorAttribute'", - Justification = "This is an internal constructor exposed for testing and delegated to by the public importing constructor")] - internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) + public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) { _log = log; var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); From 67e5b883f398fee3c10b8b09b6d9ca0f3b0e2720 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 10 Nov 2025 15:09:40 +0100 Subject: [PATCH 3/9] Log asynchronously --- .../SdkAnalyzerAssemblyRedirector.cs | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index 106fdc7d029c6..fa45c5a09ccab 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -8,10 +8,14 @@ using System.ComponentModel.Composition; using System.IO; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Threading; // Example: // FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll" @@ -27,16 +31,21 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; [Export(typeof(IAnalyzerAssemblyRedirector))] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore( - Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), - serviceProvider.GetServiceOnMainThread()); +internal sealed class SdkAnalyzerAssemblyRedirector( + SVsServiceProvider serviceProvider, + IThreadingContext threadingContext) + : SdkAnalyzerAssemblyRedirectorCore( + Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), + () => serviceProvider.GetService(threadingContext.JoinableTaskFactory)); /// /// Core functionality of extracted for testing. /// internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector { - private readonly IVsActivityLog? _log; + private readonly Func? _logFactory; + + private AsyncQueue? _logQueue; private readonly bool _enabled; @@ -45,15 +54,15 @@ internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector /// /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers. /// - private readonly ImmutableDictionary> _analyzerMap; + private readonly Lazy>> _analyzerMap; - public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) + public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, Func? logFactory = null) { - _log = log; + _logFactory = logFactory; var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); _enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase); _insertedAnalyzersDirectory = insertedAnalyzersDirectory; - _analyzerMap = CreateAnalyzerMap(); + _analyzerMap = new(CreateAnalyzerMap); } private ImmutableDictionary> CreateAnalyzerMap() @@ -119,12 +128,14 @@ private ImmutableDictionary> CreateAnalyzerMap() Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}"); + _logQueue?.Complete(); + return builder.ToImmutable(); } public string? RedirectPath(string fullPath) { - if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) + if (_enabled && _analyzerMap.Value.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) { foreach (var analyzer in analyzers) { @@ -182,9 +193,27 @@ static bool AreVersionMajorMinorPartEqual(string version1, string version2) private void Log(string message) { - _log?.LogEntry( - (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, - "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), - message); + if (_logFactory is null) + { + return; + } + + if (Interlocked.CompareExchange(ref _logQueue, new(), null) == null) + { + Task.Run(async () => + { + var log = _logFactory(); + while (true) + { + var message = await _logQueue.DequeueAsync().ConfigureAwait(false); + log.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, + "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), + message); + } + }); + } + + _logQueue.Enqueue(message); } } From 4d4178102a961b7e7ca9a78a9e6f80c1b3f48706 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Thu, 18 Dec 2025 13:38:06 +0100 Subject: [PATCH 4/9] Revert "Log asynchronously" This reverts commit 67e5b883f398fee3c10b8b09b6d9ca0f3b0e2720. --- .../SdkAnalyzerAssemblyRedirector.cs | 55 +++++-------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index fa45c5a09ccab..106fdc7d029c6 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -8,14 +8,10 @@ using System.ComponentModel.Composition; using System.IO; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.Threading; // Example: // FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll" @@ -31,21 +27,16 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; [Export(typeof(IAnalyzerAssemblyRedirector))] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class SdkAnalyzerAssemblyRedirector( - SVsServiceProvider serviceProvider, - IThreadingContext threadingContext) - : SdkAnalyzerAssemblyRedirectorCore( - Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), - () => serviceProvider.GetService(threadingContext.JoinableTaskFactory)); +internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore( + Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), + serviceProvider.GetServiceOnMainThread()); /// /// Core functionality of extracted for testing. /// internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector { - private readonly Func? _logFactory; - - private AsyncQueue? _logQueue; + private readonly IVsActivityLog? _log; private readonly bool _enabled; @@ -54,15 +45,15 @@ internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector /// /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers. /// - private readonly Lazy>> _analyzerMap; + private readonly ImmutableDictionary> _analyzerMap; - public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, Func? logFactory = null) + public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) { - _logFactory = logFactory; + _log = log; var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); _enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase); _insertedAnalyzersDirectory = insertedAnalyzersDirectory; - _analyzerMap = new(CreateAnalyzerMap); + _analyzerMap = CreateAnalyzerMap(); } private ImmutableDictionary> CreateAnalyzerMap() @@ -128,14 +119,12 @@ private ImmutableDictionary> CreateAnalyzerMap() Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}"); - _logQueue?.Complete(); - return builder.ToImmutable(); } public string? RedirectPath(string fullPath) { - if (_enabled && _analyzerMap.Value.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) + if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) { foreach (var analyzer in analyzers) { @@ -193,27 +182,9 @@ static bool AreVersionMajorMinorPartEqual(string version1, string version2) private void Log(string message) { - if (_logFactory is null) - { - return; - } - - if (Interlocked.CompareExchange(ref _logQueue, new(), null) == null) - { - Task.Run(async () => - { - var log = _logFactory(); - while (true) - { - var message = await _logQueue.DequeueAsync().ConfigureAwait(false); - log.LogEntry( - (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, - "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), - message); - } - }); - } - - _logQueue.Enqueue(message); + _log?.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, + "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), + message); } } From 188e55d9cdfc56bce800f83c08bea9858a5924a1 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Thu, 18 Dec 2025 13:38:57 +0100 Subject: [PATCH 5/9] Fix GetService call --- .../Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index 106fdc7d029c6..7d7c076638c78 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore( Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), - serviceProvider.GetServiceOnMainThread()); + (IVsActivityLog)serviceProvider.GetService(typeof(SVsActivityLog))); /// /// Core functionality of extracted for testing. From 4e42350b8eca43491df82071c6de8847e4062ea3 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Fri, 19 Dec 2025 13:26:18 +0100 Subject: [PATCH 6/9] Improve code --- .../SdkAnalyzerAssemblyRedirectorTests.cs | 2 + .../SdkAnalyzerAssemblyRedirector.cs | 63 ++++++++++++------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs index f82db4de040a5..d39784bb5e9db 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -55,6 +55,8 @@ public void DifferentPathSuffix() [InlineData("9.0.100", "9.1.100")] [InlineData("9.0.100", "10.0.100")] [InlineData("9.9.100", "9.10.100")] + [InlineData("111.111.0", "1.1.0")] + [InlineData("1.1.0", "111.111.0")] public void DifferentMajorMinorVersion(string a, string b) { var testDir = Temp.CreateDirectory(); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index 7d7c076638c78..9db9c83fe1110 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; +using System.Diagnostics; using System.IO; using System.Text.Json; using Microsoft.CodeAnalysis.Host.Mef; @@ -28,8 +29,20 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore( - Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), - (IVsActivityLog)serviceProvider.GetService(typeof(SVsActivityLog))); + GetInsertedAnalyzersDirectory(), + (IVsActivityLog)serviceProvider.GetService(typeof(SVsActivityLog))) +{ + private static string? GetInsertedAnalyzersDirectory() + { + var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); + if (!"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase)) + { + return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")); + } + + return null; + } +} /// /// Core functionality of extracted for testing. @@ -38,43 +51,47 @@ internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector { private readonly IVsActivityLog? _log; - private readonly bool _enabled; - - private readonly string? _insertedAnalyzersDirectory; - /// /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers. /// - private readonly ImmutableDictionary> _analyzerMap; + private readonly Lazy>> _analyzerMap; public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) { _log = log; - var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); - _enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase); - _insertedAnalyzersDirectory = insertedAnalyzersDirectory; - _analyzerMap = CreateAnalyzerMap(); + _analyzerMap = new(() => CreateAnalyzerMap(insertedAnalyzersDirectory)); } - private ImmutableDictionary> CreateAnalyzerMap() + private ImmutableDictionary> CreateAnalyzerMap(string? insertedAnalyzersDirectory) { - if (!_enabled) + if (insertedAnalyzersDirectory == null) { Log("Analyzer redirecting is disabled."); return ImmutableDictionary>.Empty; } - var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json"); + var metadataFilePath = Path.Combine(insertedAnalyzersDirectory, "metadata.json"); if (!File.Exists(metadataFilePath)) { Log($"File does not exist: {metadataFilePath}"); return ImmutableDictionary>.Empty; } - var versions = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); + Dictionary? versions; + + try + { + versions = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); + } + catch (Exception ex) + { + Log($"Failed to read or parse metadata file '{metadataFilePath}'. {ex}", __ACTIVITYLOG_ENTRYTYPE.ALE_WARNING); + return ImmutableDictionary>.Empty; + } + if (versions is null || versions.Count == 0) { - Log($"Versions are empty: {metadataFilePath}"); + Log($"Versions are empty: {metadataFilePath}", __ACTIVITYLOG_ENTRYTYPE.ALE_WARNING); return ImmutableDictionary>.Empty; } @@ -85,16 +102,18 @@ private ImmutableDictionary> CreateAnalyzerMap() // ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath - foreach (var topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory)) + foreach (var topLevelDirectory in Directory.EnumerateDirectories(insertedAnalyzersDirectory)) { + var subsetName = Path.GetFileName(topLevelDirectory); + foreach (var analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories)) { if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase)) { + Debug.Assert(false); continue; } - var subsetName = Path.GetFileName(topLevelDirectory); if (!versions.TryGetValue(subsetName, out var version)) { continue; @@ -117,14 +136,14 @@ private ImmutableDictionary> CreateAnalyzerMap() } } - Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}"); + Log($"Loaded analyzer map ({builder.Count}): {insertedAnalyzersDirectory}"); return builder.ToImmutable(); } public string? RedirectPath(string fullPath) { - if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) + if (_analyzerMap.Value.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) { foreach (var analyzer in analyzers) { @@ -180,10 +199,10 @@ static bool AreVersionMajorMinorPartEqual(string version1, string version2) } } - private void Log(string message) + private void Log(string message, __ACTIVITYLOG_ENTRYTYPE level = __ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION) { _log?.LogEntry( - (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, + (uint)level, "Roslyn" + nameof(SdkAnalyzerAssemblyRedirector), message); } From 68e5ea83d985d2fd86c3a5b8a96df12387d5cb32 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 22 Dec 2025 11:28:19 +0100 Subject: [PATCH 7/9] Improve code and tests --- .../SdkAnalyzerAssemblyRedirectorTests.cs | 42 +++++++++++++++++-- .../SdkAnalyzerAssemblyRedirector.cs | 3 +- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs index d39784bb5e9db..304b256445730 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -26,7 +26,7 @@ public void SameMajorMinorVersion(string a, string b) var vsDir = Path.Combine(testDir.Path, "vs"); Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); - var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = @$"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"; var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); @@ -41,13 +41,49 @@ public void DifferentPathSuffix() var vsDir = Path.Combine(testDir.Path, "vs"); Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); - var sdkAnalyzerPath = FakeDll(testDir.Path, @"sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = @"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb\Microsoft.AspNetCore.App.Analyzers.dll"; var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); Assert.Null(redirected); } + [Fact] + public void DifferentPathSuffix_NoParentDirectory() + { + var testDir = Temp.CreateDirectory(); + + var vsDir = Path.Combine(testDir.Path, "vs"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); + var vsAnalyzerPath = FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + + // The suffix matches but there is no parent directory. + var sdkAnalyzerPath = @"\sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"; + + var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); + var redirected = resolver.RedirectPath(sdkAnalyzerPath); + AssertEx.Equal(vsAnalyzerPath, redirected); + } + + [Fact] + public void TwoMajorVersions() + { + var testDir = Temp.CreateDirectory(); + + var vsDir = Path.Combine(testDir.Path, "vs"); + Metadata(vsDir, new() + { + { "AspNetCoreAnalyzers9", "9.0.0-preview.5.24306.11" }, + { "AspNetCoreAnalyzers10", "10.0.0-preview.5.24306.11" }, + }); + var vsAnalyzerPath9 = FakeDll(vsDir, @"AspNetCoreAnalyzers9\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var vsAnalyzerPath10 = FakeDll(vsDir, @"AspNetCoreAnalyzers10\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + + var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); + AssertEx.Equal(vsAnalyzerPath9, resolver.RedirectPath(@"Z:\sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll")); + AssertEx.Equal(vsAnalyzerPath10, resolver.RedirectPath(@"Z:\sdk\packs\Microsoft.AspNetCore.App.Ref\10.0.0-preview.7.24406.2\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll")); + } + [Theory] [InlineData("8.0.100", "9.0.0-preview.7.24406.2")] [InlineData("9.1.100", "9.0.0-preview.7.24406.2")] @@ -64,7 +100,7 @@ public void DifferentMajorMinorVersion(string a, string b) var vsDir = Path.Combine(testDir.Path, "vs"); Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); - var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + var sdkAnalyzerPath = @$"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"; var resolver = new SdkAnalyzerAssemblyRedirectorCore(vsDir); var redirected = resolver.RedirectPath(sdkAnalyzerPath); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index 9db9c83fe1110..fbfec227d4e95 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -110,7 +110,8 @@ private ImmutableDictionary> CreateAnalyzerMap(string { if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase)) { - Debug.Assert(false); + // We can't continue, because computations in code below assume this. + Debug.Fail($"Analyzer path '{analyzerPath}' must start with '{topLevelDirectory}'."); continue; } From 64ed67b7959416101f3bec282780059efb138922 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 6 Jan 2026 13:29:47 +0100 Subject: [PATCH 8/9] Obtain analyzer files from metadata --- .../SdkAnalyzerAssemblyRedirectorTests.cs | 16 +++---- .../SdkAnalyzerAssemblyRedirector.cs | 46 +++++++++---------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs index 304b256445730..82a7ef5acc213 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -24,7 +24,7 @@ public void SameMajorMinorVersion(string a, string b) var testDir = Temp.CreateDirectory(); var vsDir = Path.Combine(testDir.Path, "vs"); - Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", new() { Version = a, Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } } }); var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = @$"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"; @@ -39,7 +39,7 @@ public void DifferentPathSuffix() var testDir = Temp.CreateDirectory(); var vsDir = Path.Combine(testDir.Path, "vs"); - Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", new() { Version = "9.0.0-preview.5.24306.11", Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } } }); FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = @"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb\Microsoft.AspNetCore.App.Analyzers.dll"; @@ -54,7 +54,7 @@ public void DifferentPathSuffix_NoParentDirectory() var testDir = Temp.CreateDirectory(); var vsDir = Path.Combine(testDir.Path, "vs"); - Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", new() { Version = "9.0.0-preview.5.24306.11", Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } } }); var vsAnalyzerPath = FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); // The suffix matches but there is no parent directory. @@ -73,8 +73,8 @@ public void TwoMajorVersions() var vsDir = Path.Combine(testDir.Path, "vs"); Metadata(vsDir, new() { - { "AspNetCoreAnalyzers9", "9.0.0-preview.5.24306.11" }, - { "AspNetCoreAnalyzers10", "10.0.0-preview.5.24306.11" }, + { "AspNetCoreAnalyzers9", new() { Version = "9.0.0-preview.5.24306.11", Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } }, + { "AspNetCoreAnalyzers10", new() { Version = "10.0.0-preview.5.24306.11", Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } }, }); var vsAnalyzerPath9 = FakeDll(vsDir, @"AspNetCoreAnalyzers9\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var vsAnalyzerPath10 = FakeDll(vsDir, @"AspNetCoreAnalyzers10\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); @@ -98,7 +98,7 @@ public void DifferentMajorMinorVersion(string a, string b) var testDir = Temp.CreateDirectory(); var vsDir = Path.Combine(testDir.Path, "vs"); - Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", new() { Version = a, Files = [@"analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"] } } }); FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = @$"Z:\Program Files\dotnet\sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll"; @@ -115,10 +115,10 @@ private static string FakeDll(string root, string subdir, string name) return dllPath; } - private static void Metadata(string root, Dictionary versions) + private static void Metadata(string root, Dictionary metadata) { var metadataFilePath = Path.Combine(root, "metadata.json"); Directory.CreateDirectory(Path.GetDirectoryName(metadataFilePath)); - File.WriteAllText(metadataFilePath, JsonSerializer.Serialize(versions)); + File.WriteAllText(metadataFilePath, JsonSerializer.Serialize(metadata)); } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index fbfec227d4e95..60b657c71b168 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; -using System.Diagnostics; using System.IO; using System.Text.Json; using Microsoft.CodeAnalysis.Host.Mef; @@ -77,11 +76,11 @@ private ImmutableDictionary> CreateAnalyzerMap(string return ImmutableDictionary>.Empty; } - Dictionary? versions; + Dictionary? metadata; try { - versions = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); + metadata = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); } catch (Exception ex) { @@ -89,9 +88,9 @@ private ImmutableDictionary> CreateAnalyzerMap(string return ImmutableDictionary>.Empty; } - if (versions is null || versions.Count == 0) + if (metadata is null || metadata.Count == 0) { - Log($"Versions are empty: {metadataFilePath}", __ACTIVITYLOG_ENTRYTYPE.ALE_WARNING); + Log($"Metadata dictionary is empty: {metadataFilePath}", __ACTIVITYLOG_ENTRYTYPE.ALE_WARNING); return ImmutableDictionary>.Empty; } @@ -99,32 +98,23 @@ private ImmutableDictionary> CreateAnalyzerMap(string // Expects layout like: // VsInstallDir\DotNetRuntimeAnalyzers\WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll - // ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath + // ~~~~~~~~~~~~~~~~~~~~~~~ = subsetName + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = pathSuffix - foreach (var topLevelDirectory in Directory.EnumerateDirectories(insertedAnalyzersDirectory)) + foreach (var (subsetName, entry) in metadata) { - var subsetName = Path.GetFileName(topLevelDirectory); - - foreach (var analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories)) + foreach (var file in entry.Files) { - if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase)) - { - // We can't continue, because computations in code below assume this. - Debug.Fail($"Analyzer path '{analyzerPath}' must start with '{topLevelDirectory}'."); - continue; - } + var analyzerFullPath = Path.Combine(insertedAnalyzersDirectory, subsetName, file); - if (!versions.TryGetValue(subsetName, out var version)) + AnalyzerInfo analyzer = new() { - continue; - } + FullPath = analyzerFullPath, + ProductVersion = entry.Version, + PathSuffix = Path.GetDirectoryName(file), + }; - var analyzerName = Path.GetFileNameWithoutExtension(analyzerPath); - var pathSuffix = analyzerPath.Substring(topLevelDirectory.Length + 1 /* slash */); - pathSuffix = Path.GetDirectoryName(pathSuffix); - - AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix }; + var analyzerName = Path.GetFileNameWithoutExtension(file); if (builder.TryGetValue(analyzerName, out var existing)) { @@ -208,3 +198,9 @@ private void Log(string message, __ACTIVITYLOG_ENTRYTYPE level = __ACTIVITYLOG_E message); } } + +internal sealed class MetadataEntry +{ + public required string Version { get; init; } + public ImmutableArray Files { get; init; } +} From 9a8b74832021739c44150a5cdfe255016d3f8e59 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 12 Jan 2026 12:05:32 +0100 Subject: [PATCH 9/9] Fix version comparsion logic --- .../ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs | 2 ++ .../Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs index 82a7ef5acc213..9d95f6ce2267c 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/SdkAnalyzerAssemblyRedirectorTests.cs @@ -86,6 +86,8 @@ public void TwoMajorVersions() [Theory] [InlineData("8.0.100", "9.0.0-preview.7.24406.2")] + [InlineData("8.0.1", "8.01.1")] + [InlineData("8.01.1", "8.0.1")] [InlineData("9.1.100", "9.0.0-preview.7.24406.2")] [InlineData("9.1.0-preview.5.24306.11", "9.0.0-preview.7.24406.2")] [InlineData("9.0.100", "9.1.100")] diff --git a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs index 60b657c71b168..76bb89ff96a6c 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs @@ -186,7 +186,7 @@ static bool AreVersionMajorMinorPartEqual(string version1, string version2) return false; } - return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase); + return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex + 1, StringComparison.OrdinalIgnoreCase); } }