Skip to content

Commit

Permalink
Add analyzer redirecting API (#74820)
Browse files Browse the repository at this point in the history
* Add IAnalyzerAssemblyResolver public API

* Load analyzers directly in ServiceHub

* Handle missing assembly on disk

* Redirect path only for serialization

* Redirect full path on creation

* Add separate redirector API

* Remove superfluous methods

* Report caught exceptions

* Make the API RestrictedInternalsVisibleTo

* Remove unnecessary resolver modification

* Fixup after merge

* Remove redirecting from AnalyzerFileReference

* Redirect analyzers in ProjectSystemProject

* Expand doc comment

* Improve code
  • Loading branch information
jjonescz authored Feb 12, 2025
1 parent 1fa0fb7 commit 3f2e5e2
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
{
// This API returns duplicates of analyzers that support multiple languages.
Expand Down
1 change: 1 addition & 0 deletions src/Compilers/Core/Portable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,7 @@ internal sealed class VisualStudioProjectFactory : IVsTypeScriptVisualStudioProj
private readonly VisualStudioWorkspaceImpl _visualStudioWorkspaceImpl;
private readonly ImmutableArray<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> _dynamicFileInfoProviders;
private readonly IVisualStudioDiagnosticAnalyzerProviderFactory _vsixAnalyzerProviderFactory;
private readonly ImmutableArray<IAnalyzerAssemblyRedirector> _analyzerAssemblyRedirectors;
private readonly IVsService<SVsSolution, IVsSolution2> _solution2;

[ImportingConstructor]
Expand All @@ -42,12 +44,14 @@ public VisualStudioProjectFactory(
VisualStudioWorkspaceImpl visualStudioWorkspaceImpl,
[ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
[ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
IVsService<SVsSolution, IVsSolution2> solution2)
{
_threadingContext = threadingContext;
_visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
_dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
_vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
_analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
_solution2 = solution2;
}

Expand Down Expand Up @@ -93,7 +97,7 @@ public async Task<ProjectSystemProject> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -285,5 +285,43 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
Assert.False(project.HasSdkCodeStyleAnalyzers)
End Using
End Function

<WpfFact>
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

<Export(GetType(IAnalyzerAssemblyRedirector))>
Private Class Redirector
Implements IAnalyzerAssemblyRedirector

<ImportingConstructor, Obsolete(MefConstruction.ImportingConstructorMessage, True)>
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
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.IntelliCode" Partner="Pythia" Key="$(IntelliCodeKey)" />
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.IntelliCode.CSharp" Partner="Pythia" Key="$(IntelliCodeCSharpKey)" />
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.IntelliCode.CSharp.Extraction" Partner="Pythia" Key="$(IntelliCodeCSharpKey)" />
<RestrictedInternalsVisibleTo Include="Microsoft.Net.Sdk.AnalyzerRedirecting" Namespace="Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting" />
</ItemGroup>
<ItemGroup>
<!-- TODO: Remove the below IVTs to CodeStyle Unit test projects once all analyzer/code fix tests are switched to Microsoft.CodeAnalysis.Testing -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Any MEF component implementing this interface will be used to redirect analyzer assemblies.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
internal interface IAnalyzerAssemblyRedirector
{
/// <param name="fullPath">
/// Original full path of the analyzer assembly.
/// </param>
/// <returns>
/// The redirected full path of the analyzer assembly
/// or <see langword="null"/> if this instance cannot redirect the given assembly.
/// </returns>
/// <remarks>
/// <para>
/// If two redirectors return different paths for the same assembly, no redirection will be performed.
/// </para>
/// <para>
/// No thread switching inside this method is allowed.
/// </para>
/// </remarks>
string? RedirectPath(string fullPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1127,9 +1128,43 @@ private OneOrMany<string> 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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> DynamicFileInfoProviders,
IHostDiagnosticAnalyzerProvider HostDiagnosticAnalyzerProvider);
IHostDiagnosticAnalyzerProvider HostDiagnosticAnalyzerProvider,
ImmutableArray<IAnalyzerAssemblyRedirector> AnalyzerAssemblyRedirectors);

0 comments on commit 3f2e5e2

Please sign in to comment.