Repository: dotnet/roslyn
Pull request #78116: Fix razor analyzer loading
Author: login: chsienki
Labels: name: Area-Analyzers
name: untriaged
description: Issues and PRs which have not yet been triaged by a lead
name: VSCode
Created: 2025-04-11T17:07:22Z | Merged: 2025-04-22T17:17:26Z

Today Razor lists the compiler/source generator as a VSIX analyzer. Roslyn uses this information to find the location of the razor compiler assembly so that it can redirect the SDK copy to the copy shipped in the Razor VSIX.

Previously this was ok, as the assemblies only contained a source generator, not any analyzers, so listing them as VSIX analyzers was essentially a NOP. As of https://github.com/dotnet/razor/pull/11601 however, the assemblies now contain a diagnostic suppressor. Because it's listed as a VSIX analyzer, VS now loads the suppressor in all scenarios, even if razor isn't involved.


This change (along with the matching razor one here https://github.com/dotnet/razor/pull/11731) instead reads razor via a custom asset type. The logic is basically the same but means that the compiler will no longer be listed as a VSIX analyzer, and so the suppressor won't be loaded in scenarios where razor isn't involved.

I don't love making yet more custom Razor stuff, but there is a bigger change here to come, where we can actually remove the razor redirection logic from Roslyn altogether (including the new `GetRazorReferencesInExtensions` call added here), so hopefully this is only a temporary state of affairs.


Diff:
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
index 87a81f4de7dec..a7f193fe160d9 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
@@ -8,44 +8,17 @@
 using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
 namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-internal sealed class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider
-{
-
-    private readonly ImmutableArray<(AnalyzerFileReference reference, string extensionId)> _analyzerReferences;
 
-    public HostDiagnosticAnalyzerProvider(string? razorSourceGenerator)
-    {
-        if (razorSourceGenerator == null || !File.Exists(razorSourceGenerator))
-        {
-            _analyzerReferences = [];
-        }
-        else
-        {
-            _analyzerReferences = [(
-                new AnalyzerFileReference(razorSourceGenerator, new SimpleAnalyzerAssemblyLoader()),
-                ProjectSystemProject.RazorVsixExtensionId
-            )];
-        }
-    }
-
-    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
-    {
-        return _analyzerReferences;
-    }
+internal sealed class HostDiagnosticAnalyzerProvider(string? razorSourceGenerator) : IHostDiagnosticAnalyzerProvider
+{
+    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() => [];
 
-    private sealed class SimpleAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
+    public ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions()
     {
-        public void AddDependencyLocation(string fullPath)
-        {
-            // This method is used to add a path that should be probed for analyzer dependencies.
-            // In this simple implementation, we do nothing.
-        }
-
-        public Assembly LoadFromPath(string fullPath)
+        if (File.Exists(razorSourceGenerator))
         {
-            // This method is used to load an analyzer assembly from the specified path.
-            // In this simple implementation, we use Assembly.LoadFrom to load the assembly.
-            return Assembly.LoadFrom(fullPath);
+            return [(razorSourceGenerator, ProjectSystemProject.RazorVsixExtensionId)];
         }
+        return [];
     }
 }
diff --git a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
index 95a248768a7b3..dd4f125902eb2 100644
--- a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
+++ b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
@@ -22,6 +22,8 @@ internal sealed partial class VisualStudioDiagnosticAnalyzerProvider : IHostDiag
 {
     private const string AnalyzerContentTypeName = "Microsoft.VisualStudio.Analyzer";
 
+    internal const string RazorContentTypeName = "Microsoft.VisualStudio.RazorAssembly";
+
     /// <summary>
     /// Loader for VSIX-based analyzers.
     /// </summary>
@@ -31,6 +33,7 @@ internal sealed partial class VisualStudioDiagnosticAnalyzerProvider : IHostDiag
     private readonly Type _typeIExtensionContent;
 
     private readonly Lazy<ImmutableArray<(AnalyzerFileReference reference, string extensionId)>> _lazyAnalyzerReferences;
+    private readonly Lazy<ImmutableArray<(string path, string extensionId)>> _lazyRazorReferences;
 
     // internal for testing
     internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type typeIExtensionContent)
@@ -40,24 +43,28 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
 
         _extensionManager = extensionManager;
         _typeIExtensionContent = typeIExtensionContent;
-        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(GetAnalyzerReferencesImpl);
+        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(() => GetExtensionContent(AnalyzerContentTypeName).SelectAsArray(c => (new AnalyzerFileReference(c.path, AnalyzerAssemblyLoader), c.extensionId)));
+        _lazyRazorReferences = new Lazy<ImmutableArray<(string, string)>>(() => GetExtensionContent(RazorContentTypeName));
     }
 
     public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
         => _lazyAnalyzerReferences.Value;
 
-    private ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesImpl()
+    public ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions()
+        => _lazyRazorReferences.Value;
+
+    private ImmutableArray<(string path, string extensionId)> GetExtensionContent(string contentTypeName)
     {
         try
         {
             // dynamic is weird. it can't see internal type with public interface even if callee is
             // implementation of the public interface in internal type. so we can't use dynamic here
-            var _ = PooledDictionary<AnalyzerFileReference, string>.GetInstance(out var analyzePaths);
+            var _ = PooledDictionary<string, string>.GetInstance(out var analyzePaths);
 
-            // var enabledExtensions = extensionManager.GetEnabledExtensions(AnalyzerContentTypeName);
+            // var enabledExtensions = extensionManager.GetEnabledExtensions(contentTypeName);
             var extensionManagerType = _extensionManager.GetType();
             var extensionManager_GetEnabledExtensionsMethod = extensionManagerType.GetRuntimeMethod("GetEnabledExtensions", [typeof(string)]);
-            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [AnalyzerContentTypeName]);
+            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [contentTypeName]);
 
             foreach (var extension in enabledExtensions)
             {
@@ -73,7 +80,7 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
 
                 foreach (var content in extension_Content)
                 {
-                    if (!ShouldInclude(content))
+                    if (!ShouldInclude(content, contentTypeName))
                     {
                         continue;
                     }
@@ -85,7 +92,7 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
                         continue;
                     }
 
-                    analyzePaths.Add(new AnalyzerFileReference(assemblyPath, AnalyzerAssemblyLoader), identifier);
+                    analyzePaths.Add(assemblyPath, identifier);
                 }
             }
 
@@ -94,7 +101,7 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
             GC.KeepAlive(enabledExtensions);
 
             // Order for deterministic result.
-            return analyzePaths.OrderBy((x, y) => string.CompareOrdinal(x.Key.FullPath, y.Key.FullPath)).SelectAsArray(entry => (entry.Key, entry.Value));
+            return analyzePaths.OrderBy((x, y) => string.CompareOrdinal(x.Key, y.Key)).SelectAsArray(entry => (entry.Key, entry.Value));
         }
         catch (TargetInvocationException ex) when (ex.InnerException is InvalidOperationException)
         {
@@ -108,13 +115,13 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
         }
     }
 
-    private static bool ShouldInclude(object content)
+    private static bool ShouldInclude(object content, string contentTypeName)
     {
         // var content_ContentTypeName = content.ContentTypeName;
         var contentType = content.GetType();
         var contentType_ContentTypeNameProperty = contentType.GetRuntimeProperty("ContentTypeName");
         var content_ContentTypeName = contentType_ContentTypeNameProperty.GetValue(content) as string;
 
-        return string.Equals(content_ContentTypeName, AnalyzerContentTypeName, StringComparison.InvariantCultureIgnoreCase);
+        return string.Equals(content_ContentTypeName, contentTypeName, StringComparison.InvariantCultureIgnoreCase);
     }
 }
diff --git a/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb
index bdd55a2ac41c4..8dc1930f29df0 100644
--- a/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb
+++ b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb
@@ -60,5 +60,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
                 Assert.Equal("TestAnalyzer", analyzers(0).ToString)
             End Using
         End Sub
+
+        <Fact>
+        Public Sub GetRazorReferencesInExtensions()
+            Dim extensionManager = New VisualStudioDiagnosticAnalyzerProvider(
+                New MockExtensionManager({({"razorPath1", "razorPath2"}, "RazorVsix")}, contentType:="Microsoft.VisualStudio.RazorAssembly"),
+                GetType(MockExtensionManager.MockContent))
+
+            Dim references = extensionManager.GetRazorAssembliesInExtensions()
+
+            AssertEx.SetEqual(
+            {
+                Path.Combine(TempRoot.Root, "InstallPath\razorPath1"),
+                Path.Combine(TempRoot.Root, "InstallPath\razorPath2")
+            },
+            references.Select(Function(pathAndId) pathAndId.path))
+        End Sub
     End Class
 End Namespace
diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb
index c0d0b85ee28c5..0c6b438aebe8d 100644
--- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb
+++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb
@@ -102,6 +102,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
         Public Async Function RazorSourceGenerator_FromVsix() As Task
             Using environment = New TestEnvironment()
                 Dim providerFactory = DirectCast(environment.ExportProvider.GetExportedValue(Of IVisualStudioDiagnosticAnalyzerProviderFactory), MockVisualStudioDiagnosticAnalyzerProviderFactory)
+                providerFactory.ContentTypeName = VisualStudioDiagnosticAnalyzerProvider.RazorContentTypeName
                 providerFactory.Extensions =
                 {
                     ({
@@ -184,6 +185,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
         Public Async Function RazorSourceGenerator_FromSdk() As Task
             Using environment = New TestEnvironment()
                 Dim providerFactory = DirectCast(environment.ExportProvider.GetExportedValue(Of IVisualStudioDiagnosticAnalyzerProviderFactory), MockVisualStudioDiagnosticAnalyzerProviderFactory)
+                providerFactory.ContentTypeName = VisualStudioDiagnosticAnalyzerProvider.RazorContentTypeName
                 providerFactory.Extensions =
                 {
                      ({
diff --git a/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb b/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb
index 2bca62d09f7d7..821233af421fa 100644
--- a/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb
+++ b/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb
@@ -15,13 +15,15 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
 
         Public Property Extensions As (Paths As String(), Id As String)() = Array.Empty(Of (Paths As String(), Id As String))()
 
+        Public Property ContentTypeName As String = "Microsoft.VisualStudio.Analyzer"
+
         <ImportingConstructor>
         <Obsolete(MefConstruction.ImportingConstructorMessage, True)>
         Public Sub New()
         End Sub
 
         Public Function GetOrCreateProviderAsync(cancellationToken As CancellationToken) As Task(Of VisualStudioDiagnosticAnalyzerProvider) Implements IVisualStudioDiagnosticAnalyzerProviderFactory.GetOrCreateProviderAsync
-            Return Task.FromResult(New VisualStudioDiagnosticAnalyzerProvider(New MockExtensionManager(Extensions), GetType(MockExtensionManager.MockContent)))
+            Return Task.FromResult(New VisualStudioDiagnosticAnalyzerProvider(New MockExtensionManager(Extensions, ContentTypeName), GetType(MockExtensionManager.MockContent)))
         End Function
     End Class
 End Namespace
diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs
index 26854deac43a7..709388e199aed 100644
--- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs
+++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs
@@ -14,4 +14,9 @@ namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 internal interface IHostDiagnosticAnalyzerProvider
 {
     ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions();
+
+    /// <summary>
+    /// Gets the path to any assemblies that represent the closure of razor compiler.
+    /// </summary>
+    ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions();
 }
diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs
index fdf1663a2a0b5..9e8b4a378196d 100644
--- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs
+++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs
@@ -1190,9 +1190,9 @@ private OneOrMany<string> GetMappedAnalyzerPaths(string fullPath)
 
     private OneOrMany<string> GetMappedRazorSourceGenerator(string fullPath)
     {
-        var vsixRazorAnalyzers = _hostInfo.HostDiagnosticAnalyzerProvider.GetAnalyzerReferencesInExtensions().SelectAsArray(
+        var vsixRazorAnalyzers = _hostInfo.HostDiagnosticAnalyzerProvider.GetRazorAssembliesInExtensions().SelectAsArray(
             predicate: item => item.extensionId == RazorVsixExtensionId,
-            selector: item => item.reference.FullPath);
+            selector: item => item.path);
 
         if (!vsixRazorAnalyzers.IsEmpty)
         {


Reviews:
user: login: jjonescz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T08:36:00Z
user: login: chsienki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T21:20:31Z
user: login: chsienki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T21:20:51Z
user: login: chsienki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T21:22:23Z
user: login: chsienki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T21:55:11Z
user: login: jjonescz
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T08:48:22Z
user: login: jaredpar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:33:55Z
user: login: jaredpar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:34:50Z
user: login: jaredpar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:35:45Z
user: login: jaredpar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:35:49Z
user: login: chsienki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T17:56:58Z
user: login: jaredpar
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T20:51:22Z

Review comments:
path: src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs
user: login: jjonescz
body: > I don't love making yet more custom Razor stuff, but there is a bigger change here to come, where we can actually remove the razor redirection logic from Roslyn altogether (including the new `GetRazorReferencesInExtensions` call added here), so hopefully this is only a temporary state of affairs.

What's the change you are talking about here?
created_at: 2025-04-15T08:13:55Z
author_association: MEMBER
diff_hunk: @@ -40,24 +43,28 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
 
         _extensionManager = extensionManager;
         _typeIExtensionContent = typeIExtensionContent;
-        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(GetAnalyzerReferencesImpl);
+        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(() => GetExtensionContent(AnalyzerContentTypeName).SelectAsArray(c => (new AnalyzerFileReference(c.path, AnalyzerAssemblyLoader), c.extensionId)));
+        _lazyRazorReferences = new Lazy<ImmutableArray<(string, string)>>(() => GetExtensionContent(RazorContentTypeName));
     }
 
     public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
         => _lazyAnalyzerReferences.Value;
 
-    private ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesImpl()
+    public ImmutableArray<(string path, string extensionId)> GetRazorReferencesInExtensions()
+        => _lazyRazorReferences.Value;
+
+    private ImmutableArray<(string path, string extensionId)> GetExtensionContent(string contentTypeName)
     {
         try
         {
             // dynamic is weird. it can't see internal type with public interface even if callee is
             // implementation of the public interface in internal type. so we can't use dynamic here
-            var _ = PooledDictionary<AnalyzerFileReference, string>.GetInstance(out var analyzePaths);
+            var _ = PooledDictionary<string, string>.GetInstance(out var analyzePaths);
 
-            // var enabledExtensions = extensionManager.GetEnabledExtensions(AnalyzerContentTypeName);
+            // var enabledExtensions = extensionManager.GetEnabledExtensions(contentTypeName);
             var extensionManagerType = _extensionManager.GetType();
             var extensionManager_GetEnabledExtensionsMethod = extensionManagerType.GetRuntimeMethod("GetEnabledExtensions", [typeof(string)]);
-            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [AnalyzerContentTypeName]);
+            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [contentTypeName]);
path: src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
user: login: jjonescz
body: > Previously this was ok, as the assemblies only contained a source generator, not any analyzers, so listing them as VSIX analyzers was essentially a NOP.

Trying to understand this. When we had

```xml
<Asset Type="Microsoft.VisualStudio.Analyzer" Path="Microsoft.CodeAnalysis.Razor.Compiler.dll" />
```

how come it wasn't loaded until we added the suppressor? Shouldn't Roslyn need to load the DLL and look inside to determine whether there is an analyzer or not?
created_at: 2025-04-15T08:23:54Z
author_association: MEMBER
diff_hunk: @@ -14,4 +14,6 @@ namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 internal interface IHostDiagnosticAnalyzerProvider
 {
     ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions();
+
+    ImmutableArray<(string path, string extensionId)> GetRazorReferencesInExtensions();
path: src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs
user: login: jjonescz
body: Consider adding documentation to this method or clarifying its name - it is supposed to return only razor *source generators* not *analyzers*, right?
created_at: 2025-04-15T08:32:17Z
author_association: MEMBER
diff_hunk: @@ -15,13 +15,15 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
 
         Public Property Extensions As (Paths As String(), Id As String)() = Array.Empty(Of (Paths As String(), Id As String))()
path: src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb
user: login: jjonescz
body: Wouldn't it be better to let callers specify the content type in the tuple here, so it is possible to mock the razor extension as containing both Analyzer and Razor items? Or would multiple mock providers for the same extension with different content types work equivalently?
created_at: 2025-04-15T08:35:05Z
author_association: MEMBER
diff_hunk: @@ -60,5 +60,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
                 Assert.Equal("TestAnalyzer", analyzers(0).ToString)
             End Using
         End Sub
+
+        <Fact>
+        Public Sub GetRazorReferencesInExtensions()
+            Dim extensionManager = New VisualStudioDiagnosticAnalyzerProvider(
+                New MockExtensionManager({({"razorPath1", "razorPath2"}, "RazorVsix")}, contentType:="Microsoft.VisualStudio.RazorAssembly"),
path: src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb
user: login: jjonescz
body: Consider also testing when there are Analyzer items (they shouldn't be returned by GetRazorReferencesInExtensions)
created_at: 2025-04-15T08:35:57Z
author_association: MEMBER
path: src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs
user: login: chsienki
body: Not in PR yet, but basically using the analyzer redirector stuff you added for the SDK
created_at: 2025-04-15T21:20:31Z
author_association: MEMBER
diff_hunk: @@ -40,24 +43,28 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty
 
         _extensionManager = extensionManager;
         _typeIExtensionContent = typeIExtensionContent;
-        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(GetAnalyzerReferencesImpl);
+        _lazyAnalyzerReferences = new Lazy<ImmutableArray<(AnalyzerFileReference, string)>>(() => GetExtensionContent(AnalyzerContentTypeName).SelectAsArray(c => (new AnalyzerFileReference(c.path, AnalyzerAssemblyLoader), c.extensionId)));
+        _lazyRazorReferences = new Lazy<ImmutableArray<(string, string)>>(() => GetExtensionContent(RazorContentTypeName));
     }
 
     public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
         => _lazyAnalyzerReferences.Value;
 
-    private ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesImpl()
+    public ImmutableArray<(string path, string extensionId)> GetRazorReferencesInExtensions()
+        => _lazyRazorReferences.Value;
+
+    private ImmutableArray<(string path, string extensionId)> GetExtensionContent(string contentTypeName)
     {
         try
         {
             // dynamic is weird. it can't see internal type with public interface even if callee is
             // implementation of the public interface in internal type. so we can't use dynamic here
-            var _ = PooledDictionary<AnalyzerFileReference, string>.GetInstance(out var analyzePaths);
+            var _ = PooledDictionary<string, string>.GetInstance(out var analyzePaths);
 
-            // var enabledExtensions = extensionManager.GetEnabledExtensions(AnalyzerContentTypeName);
+            // var enabledExtensions = extensionManager.GetEnabledExtensions(contentTypeName);
             var extensionManagerType = _extensionManager.GetType();
             var extensionManager_GetEnabledExtensionsMethod = extensionManagerType.GetRuntimeMethod("GetEnabledExtensions", [typeof(string)]);
-            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [AnalyzerContentTypeName]);
+            var enabledExtensions = (IEnumerable<object>)extensionManager_GetEnabledExtensionsMethod.Invoke(_extensionManager, [contentTypeName]);
path: src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
user: login: chsienki
body: It was loaded once, to see if it had any analyzers etc. in it. It's then loaded again later on if it was found to contain analyzers previously. Because it didn't find anything before, we didn't load it the second time. 
created_at: 2025-04-15T21:20:51Z
author_association: MEMBER
diff_hunk: @@ -14,4 +14,6 @@ namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 internal interface IHostDiagnosticAnalyzerProvider
 {
     ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions();
+
+    ImmutableArray<(string path, string extensionId)> GetRazorReferencesInExtensions();
path: src/Workspaces/Core/Portable/Workspace/ProjectSystem/IHostDiagnosticAnalyzerProvider.cs
user: login: chsienki
body: Well, it returns anything that's marked as `Microsoft.VisualStudio.RazorAssembly`. Those can still have analyzers or generators (or nothing) in them. I'll change the name to `RazorAssemblies` to make it clearer. 
created_at: 2025-04-15T21:22:23Z
author_association: MEMBER
diff_hunk: @@ -15,13 +15,15 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
 
         Public Property Extensions As (Paths As String(), Id As String)() = Array.Empty(Of (Paths As String(), Id As String))()
path: src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb
user: login: chsienki
body: It doesn't seem worth it. All we'd actually be testing is the mock itself. We could make it work by just removing the assert in the mock that the type being retrieved is the type passed into the constructor, but that at least seem slightly useful to make sure the correct mock is being used. 
created_at: 2025-04-15T21:55:11Z
author_association: MEMBER
diff_hunk: @@ -8,44 +8,17 @@
 using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
 namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-internal sealed class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider
-{
-
-    private readonly ImmutableArray<(AnalyzerFileReference reference, string extensionId)> _analyzerReferences;
 
-    public HostDiagnosticAnalyzerProvider(string? razorSourceGenerator)
-    {
-        if (razorSourceGenerator == null || !File.Exists(razorSourceGenerator))
-        {
-            _analyzerReferences = [];
-        }
-        else
-        {
-            _analyzerReferences = [(
-                new AnalyzerFileReference(razorSourceGenerator, new SimpleAnalyzerAssemblyLoader()),
-                ProjectSystemProject.RazorVsixExtensionId
-            )];
-        }
-    }
-
-    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
-    {
-        return _analyzerReferences;
-    }
+internal sealed class HostDiagnosticAnalyzerProvider(string? razorSourceGenerator) : IHostDiagnosticAnalyzerProvider
+{
+    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() => [];
 
-    private sealed class SimpleAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
+    public ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions()
     {
-        public void AddDependencyLocation(string fullPath)
-        {
-            // This method is used to add a path that should be probed for analyzer dependencies.
-            // In this simple implementation, we do nothing.
-        }
-
-        public Assembly LoadFromPath(string fullPath)
+        if (razorSourceGenerator is not null)
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
user: login: jaredpar
body: The previous code had a `File.Exists` call, is it okay to remove that here? Also are we sure this points to the generator DLL and not the generator directory? 
created_at: 2025-04-16T22:33:55Z
author_association: MEMBER
diff_hunk: @@ -8,44 +8,17 @@
 using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
 namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-internal sealed class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider
-{
-
-    private readonly ImmutableArray<(AnalyzerFileReference reference, string extensionId)> _analyzerReferences;
 
-    public HostDiagnosticAnalyzerProvider(string? razorSourceGenerator)
-    {
-        if (razorSourceGenerator == null || !File.Exists(razorSourceGenerator))
-        {
-            _analyzerReferences = [];
-        }
-        else
-        {
-            _analyzerReferences = [(
-                new AnalyzerFileReference(razorSourceGenerator, new SimpleAnalyzerAssemblyLoader()),
-                ProjectSystemProject.RazorVsixExtensionId
-            )];
-        }
-    }
-
-    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
-    {
-        return _analyzerReferences;
-    }
+internal sealed class HostDiagnosticAnalyzerProvider(string? razorSourceGenerator) : IHostDiagnosticAnalyzerProvider
+{
+    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() => [];
 
-    private sealed class SimpleAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
+    public ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions()
     {
-        public void AddDependencyLocation(string fullPath)
-        {
-            // This method is used to add a path that should be probed for analyzer dependencies.
-            // In this simple implementation, we do nothing.
-        }
-
-        public Assembly LoadFromPath(string fullPath)
+        if (razorSourceGenerator is not null)
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
user: login: jaredpar
body: Basically the old code, intentionally or accidentally, guarded against a directory being specified with the `File.Exists` call. That is removed now. So if someone is passing the razor generator directory in a VS code setting what will happen now? 
created_at: 2025-04-16T22:34:49Z
author_association: MEMBER
diff_hunk: @@ -102,6 +102,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
         Public Async Function RazorSourceGenerator_FromVsix() As Task
             Using environment = New TestEnvironment()
                 Dim providerFactory = DirectCast(environment.ExportProvider.GetExportedValue(Of IVisualStudioDiagnosticAnalyzerProviderFactory), MockVisualStudioDiagnosticAnalyzerProviderFactory)
+                providerFactory.ContentTypeName = "Microsoft.VisualStudio.RazorAssembly"
path: src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb
user: login: jaredpar
body: Could we use the constant `RazorContentTypeName` here?
created_at: 2025-04-16T22:35:45Z
author_association: MEMBER
diff_hunk: @@ -184,6 +185,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
         Public Async Function RazorSourceGenerator_FromSdk() As Task
             Using environment = New TestEnvironment()
                 Dim providerFactory = DirectCast(environment.ExportProvider.GetExportedValue(Of IVisualStudioDiagnosticAnalyzerProviderFactory), MockVisualStudioDiagnosticAnalyzerProviderFactory)
+                providerFactory.ContentTypeName = "Microsoft.VisualStudio.RazorAssembly"
path: src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb
user: login: jaredpar
body: same
created_at: 2025-04-16T22:35:49Z
author_association: MEMBER
diff_hunk: @@ -8,44 +8,17 @@
 using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
 namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-internal sealed class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider
-{
-
-    private readonly ImmutableArray<(AnalyzerFileReference reference, string extensionId)> _analyzerReferences;
 
-    public HostDiagnosticAnalyzerProvider(string? razorSourceGenerator)
-    {
-        if (razorSourceGenerator == null || !File.Exists(razorSourceGenerator))
-        {
-            _analyzerReferences = [];
-        }
-        else
-        {
-            _analyzerReferences = [(
-                new AnalyzerFileReference(razorSourceGenerator, new SimpleAnalyzerAssemblyLoader()),
-                ProjectSystemProject.RazorVsixExtensionId
-            )];
-        }
-    }
-
-    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions()
-    {
-        return _analyzerReferences;
-    }
+internal sealed class HostDiagnosticAnalyzerProvider(string? razorSourceGenerator) : IHostDiagnosticAnalyzerProvider
+{
+    public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() => [];
 
-    private sealed class SimpleAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
+    public ImmutableArray<(string path, string extensionId)> GetRazorAssembliesInExtensions()
     {
-        public void AddDependencyLocation(string fullPath)
-        {
-            // This method is used to add a path that should be probed for analyzer dependencies.
-            // In this simple implementation, we do nothing.
-        }
-
-        public Assembly LoadFromPath(string fullPath)
+        if (razorSourceGenerator is not null)
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
user: login: chsienki
body: Ah yeah, good catch, will put the exists back in.
created_at: 2025-04-17T17:56:57Z
author_association: MEMBER

PR comments:
user: login: chsienki
created_at: 2025-04-11T17:31:00Z
body: I'll run a val build on this PR and make sure the numbers are what we're expecting.
author_association: MEMBER
user: login: chsienki
created_at: 2025-04-11T17:31:45Z
body: /cc @jaredpar @tmat 
author_association: MEMBER
user: login: davidwengier
created_at: 2025-04-13T23:58:50Z
body: I don't begin to understand this code, but I will say that cherry-picking the first commit into my local Roslyn clone has fixed a loading issue in VS Code when trying to get cohosting working :)
author_association: MEMBER
user: login: chsienki
created_at: 2025-04-14T18:50:26Z
body: > I don't begin to understand this code, but I will say that cherry-picking the first commit into my local Roslyn clone has fixed a loading issue in VS Code when trying to get cohosting working :)

That's probably because you don't have the corresponding Razor change, so you've effectively disabled razor redirection :)
author_association: MEMBER
user: login: chsienki
created_at: 2025-04-14T21:19:36Z
body: @dotnet/roslyn-compiler and @dotnet/razor-compiler for reviews please.
author_association: MEMBER
user: login: davidwengier
created_at: 2025-04-15T01:50:03Z
body: > That's probably because you don't have the corresponding Razor change, so you've effectively disabled razor redirection :)

If you mean the Razor change in the PR you linked, that's not going to be relevant because that only affects VS. But also turns out this isn't working in VS Code, so probably some different changes needed on one side or another there
author_association: MEMBER
user: login: chsienki
created_at: 2025-04-21T19:52:57Z
body: @jaredpar Any more thoughts on this?
author_association: MEMBER
user: login: chsienki
created_at: 2025-04-22T17:17:39Z
body: /backport to release/dev17.14
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-22T17:17:52Z
body: Started backporting to _release/dev17.14_: https://github.com/dotnet/roslyn/actions/runs/14600722892
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-22T17:20:22Z
body: @chsienki backporting to "release/dev17.14" failed, the patch most likely resulted in conflicts:

```shell
$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch

Applying: Separate out razor redirection: - Razor is not going to be listed as an analyzer anymore - Add a version of the extension loading logic that loads the razor specified files in the razor VSIX - Add tests
Using index info to reconstruct a base tree...
M	src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
M	src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
Falling back to patching base and 3-way merge...
Auto-merging src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs
Auto-merging src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
CONFLICT (content): Merge conflict in src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0001 Separate out razor redirection: - Razor is not going to be listed as an analyzer anymore - Add a version of the extension loading logic that loads the razor specified files in the razor VSIX - Add tests
Error: The process '/usr/bin/git' failed with exit code 128
```

Please backport manually!
author_association: CONTRIBUTOR