Skip to content

Commit

Permalink
Replace Razor Source Generator reference with one loaded from a VSIX
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Sep 9, 2022
1 parent 3e39dd3 commit 4d3f1ab
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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.

using System;
using System.Composition;
using System.Reflection;
using System.Threading.Tasks;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics
{
internal partial class VisualStudioDiagnosticAnalyzerProvider
{
[Export, Shared]
internal sealed class Factory
{
private readonly IThreadingContext _threadingContext;
private readonly IServiceProvider _serviceProvider;

private VisualStudioDiagnosticAnalyzerProvider? _lazyProvider;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public Factory(IThreadingContext threadingContext, IServiceProvider serviceProvider)
{
_threadingContext = threadingContext;
_serviceProvider = serviceProvider;
}

public async Task<VisualStudioDiagnosticAnalyzerProvider> GetOrCreateProviderAsync()
{
// the following code requires UI thread:
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
return GetOrCreateProviderOnMainThread();
}

public VisualStudioDiagnosticAnalyzerProvider GetOrCreateProviderOnMainThread()
{
Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread);

if (_lazyProvider != null)
{
return _lazyProvider;
}

var dte = (EnvDTE.DTE)_serviceProvider.GetService(typeof(EnvDTE.DTE));

// Microsoft.VisualStudio.ExtensionManager is non-versioned, so we need to dynamically load it, depending on the version of VS we are running on
// this will allow us to build once and deploy on different versions of VS SxS.
var vsDteVersion = Version.Parse(dte.Version.Split(' ')[0]); // DTE.Version is in the format of D[D[.D[D]]][ (?+)], so we need to split out the version part and check for uninitialized Major/Minor below

var assembly = Assembly.Load($"Microsoft.VisualStudio.ExtensionManager, Version={(vsDteVersion.Major == -1 ? 0 : vsDteVersion.Major)}.{(vsDteVersion.Minor == -1 ? 0 : vsDteVersion.Minor)}.0.0, PublicKeyToken=b03f5f7f11d50a3a");
var typeIExtensionContent = assembly.GetType("Microsoft.VisualStudio.ExtensionManager.IExtensionContent");
var type = assembly.GetType("Microsoft.VisualStudio.ExtensionManager.SVsExtensionManager");
var extensionManager = _serviceProvider.GetService(type);

return _lazyProvider = new VisualStudioDiagnosticAnalyzerProvider(extensionManager, typeIExtensionContent);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
Expand All @@ -27,19 +28,16 @@ internal partial class VisualStudioDiagnosticAnalyzerProvider
internal sealed class WorkspaceEventListener : IEventListener<object>
{
private readonly IAsynchronousOperationListener _listener;
private readonly IThreadingContext _threadingContext;
private readonly IServiceProvider _serviceProvider;
private readonly Factory _providerFactory;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public WorkspaceEventListener(
IThreadingContext threadingContext,
Shell.SVsServiceProvider serviceProvider,
IAsynchronousOperationListenerProvider listenerProvider)
IAsynchronousOperationListenerProvider listenerProvider,
Factory providerFactory)
{
_threadingContext = threadingContext;
_serviceProvider = serviceProvider;
_listener = listenerProvider.GetListener(nameof(Workspace));
_providerFactory = providerFactory;
}

public void StartListening(Workspace workspace, object serviceOpt)
Expand All @@ -57,37 +55,18 @@ private async Task InitializeWorkspaceAsync(ISolutionAnalyzerSetterWorkspaceServ
{
try
{
var provider = await CreateProviderAsync().ConfigureAwait(false);
var provider = await _providerFactory.GetOrCreateProviderAsync().ConfigureAwait(false);

var references = provider.GetAnalyzerReferencesInExtensions();
LogWorkspaceAnalyzerCount(references.Length);
setter.SetAnalyzerReferences(references);
setter.SetAnalyzerReferences(references.CastArray<AnalyzerReference>());
}
catch (Exception e) when (FatalError.ReportAndPropagate(e, ErrorSeverity.Diagnostic))
{
throw ExceptionUtilities.Unreachable;
}
}

private async Task<VisualStudioDiagnosticAnalyzerProvider> CreateProviderAsync()
{
// the following code requires UI thread:
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();

var dte = (EnvDTE.DTE)_serviceProvider.GetService(typeof(EnvDTE.DTE));

// Microsoft.VisualStudio.ExtensionManager is non-versioned, so we need to dynamically load it, depending on the version of VS we are running on
// this will allow us to build once and deploy on different versions of VS SxS.
var vsDteVersion = Version.Parse(dte.Version.Split(' ')[0]); // DTE.Version is in the format of D[D[.D[D]]][ (?+)], so we need to split out the version part and check for uninitialized Major/Minor below

var assembly = Assembly.Load($"Microsoft.VisualStudio.ExtensionManager, Version={(vsDteVersion.Major == -1 ? 0 : vsDteVersion.Major)}.{(vsDteVersion.Minor == -1 ? 0 : vsDteVersion.Minor)}.0.0, PublicKeyToken=b03f5f7f11d50a3a");
var typeIExtensionContent = assembly.GetType("Microsoft.VisualStudio.ExtensionManager.IExtensionContent");
var type = assembly.GetType("Microsoft.VisualStudio.ExtensionManager.SVsExtensionManager");
var extensionManager = _serviceProvider.GetService(type);

return new VisualStudioDiagnosticAnalyzerProvider(extensionManager, typeIExtensionContent);
}

private static void LogWorkspaceAnalyzerCount(int analyzerCount)
{
Logger.Log(FunctionId.DiagnosticAnalyzerService_Analyzers, KeyValueLogMessage.Create(m => m["AnalyzerCount"] = analyzerCount));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ internal partial class VisualStudioDiagnosticAnalyzerProvider
private readonly object _extensionManager;
private readonly Type _typeIExtensionContent;

private readonly Lazy<ImmutableArray<AnalyzerFileReference>> _lazyAnalyzerReferences;

// internal for testing
internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type typeIExtensionContent)
{
Expand All @@ -37,10 +39,13 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty

_extensionManager = extensionManager;
_typeIExtensionContent = typeIExtensionContent;
_lazyAnalyzerReferences = new Lazy<ImmutableArray<AnalyzerFileReference>>(GetAnalyzerReferencesImpl);
}

// internal for testing
internal ImmutableArray<AnalyzerReference> GetAnalyzerReferencesInExtensions()
public ImmutableArray<AnalyzerFileReference> GetAnalyzerReferencesInExtensions()
=> _lazyAnalyzerReferences.Value;

private ImmutableArray<AnalyzerFileReference> GetAnalyzerReferencesImpl()
{
try
{
Expand Down Expand Up @@ -89,7 +94,7 @@ internal ImmutableArray<AnalyzerReference> GetAnalyzerReferencesInExtensions()
// so that we can debug it through if mandatory analyzers are missing
GC.KeepAlive(enabledExtensions);

return analyzePaths.SelectAsArray(path => (AnalyzerReference)new AnalyzerFileReference(path, AnalyzerAssemblyLoader));
return analyzePaths.SelectAsArray(path => new AnalyzerFileReference(path, AnalyzerAssemblyLoader));
}
catch (TargetInvocationException ex) when (ex.InnerException is InvalidOperationException)
{
Expand All @@ -99,7 +104,7 @@ internal ImmutableArray<AnalyzerReference> GetAnalyzerReferencesInExtensions()
//
// fortunately, this only happens on disposing at shutdown, so we just catch the exception and silently swallow it.
// we are about to shutdown anyway.
return ImmutableArray<AnalyzerReference>.Empty;
return ImmutableArray<AnalyzerFileReference>.Empty;
}
}

Expand Down
23 changes: 21 additions & 2 deletions src/VisualStudio/Core/Def/ProjectSystem/VisualStudioAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
{
Expand All @@ -23,16 +27,23 @@ internal sealed class VisualStudioAnalyzer : IDisposable

private readonly ProjectId _projectId;
private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource;
private readonly VisualStudioDiagnosticAnalyzerProvider _vsixAnalyzerProvider;
private readonly string _language;

// these 2 are mutable states that must be guarded under the _gate.
private readonly object _gate = new();
private AnalyzerReference? _analyzerReference;
private ImmutableArray<DiagnosticData> _analyzerLoadErrors = ImmutableArray<DiagnosticData>.Empty;

public VisualStudioAnalyzer(string fullPath, HostDiagnosticUpdateSource hostDiagnosticUpdateSource, ProjectId projectId, string language)
public VisualStudioAnalyzer(
string fullPath,
VisualStudioDiagnosticAnalyzerProvider vsixAnalyzerProvider,
HostDiagnosticUpdateSource hostDiagnosticUpdateSource,
ProjectId projectId,
string language)
{
FullPath = fullPath;
_vsixAnalyzerProvider = vsixAnalyzerProvider;
_hostDiagnosticUpdateSource = hostDiagnosticUpdateSource;
_projectId = projectId;
_language = language;
Expand All @@ -49,7 +60,12 @@ public AnalyzerReference GetReference()
// TODO: ensure the file watcher is subscribed
// (tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/661546)

var analyzerFileReference = new AnalyzerFileReference(FullPath, s_analyzerAssemblyLoader);
var analyzerFileReference = IsRazorSourceGeneratorAssembly(FullPath) ?
_vsixAnalyzerProvider.GetAnalyzerReferencesInExtensions().FirstOrDefault(r => r.FullPath != null && IsRazorSourceGeneratorAssembly(r.FullPath)) :
null;

analyzerFileReference ??= new AnalyzerFileReference(FullPath, s_analyzerAssemblyLoader);

analyzerFileReference.AnalyzerLoadFailed += OnAnalyzerLoadError;
_analyzerReference = analyzerFileReference;
}
Expand All @@ -58,6 +74,9 @@ public AnalyzerReference GetReference()
}
}

private static bool IsRazorSourceGeneratorAssembly(string fullPath)
=> fullPath.EndsWith("Microsoft.NET.Sdk.Razor.SourceGenerators.dll", StringComparison.Ordinal);

private void OnAnalyzerLoadError(object sender, AnalyzerLoadFailureEventArgs e)
{
var data = DocumentAnalysisExecutor.CreateAnalyzerLoadFailureDiagnostic(e, FullPath, _projectId, _language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Roslyn.Utilities;

Expand All @@ -30,6 +31,7 @@ internal sealed partial class VisualStudioProject

private readonly VisualStudioWorkspaceImpl _workspace;
private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource;
private readonly VisualStudioDiagnosticAnalyzerProvider _vsixAnalyzerProvider;

/// <summary>
/// Provides dynamic source files for files added through <see cref="AddDynamicSourceFile" />.
Expand Down Expand Up @@ -145,6 +147,7 @@ internal VisualStudioProject(
VisualStudioWorkspaceImpl workspace,
ImmutableArray<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> dynamicFileInfoProviders,
HostDiagnosticUpdateSource hostDiagnosticUpdateSource,
VisualStudioDiagnosticAnalyzerProvider vsixAnalyzerProvider,
ProjectId id,
string displayName,
string language,
Expand All @@ -156,6 +159,7 @@ internal VisualStudioProject(
_workspace = workspace;
_dynamicFileInfoProviders = dynamicFileInfoProviders;
_hostDiagnosticUpdateSource = hostDiagnosticUpdateSource;
_vsixAnalyzerProvider = vsixAnalyzerProvider;

Id = id;
Language = language;
Expand Down Expand Up @@ -897,6 +901,7 @@ public void AddAnalyzerReference(string fullPath)
// Nope, we actually need to make a new one.
var visualStudioAnalyzer = new VisualStudioAnalyzer(
fullPath,
_vsixAnalyzerProvider,
_hostDiagnosticUpdateSource,
Id,
Language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.LanguageServices.ExternalAccess.VSTypeScript.Api;
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
Expand All @@ -33,6 +34,7 @@ internal sealed class VisualStudioProjectFactory : IVsTypeScriptVisualStudioProj
private readonly VisualStudioWorkspaceImpl _visualStudioWorkspaceImpl;
private readonly ImmutableArray<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> _dynamicFileInfoProviders;
private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource;
private readonly VisualStudioDiagnosticAnalyzerProvider.Factory _vsixAnalyzerProviderFactory;
private readonly Shell.IAsyncServiceProvider _serviceProvider;

[ImportingConstructor]
Expand All @@ -42,12 +44,14 @@ public VisualStudioProjectFactory(
VisualStudioWorkspaceImpl visualStudioWorkspaceImpl,
[ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
HostDiagnosticUpdateSource hostDiagnosticUpdateSource,
VisualStudioDiagnosticAnalyzerProvider.Factory vsixAnalyzerProviderFactory,
SVsServiceProvider serviceProvider)
{
_threadingContext = threadingContext;
_visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
_dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
_hostDiagnosticUpdateSource = hostDiagnosticUpdateSource;
_vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
_serviceProvider = (Shell.IAsyncServiceProvider)serviceProvider;
}

Expand All @@ -74,6 +78,8 @@ public async Task<VisualStudioProject> CreateAndAddToWorkspaceAsync(
? filePath
: null;

var vsixAnalyzerProvider = _vsixAnalyzerProviderFactory.GetOrCreateProviderOnMainThread();

// Following can be off the UI thread.
await TaskScheduler.Default;

Expand All @@ -88,6 +94,7 @@ public async Task<VisualStudioProject> CreateAndAddToWorkspaceAsync(
_visualStudioWorkspaceImpl,
_dynamicFileInfoProviders,
_hostDiagnosticUpdateSource,
vsixAnalyzerProvider,
id,
displayName: projectSystemName,
language,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
Private ReadOnly _contentType As String
Private ReadOnly _locations() As String

Public Sub New(contentType As String, ParamArray locations() As String)
Public Sub New(Optional locations As String() = Nothing,
Optional contentType As String = "Microsoft.VisualStudio.Analyzer")
_contentType = contentType
_locations = locations
_locations = If(locations, Array.Empty(Of String))
End Sub

Public Function GetEnabledExtensionContentLocations(contentTypeName As String) As IEnumerable(Of String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
<Fact>
Public Sub GetAnalyzerReferencesInExtensions_Substitution()
Dim extensionManager = New VisualStudioDiagnosticAnalyzerProvider(
New MockExtensionManager("Microsoft.VisualStudio.Analyzer", "$RootFolder$\test\test.dll", "$ShellFolder$\test\test.dll", "test\test.dll"),
New MockExtensionManager({"$RootFolder$\test\test.dll", "$ShellFolder$\test\test.dll", "test\test.dll"}),
GetType(MockExtensionManager.MockContent))

Dim references = extensionManager.GetAnalyzerReferencesInExtensions()
Expand All @@ -32,7 +32,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
<Fact>
Public Sub GetAnalyzerReferencesInExtensions()
Dim extensionManager = New VisualStudioDiagnosticAnalyzerProvider(
New MockExtensionManager("Microsoft.VisualStudio.Analyzer", "installPath1", "installPath2", "installPath3"),
New MockExtensionManager({"installPath1", "installPath2", "installPath3"}),
GetType(MockExtensionManager.MockContent))

Dim references = extensionManager.GetAnalyzerReferencesInExtensions()
Expand Down
Loading

0 comments on commit 4d3f1ab

Please sign in to comment.