diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs index fbbd8f4553645..2f1eafe06b16d 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs @@ -106,6 +106,9 @@ public bool Equals(AnalyzerReference? other) public override int GetHashCode() => Hash.Combine(RuntimeHelpers.GetHashCode(_assemblyLoader), FullPath.GetHashCode()); + public override string ToString() + => $"{nameof(AnalyzerFileReference)}({nameof(FullPath)} = {FullPath})"; + public override ImmutableArray GetAnalyzersForAllLanguages() { // This API returns duplicates of analyzers that support multiple languages. diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index e507230349b2a..139e0b0ce1df8 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -17,6 +17,7 @@ Microsoft.CodeAnalysis.IPropertySymbol.PartialImplementationPart.get -> Microsof Microsoft.CodeAnalysis.IPropertySymbol.IsPartialDefinition.get -> bool Microsoft.CodeAnalysis.ITypeParameterSymbol.AllowsRefLikeType.get -> bool Microsoft.CodeAnalysis.RuntimeCapability.ByRefLikeGenerics = 8 -> Microsoft.CodeAnalysis.RuntimeCapability +override Microsoft.CodeAnalysis.Diagnostics.AnalyzerFileReference.ToString() -> string! static Microsoft.CodeAnalysis.GeneratorExtensions.AsIncrementalGenerator(this Microsoft.CodeAnalysis.ISourceGenerator! sourceGenerator) -> Microsoft.CodeAnalysis.IIncrementalGenerator! static Microsoft.CodeAnalysis.GeneratorExtensions.GetGeneratorType(this Microsoft.CodeAnalysis.IIncrementalGenerator! generator) -> System.Type! Microsoft.CodeAnalysis.Compilation.CreatePreprocessingSymbol(string! name) -> Microsoft.CodeAnalysis.IPreprocessingSymbol! diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs index 9822e7020a5ab..f62d05679f3a6 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs @@ -43,7 +43,8 @@ public LanguageServerWorkspaceFactory( var razorSourceGenerator = serverConfigurationFactory?.ServerConfiguration?.RazorSourceGenerator; ProjectSystemHostInfo = new ProjectSystemHostInfo( DynamicFileInfoProviders: [.. dynamicFileInfoProviders], - new HostDiagnosticAnalyzerProvider(razorSourceGenerator)); + new HostDiagnosticAnalyzerProvider(razorSourceGenerator), + AnalyzerAssemblyRedirectors: []); TargetFrameworkManager = projectTargetFrameworkManager; } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs index 620d493be4cef..92da315daac29 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.ExternalAccess.VSTypeScript.Api; using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics; @@ -33,6 +34,7 @@ internal sealed class VisualStudioProjectFactory : IVsTypeScriptVisualStudioProj private readonly VisualStudioWorkspaceImpl _visualStudioWorkspaceImpl; private readonly ImmutableArray> _dynamicFileInfoProviders; private readonly IVisualStudioDiagnosticAnalyzerProviderFactory _vsixAnalyzerProviderFactory; + private readonly ImmutableArray _analyzerAssemblyRedirectors; private readonly IVsService _solution2; [ImportingConstructor] @@ -42,12 +44,14 @@ public VisualStudioProjectFactory( VisualStudioWorkspaceImpl visualStudioWorkspaceImpl, [ImportMany] IEnumerable> fileInfoProviders, IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory, + [ImportMany] IEnumerable analyzerAssemblyRedirectors, IVsService solution2) { _threadingContext = threadingContext; _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl; _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty(); _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory; + _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty(); _solution2 = solution2; } @@ -93,7 +97,7 @@ public async Task CreateAndAddToWorkspaceAsync( _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath; _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId(); - var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider); + var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors); var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo); _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName); diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb index 6d9f10df1256e..c0d0b85ee28c5 100644 --- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb +++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb @@ -2,13 +2,13 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. -Imports System.Collections.Immutable +Imports System.ComponentModel.Composition Imports System.IO Imports System.Threading Imports Microsoft.CodeAnalysis -Imports Microsoft.CodeAnalysis.Diagnostics -Imports Microsoft.CodeAnalysis.[Shared].TestHooks +Imports Microsoft.CodeAnalysis.Host.Mef Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting Imports Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics Imports Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework @@ -285,5 +285,43 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Assert.False(project.HasSdkCodeStyleAnalyzers) End Using End Function + + + Public Async Function RedirectedAnalyzers_CSharp() As Task + Using environment = New TestEnvironment(GetType(Redirector)) + Dim project = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync( + "Project", LanguageNames.CSharp, CancellationToken.None) + + ' Add analyzers + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk", "analyzers", "Microsoft.CodeAnalysis.NetAnalyzers.dll")) + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk", "analyzers", "Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll")) + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Dir", "File.dll")) + + ' Ensure the SDK ones are redirected + AssertEx.Equal( + { + Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk", "analyzers", "Microsoft.CodeAnalysis.NetAnalyzers.redirected.dll"), + Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk", "analyzers", "Microsoft.CodeAnalysis.CSharp.NetAnalyzers.redirected.dll"), + Path.Combine(TempRoot.Root, "Dir", "File.dll") + }, environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences.Select(Function(r) r.FullPath)) + End Using + End Function + + + Private Class Redirector + Implements IAnalyzerAssemblyRedirector + + + Public Sub New() + End Sub + + Public Function RedirectPath(fullPath As String) As String Implements IAnalyzerAssemblyRedirector.RedirectPath + If fullPath.Contains("Microsoft.NET.Sdk") Then + Return Path.ChangeExtension(fullPath, ".redirected.dll") + End If + + Return Nothing + End Function + End Class End Class End Namespace diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index 7ddf1ce681c17..3006e33195758 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -146,6 +146,7 @@ + diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IAnalyzerAssemblyRedirector.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IAnalyzerAssemblyRedirector.cs new file mode 100644 index 0000000000000..c1881e5ccc073 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IAnalyzerAssemblyRedirector.cs @@ -0,0 +1,33 @@ +// 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. + +namespace Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; + +/// +/// Any MEF component implementing this interface will be used to redirect analyzer assemblies. +/// +/// +/// The redirected path is passed to the compiler where it is processed in the standard way, +/// e.g., the redirected assembly is shadow copied before it's loaded +/// (this could be improved in the future since shadow copying redirected assemblies is usually unnecessary). +/// +internal interface IAnalyzerAssemblyRedirector +{ + /// + /// Original full path of the analyzer assembly. + /// + /// + /// The redirected full path of the analyzer assembly + /// or if this instance cannot redirect the given assembly. + /// + /// + /// + /// If two redirectors return different paths for the same assembly, no redirection will be performed. + /// + /// + /// No thread switching inside this method is allowed. + /// + /// + string? RedirectPath(string fullPath); +} diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 5fcbed2ccaeb0..388643fe3325a 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.PooledObjects; @@ -1126,9 +1127,43 @@ private OneOrMany GetMappedAnalyzerPaths(string fullPath) return GetMappedRazorSourceGenerator(fullPath); } + if (TryRedirectAnalyzerAssembly(fullPath) is { } redirectedPath) + { + return OneOrMany.Create(redirectedPath); + } + return OneOrMany.Create(fullPath); } + private string? TryRedirectAnalyzerAssembly(string fullPath) + { + string? redirectedPath = null; + + foreach (var redirector in _hostInfo.AnalyzerAssemblyRedirectors) + { + try + { + if (redirector.RedirectPath(fullPath) is { } currentlyRedirectedPath) + { + if (redirectedPath == null) + { + redirectedPath = currentlyRedirectedPath; + } + else if (redirectedPath != currentlyRedirectedPath) + { + throw new InvalidOperationException($"Multiple redirectors disagree on the path to redirect '{fullPath}' to ('{redirectedPath}' vs '{currentlyRedirectedPath}')."); + } + } + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.General)) + { + // Ignore if the external redirector throws. + } + } + + return redirectedPath; + } + private static readonly string s_csharpCodeStyleAnalyzerSdkDirectory = CreateDirectoryPathFragment("Sdks", "Microsoft.NET.Sdk", "codestyle", "cs"); private static readonly string s_visualBasicCodeStyleAnalyzerSdkDirectory = CreateDirectoryPathFragment("Sdks", "Microsoft.NET.Sdk", "codestyle", "vb"); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectHostInfo.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectHostInfo.cs index fc3f0a58ef18a..5bb5f96513091 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectHostInfo.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectHostInfo.cs @@ -6,9 +6,11 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem; internal record ProjectSystemHostInfo( ImmutableArray> DynamicFileInfoProviders, - IHostDiagnosticAnalyzerProvider HostDiagnosticAnalyzerProvider); + IHostDiagnosticAnalyzerProvider HostDiagnosticAnalyzerProvider, + ImmutableArray AnalyzerAssemblyRedirectors);