File: SdkAnalyzerAssemblyRedirector.cs
Web Access
Project: ..\..\..\src\Microsoft.Net.Sdk.AnalyzerRedirecting\Microsoft.Net.Sdk.AnalyzerRedirecting.csproj (Microsoft.Net.Sdk.AnalyzerRedirecting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting;
 
// 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.Net.Sdk.AnalyzerRedirecting;
 
[Export(typeof(IAnalyzerAssemblyRedirector))]
public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector
{
    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]
    public SdkAnalyzerAssemblyRedirector()
        : this(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\DotNetRuntimeAnalyzers"))) { }
 
    // Internal for testing.
    internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory)
    {
        _insertedAnalyzersDirectory = insertedAnalyzersDirectory;
        _analyzerMap = CreateAnalyzerMap();
    }
 
    private ImmutableDictionary<string, List<AnalyzerInfo>> CreateAnalyzerMap()
    {
        var builder = ImmutableDictionary.CreateBuilder<string, List<AnalyzerInfo>>(StringComparer.OrdinalIgnoreCase);
 
        // Expects layout like:
        // VsInstallDir\SDK\RuntimeAnalyzers\WindowsDesktopAnalyzers\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll
        //                                   ~~~~~~~~~~~~~~~~~~~~~~~                                                           = topLevelDirectory
        //                                                           ~~~~~                                                     = versionDirectory
        //                                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath
 
        foreach (string topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory))
        {
            foreach (string versionDirectory in Directory.EnumerateDirectories(topLevelDirectory))
            {
                foreach (string analyzerPath in Directory.EnumerateFiles(versionDirectory, "*.dll", SearchOption.AllDirectories))
                {
                    if (!analyzerPath.StartsWith(versionDirectory, StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }
 
                    string version = Path.GetFileName(versionDirectory);
                    string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath);
                    string pathSuffix = analyzerPath.Substring(versionDirectory.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]);
                    }
                }
            }
        }
 
        return builder.ToImmutable();
    }
 
    public string? RedirectPath(string fullPath)
    {
        if (_analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers))
        {
            foreach (AnalyzerInfo 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\"
 
            int index = directoryPath.LastIndexOf(pathSuffix, StringComparison.OrdinalIgnoreCase);
            if (index < 0)
            {
                return false;
            }
 
            string directoryPathVersion = Path.GetFileName(Path.GetDirectoryName(directoryPath.Substring(0, index)));
 
            return areVersionMajorMinorPartEqual(directoryPathVersion, version);
        }
 
        static bool areVersionMajorMinorPartEqual(string version1, string version2)
        {
            int firstDotIndex = version1.IndexOf('.');
            if (firstDotIndex < 0)
            {
                return false;
            }
 
            int secondDotIndex = version1.IndexOf('.', firstDotIndex + 1);
            if (secondDotIndex < 0)
            {
                return false;
            }
 
            return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase);
        }
    }
}