Repository: dotnet/roslyn
File: src/VisualStudio/Core/Def/ProjectSystem/SdkAnalyzerAssemblyRedirector.cs (C#)

Code before:
using AnalyzerInfo = (string FullPath, string ProductVersion, string PathSuffix);

namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem;

/// <summary>
/// See <see href="https://github.com/dotnet/sdk/blob/main/documentation/general/analyzer-redirecting.md"/>.
/// </summary>
[Export(typeof(IAnalyzerAssemblyRedirector))]
internal sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
{
    private readonly IVsActivityLog? _log;

    private readonly bool _enabled;

    private readonly string? _insertedAnalyzersDirectory;

    /// <summary>
    /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers.
    /// </summary>
    private readonly ImmutableDictionary<string, List<AnalyzerInfo>> _analyzerMap;

    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this(
        Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")),
        serviceProvider.GetServiceOnMainThread<SVsActivityLog, IVsActivityLog>())
    {
    }

    // 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<string, List<AnalyzerInfo>> CreateAnalyzerMap()
    {
        if (!_enabled)
        {
            Log("Analyzer redirecting is disabled.");
            return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
        }

        var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json");
        if (!File.Exists(metadataFilePath))

Review comment (bug):
Eek this is a deadlock waiting to happen. Since this isn't cross-repo anymore, can we adjust the interface to follow VS threading rules? Especially since this just appears for logging, we could easily fetch it asynchronously and write out the log once we get the service.

Code after:

namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem;

/// <summary>
/// See <see href="https://github.com/dotnet/sdk/blob/main/documentation/general/analyzer-redirecting.md"/>.
/// </summary>
[Export(typeof(IAnalyzerAssemblyRedirector))]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : SdkAnalyzerAssemblyRedirectorCore(
    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;
    }
}

/// <summary>
/// Core functionality of <see cref="SdkAnalyzerAssemblyRedirector"/> extracted for testing.
/// </summary>
internal class SdkAnalyzerAssemblyRedirectorCore : IAnalyzerAssemblyRedirector
{
    private readonly IVsActivityLog? _log;

    /// <summary>
    /// Map from analyzer assembly name (file name without extension) to a list of matching analyzers.
    /// </summary>
    private readonly Lazy<ImmutableDictionary<string, List<AnalyzerInfo>>> _analyzerMap;

    public SdkAnalyzerAssemblyRedirectorCore(string? insertedAnalyzersDirectory, IVsActivityLog? log = null)
    {
        _log = log;
        _analyzerMap = new(() => CreateAnalyzerMap(insertedAnalyzersDirectory));
    }

    private ImmutableDictionary<string, List<AnalyzerInfo>> CreateAnalyzerMap(string? insertedAnalyzersDirectory)
    {
        if (insertedAnalyzersDirectory == null)
        {
            Log("Analyzer redirecting is disabled.");
            return ImmutableDictionary<string, List<AnalyzerInfo>>.Empty;
        }