Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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!
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" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be 100% clear here, this Microsoft.Net.Sdk.AnalyzerRedirecting assembly is something that ships with VS, and is versioned with VS, and does not ship inside the .NET SDK?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is built and versioned from the .NET SDK repo, then inserted into VS and shipped with VS.

</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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a bit here that drives home this happens before shadow copy? Essentially this redirects the path that we pass to the compiler and the compiler will then do as it normally does with the path?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a comment explicitly stating what threading expectations exist here. Since it's not async that means no thread switching would be allowed. (And if it was async, it might still require the stronger guarantee that no thread switching is allowed.)

Per the conversation I had with @jaredpar earlier today, I'm not entirely convinced this interface could actually be implemented safely, especially if to actually implement it you need to fetch any VS service, which would require asynchrony.

/// </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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this ever need to be async? Or is the assumption that any async would be to create the redirector in the first place?

Copy link
Member Author

@jjonescz jjonescz Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't thought about that. Currently, no async is needed. When we need async during creation or redirecting, we can update the code, I think. We have just one implementation and don't expect to have more for now.

}
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 @@ -1126,9 +1127,43 @@ private OneOrMany<string> GetMappedAnalyzerPaths(string fullPath)
return GetMappedRazorSourceGenerator(fullPath);
}

if (TryRedirectAnalyzerAssembly(fullPath) is { } redirectedPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If @jaredpar's work doesn't come first, we should then refactor this method away by converting the IsSdkRazorSourceGenerator/GetMappedRazorSourceGenerator methods into a redirector.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think Jared's work is going to refactor all this and we just want to get this in so we can continue on the SDK side etc.

{
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);