diff --git a/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerProviderFactory.cs b/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerProviderFactory.cs new file mode 100644 index 0000000000000..3e58c6a824b5f --- /dev/null +++ b/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerProviderFactory.cs @@ -0,0 +1,17 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics +{ + /// + /// Abstraction for testing purposes. + /// + internal interface IVisualStudioDiagnosticAnalyzerProviderFactory + { + Task GetOrCreateProviderAsync(CancellationToken cancellationToken); + } +} diff --git a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.Factory.cs b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.Factory.cs new file mode 100644 index 0000000000000..6e8acda92a395 --- /dev/null +++ b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.Factory.cs @@ -0,0 +1,60 @@ +// 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; +using Microsoft.VisualStudio.Shell; +using System.Threading; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics +{ + internal partial class VisualStudioDiagnosticAnalyzerProvider + { + [Export(typeof(IVisualStudioDiagnosticAnalyzerProviderFactory)), Shared] + internal sealed class Factory : IVisualStudioDiagnosticAnalyzerProviderFactory + { + private readonly IThreadingContext _threadingContext; + private readonly IServiceProvider _serviceProvider; + + private VisualStudioDiagnosticAnalyzerProvider? _lazyProvider; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public Factory(IThreadingContext threadingContext, SVsServiceProvider serviceProvider) + { + _threadingContext = threadingContext; + _serviceProvider = serviceProvider; + } + + public async Task GetOrCreateProviderAsync(CancellationToken cancellationToken) + { + // the following code requires UI thread: + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + 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); + } + } + } +} diff --git a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.WorkspaceEventListener.cs b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.WorkspaceEventListener.cs index ce1d8d54c2928..f7ba92198a7ca 100644 --- a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.WorkspaceEventListener.cs +++ b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.WorkspaceEventListener.cs @@ -5,8 +5,10 @@ using System; using System.Composition; using System.Reflection; +using System.Threading; 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; @@ -27,19 +29,16 @@ internal partial class VisualStudioDiagnosticAnalyzerProvider internal sealed class WorkspaceEventListener : IEventListener { private readonly IAsynchronousOperationListener _listener; - private readonly IThreadingContext _threadingContext; - private readonly IServiceProvider _serviceProvider; + private readonly IVisualStudioDiagnosticAnalyzerProviderFactory _providerFactory; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public WorkspaceEventListener( - IThreadingContext threadingContext, - Shell.SVsServiceProvider serviceProvider, - IAsynchronousOperationListenerProvider listenerProvider) + IAsynchronousOperationListenerProvider listenerProvider, + IVisualStudioDiagnosticAnalyzerProviderFactory providerFactory) { - _threadingContext = threadingContext; - _serviceProvider = serviceProvider; _listener = listenerProvider.GetListener(nameof(Workspace)); + _providerFactory = providerFactory; } public void StartListening(Workspace workspace, object serviceOpt) @@ -57,11 +56,11 @@ private async Task InitializeWorkspaceAsync(ISolutionAnalyzerSetterWorkspaceServ { try { - var provider = await CreateProviderAsync().ConfigureAwait(false); + var provider = await _providerFactory.GetOrCreateProviderAsync(CancellationToken.None).ConfigureAwait(false); var references = provider.GetAnalyzerReferencesInExtensions(); LogWorkspaceAnalyzerCount(references.Length); - setter.SetAnalyzerReferences(references); + setter.SetAnalyzerReferences(references.SelectAsArray(referenceAndId => (AnalyzerReference)referenceAndId.reference)); } catch (Exception e) when (FatalError.ReportAndPropagate(e, ErrorSeverity.Diagnostic)) { @@ -69,25 +68,6 @@ private async Task InitializeWorkspaceAsync(ISolutionAnalyzerSetterWorkspaceServ } } - private async Task 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)); diff --git a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs index 4e461a9787535..3d285f6330263 100644 --- a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs +++ b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerProvider.cs @@ -29,6 +29,8 @@ internal partial class VisualStudioDiagnosticAnalyzerProvider private readonly object _extensionManager; private readonly Type _typeIExtensionContent; + private readonly Lazy> _lazyAnalyzerReferences; + // internal for testing internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type typeIExtensionContent) { @@ -37,16 +39,19 @@ internal VisualStudioDiagnosticAnalyzerProvider(object extensionManager, Type ty _extensionManager = extensionManager; _typeIExtensionContent = typeIExtensionContent; + _lazyAnalyzerReferences = new Lazy>(GetAnalyzerReferencesImpl); } - // internal for testing - internal ImmutableArray GetAnalyzerReferencesInExtensions() + public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() + => _lazyAnalyzerReferences.Value; + + private ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesImpl() { 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 _ = PooledHashSet.GetInstance(out var analyzePaths); + var _ = PooledDictionary.GetInstance(out var analyzePaths); // var enabledExtensions = extensionManager.GetEnabledExtensions(AnalyzerContentTypeName); var extensionManagerType = _extensionManager.GetType(); @@ -55,15 +60,13 @@ internal ImmutableArray GetAnalyzerReferencesInExtensions() foreach (var extension in enabledExtensions) { - // var name = extension.Header.LocalizedName; var extensionType = extension.GetType(); var extensionType_HeaderProperty = extensionType.GetRuntimeProperty("Header"); var extension_Header = extensionType_HeaderProperty.GetValue(extension); var extension_HeaderType = extension_Header.GetType(); - var extension_HeaderType_LocalizedNameProperty = extension_HeaderType.GetRuntimeProperty("LocalizedName"); - var name = extension_HeaderType_LocalizedNameProperty.GetValue(extension_Header) as string; + var extension_HeaderType_Identifier = extension_HeaderType.GetRuntimeProperty("Identifier"); + var identifier = (string)extension_HeaderType_Identifier.GetValue(extension_Header); - // var extension_Content = extension.Content; var extensionType_ContentProperty = extensionType.GetRuntimeProperty("Content"); var extension_Content = (IEnumerable)extensionType_ContentProperty.GetValue(extension); @@ -81,7 +84,7 @@ internal ImmutableArray GetAnalyzerReferencesInExtensions() continue; } - analyzePaths.Add(assemblyPath); + analyzePaths.Add(new AnalyzerFileReference(assemblyPath, AnalyzerAssemblyLoader), identifier); } } @@ -89,7 +92,8 @@ internal ImmutableArray 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)); + // Order for deterministic result. + return analyzePaths.OrderBy((x, y) => string.CompareOrdinal(x.Key.FullPath, y.Key.FullPath)).SelectAsArray(entry => (entry.Key, entry.Value)); } catch (TargetInvocationException ex) when (ex.InnerException is InvalidOperationException) { @@ -99,7 +103,7 @@ internal ImmutableArray 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.Empty; + return ImmutableArray<(AnalyzerFileReference, string)>.Empty; } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs index 310bd18639769..b2c5e26376030 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs @@ -13,12 +13,14 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; 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; @@ -26,10 +28,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem { internal sealed partial class VisualStudioProject { + private static readonly char[] s_directorySeparator = { Path.DirectorySeparatorChar }; private static readonly ImmutableArray s_defaultMetadataReferenceProperties = ImmutableArray.Create(default(MetadataReferenceProperties)); private readonly VisualStudioWorkspaceImpl _workspace; private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource; + private readonly VisualStudioDiagnosticAnalyzerProvider _vsixAnalyzerProvider; /// /// Provides dynamic source files for files added through . @@ -145,6 +149,7 @@ internal VisualStudioProject( VisualStudioWorkspaceImpl workspace, ImmutableArray> dynamicFileInfoProviders, HostDiagnosticUpdateSource hostDiagnosticUpdateSource, + VisualStudioDiagnosticAnalyzerProvider vsixAnalyzerProvider, ProjectId id, string displayName, string language, @@ -156,6 +161,7 @@ internal VisualStudioProject( _workspace = workspace; _dynamicFileInfoProviders = dynamicFileInfoProviders; _hostDiagnosticUpdateSource = hostDiagnosticUpdateSource; + _vsixAnalyzerProvider = vsixAnalyzerProvider; Id = id; Language = language; @@ -877,39 +883,48 @@ public void AddAnalyzerReference(string fullPath) { CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath)); + var mappedPaths = GetMappedAnalyzerPaths(fullPath); + using (_gate.DisposableWait()) { - if (_analyzerPathsToAnalyzers.ContainsKey(fullPath)) + // check all mapped paths first, so that all analyzers are either added or not + foreach (var mappedFullPath in mappedPaths) { - throw new ArgumentException($"'{fullPath}' has already been added to this project.", nameof(fullPath)); + if (_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath)) + { + throw new ArgumentException($"'{fullPath}' has already been added to this project.", nameof(fullPath)); + } } - // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid removing - // it once we apply the batch - var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(a => a.FullPath == fullPath); - if (analyzerPendingRemoval != null) - { - _analyzersRemovedInBatch.Remove(analyzerPendingRemoval); - _analyzerPathsToAnalyzers.Add(fullPath, analyzerPendingRemoval); - } - else + foreach (var mappedFullPath in mappedPaths) { - // Nope, we actually need to make a new one. - var visualStudioAnalyzer = new VisualStudioAnalyzer( - fullPath, - _hostDiagnosticUpdateSource, - Id, - Language); - - _analyzerPathsToAnalyzers.Add(fullPath, visualStudioAnalyzer); - - if (_activeBatchScopes > 0) + // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid removing + // it once we apply the batch + var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(a => a.FullPath == mappedFullPath); + if (analyzerPendingRemoval != null) { - _analyzersAddedInBatch.Add(visualStudioAnalyzer); + _analyzersRemovedInBatch.Remove(analyzerPendingRemoval); + _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerPendingRemoval); } else { - _workspace.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceAdded(Id, visualStudioAnalyzer.GetReference())); + // Nope, we actually need to make a new one. + var visualStudioAnalyzer = new VisualStudioAnalyzer( + mappedFullPath, + _hostDiagnosticUpdateSource, + Id, + Language); + + _analyzerPathsToAnalyzers.Add(mappedFullPath, visualStudioAnalyzer); + + if (_activeBatchScopes > 0) + { + _analyzersAddedInBatch.Add(visualStudioAnalyzer); + } + else + { + _workspace.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceAdded(Id, visualStudioAnalyzer.GetReference())); + } } } } @@ -922,35 +937,71 @@ public void RemoveAnalyzerReference(string fullPath) throw new ArgumentException("message", nameof(fullPath)); } + var mappedPaths = GetMappedAnalyzerPaths(fullPath); + using (_gate.DisposableWait()) { - if (!_analyzerPathsToAnalyzers.TryGetValue(fullPath, out var visualStudioAnalyzer)) + // check all mapped paths first, so that all analyzers are either removed or not + foreach (var mappedFullPath in mappedPaths) { - throw new ArgumentException($"'{fullPath}' is not an analyzer of this project.", nameof(fullPath)); + if (!_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath)) + { + throw new ArgumentException($"'{fullPath}' is not an analyzer of this project.", nameof(fullPath)); + } } - _analyzerPathsToAnalyzers.Remove(fullPath); - - if (_activeBatchScopes > 0) + foreach (var mappedFullPath in mappedPaths) { - // This analyzer may be one we've just added in the same batch; in that case, just don't add - // it in the first place. - if (_analyzersAddedInBatch.Remove(visualStudioAnalyzer)) + var visualStudioAnalyzer = _analyzerPathsToAnalyzers[mappedFullPath]; + + _analyzerPathsToAnalyzers.Remove(mappedFullPath); + + if (_activeBatchScopes > 0) { - // Nothing is holding onto this analyzer now, so get rid of it - visualStudioAnalyzer.Dispose(); + // This analyzer may be one we've just added in the same batch; in that case, just don't add + // it in the first place. + if (_analyzersAddedInBatch.Remove(visualStudioAnalyzer)) + { + // Nothing is holding onto this analyzer now, so get rid of it + visualStudioAnalyzer.Dispose(); + } + else + { + _analyzersRemovedInBatch.Add(visualStudioAnalyzer); + } } else { - _analyzersRemovedInBatch.Add(visualStudioAnalyzer); + _workspace.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceRemoved(Id, visualStudioAnalyzer.GetReference())); + + visualStudioAnalyzer.Dispose(); } } - else + } + } + + private const string RazorVsixExtensionId = "Microsoft.VisualStudio.RazorExtension"; + private static readonly string s_razorSourceGeneratorSdkDirectory = Path.Combine("Sdks", "Microsoft.NET.Sdk.Razor", "source-generators") + PathUtilities.DirectorySeparatorStr; + private static readonly string s_razorSourceGeneratorMainAssemblyRootedFileName = PathUtilities.DirectorySeparatorStr + "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"; + + private OneOrMany GetMappedAnalyzerPaths(string fullPath) + { + // Map all files in the SDK directory that contains the Razor source generator to source generator files loaded from VSIX. + // Include the generator and all its dependencies shipped in VSIX, discard the generator and all dependencies in the SDK + if (fullPath.LastIndexOf(s_razorSourceGeneratorSdkDirectory, StringComparison.OrdinalIgnoreCase) + s_razorSourceGeneratorSdkDirectory.Length - 1 == + fullPath.LastIndexOf(Path.DirectorySeparatorChar)) + { + if (fullPath.EndsWith(s_razorSourceGeneratorMainAssemblyRootedFileName, StringComparison.OrdinalIgnoreCase)) { - _workspace.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceRemoved(Id, visualStudioAnalyzer.GetReference())); - visualStudioAnalyzer.Dispose(); + return OneOrMany.Create(_vsixAnalyzerProvider.GetAnalyzerReferencesInExtensions().SelectAsArray( + predicate: item => item.extensionId == RazorVsixExtensionId, + selector: item => item.reference.FullPath)); } + + return OneOrMany.Create(ImmutableArray.Empty); } + + return OneOrMany.Create(fullPath); } #endregion diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs index 42871a3177534..3b862e404dfac 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs @@ -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; @@ -33,6 +34,7 @@ internal sealed class VisualStudioProjectFactory : IVsTypeScriptVisualStudioProj private readonly VisualStudioWorkspaceImpl _visualStudioWorkspaceImpl; private readonly ImmutableArray> _dynamicFileInfoProviders; private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSource; + private readonly IVisualStudioDiagnosticAnalyzerProviderFactory _vsixAnalyzerProviderFactory; private readonly Shell.IAsyncServiceProvider _serviceProvider; [ImportingConstructor] @@ -42,12 +44,14 @@ public VisualStudioProjectFactory( VisualStudioWorkspaceImpl visualStudioWorkspaceImpl, [ImportMany] IEnumerable> fileInfoProviders, HostDiagnosticUpdateSource hostDiagnosticUpdateSource, + IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory, SVsServiceProvider serviceProvider) { _threadingContext = threadingContext; _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl; _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty(); _hostDiagnosticUpdateSource = hostDiagnosticUpdateSource; + _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory; _serviceProvider = (Shell.IAsyncServiceProvider)serviceProvider; } @@ -74,6 +78,8 @@ public async Task CreateAndAddToWorkspaceAsync( ? filePath : null; + var vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(false); + // Following can be off the UI thread. await TaskScheduler.Default; @@ -88,6 +94,7 @@ public async Task CreateAndAddToWorkspaceAsync( _visualStudioWorkspaceImpl, _dynamicFileInfoProviders, _hostDiagnosticUpdateSource, + vsixAnalyzerProvider, id, displayName: projectSystemName, language, diff --git a/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb index bf2bb51780089..b2d7872a6bcb9 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticAnalyzerProviderTests.vb @@ -15,7 +15,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics 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"}, "Vsix")}), GetType(MockExtensionManager.MockContent)) Dim references = extensionManager.GetAnalyzerReferencesInExtensions() @@ -26,13 +26,13 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Path.Combine(TempRoot.Root, "ResolvedShellFolder\test\test.dll"), Path.Combine(TempRoot.Root, "InstallPath\test\test.dll") }, - references.Select(Function(reference) reference.FullPath)) + references.Select(Function(referenceAndId) referenceAndId.reference.FullPath)) End Sub Public Sub GetAnalyzerReferencesInExtensions() Dim extensionManager = New VisualStudioDiagnosticAnalyzerProvider( - New MockExtensionManager("Microsoft.VisualStudio.Analyzer", "installPath1", "installPath2", "installPath3"), + New MockExtensionManager({({"installPath1", "installPath2", "installPath3"}, "Vsix")}), GetType(MockExtensionManager.MockContent)) Dim references = extensionManager.GetAnalyzerReferencesInExtensions() @@ -43,7 +43,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Path.Combine(TempRoot.Root, "InstallPath\installPath2"), Path.Combine(TempRoot.Root, "InstallPath\installPath3") }, - references.Select(Function(reference) reference.FullPath)) + references.Select(Function(referenceAndId) referenceAndId.reference.FullPath)) End Sub diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb index f4c412bebb0f3..518a751398c70 100644 --- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb +++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/AnalyzerReferenceTests.vb @@ -3,11 +3,14 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.Immutable +Imports System.IO Imports System.Threading Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.[Shared].TestHooks Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework Imports Roslyn.Test.Utilities @@ -108,6 +111,88 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim End Using End Function + + Public Async Function RazorSourceGenerator() As Task + Using environment = New TestEnvironment() + Dim providerFactory = DirectCast(environment.ExportProvider.GetExportedValue(Of IVisualStudioDiagnosticAnalyzerProviderFactory), MockVisualStudioDiagnosticAnalyzerProviderFactory) + providerFactory.Extensions = + { + ({ + Path.Combine(TempRoot.Root, "RazorVsix", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency1.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency2.dll") + }, + "Microsoft.VisualStudio.RazorExtension"), + ({ + Path.Combine(TempRoot.Root, "File.dll") + }, + "AnotherExtension") + } + + Dim project = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync( + "Project", LanguageNames.CSharp, CancellationToken.None) + + ' adding just Razor dependency and not the main source generator is a no-op + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "SdkDependency1.dll")) + + Assert.Empty(environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences) + + ' removing just Razor dependency and not the main source generator is a no-op + project.RemoveAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "SdkDependency1.dll")) + + Assert.Empty(environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences) + + ' add Razor source generator and a couple more other analyzer filess: + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "SdkDependency1.dll")) + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll")) + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Some other directory", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll")) + project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Dir", "File.dll")) + + AssertEx.Equal( + { + Path.Combine(TempRoot.Root, "RazorVsix", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency1.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency2.dll"), + Path.Combine(TempRoot.Root, "Some other directory", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"), + Path.Combine(TempRoot.Root, "Dir", "File.dll") + }, environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences.Select(Function(r) r.FullPath)) + + ' add Razor source generator again: + Assert.Throws(Of ArgumentException)( + "fullPath", + Sub() project.AddAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"))) + + AssertEx.Equal( + { + Path.Combine(TempRoot.Root, "RazorVsix", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency1.dll"), + Path.Combine(TempRoot.Root, "RazorVsix", "VsixDependency2.dll"), + Path.Combine(TempRoot.Root, "Some other directory", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"), + Path.Combine(TempRoot.Root, "Dir", "File.dll") + }, environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences.Select(Function(r) r.FullPath)) + + ' remove: + project.RemoveAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "SdkDependency1.dll")) + project.RemoveAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll")) + project.RemoveAnalyzerReference(Path.Combine(TempRoot.Root, "Some other directory", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll")) + + AssertEx.Equal( + { + Path.Combine(TempRoot.Root, "Dir", "File.dll") + }, environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences.Select(Function(r) r.FullPath)) + + ' remove again: + Assert.Throws(Of ArgumentException)( + "fullPath", + Sub() project.RemoveAnalyzerReference(Path.Combine(TempRoot.Root, "Sdks", "Microsoft.NET.Sdk.Razor", "source-generators", "Microsoft.NET.Sdk.Razor.SourceGenerators.dll"))) + + AssertEx.Equal( + { + Path.Combine(TempRoot.Root, "Dir", "File.dll") + }, environment.Workspace.CurrentSolution.Projects.Single().AnalyzerReferences.Select(Function(r) r.FullPath)) + End Using + End Function + Private Shared Async Function GetDiagnostics(environment As TestEnvironment) As Task(Of ImmutableArray(Of DiagnosticData)) ' Wait for diagnostics to be updated asynchronously Dim waiter = environment.ExportProvider.GetExportedValue(Of AsynchronousOperationListenerProvider).GetWaiter(FeatureAttribute.DiagnosticService) diff --git a/src/VisualStudio/Core/Test/Diagnostics/MockExtensionManager.vb b/src/VisualStudio/TestUtilities2/MockExtensionManager.vb similarity index 62% rename from src/VisualStudio/Core/Test/Diagnostics/MockExtensionManager.vb rename to src/VisualStudio/TestUtilities2/MockExtensionManager.vb index f1cae1e1428e0..debf644c7f18b 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/MockExtensionManager.vb +++ b/src/VisualStudio/TestUtilities2/MockExtensionManager.vb @@ -2,44 +2,38 @@ ' 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.ComponentModel Imports System.Globalization Imports System.IO Imports System.Xml Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Test.Utilities -Imports Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics Imports Moq Imports Roslyn.Utilities Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Public Class MockExtensionManager - Private ReadOnly _contentType As String - Private ReadOnly _locations() As String + Private ReadOnly _extensions As (Paths As String(), Id As String)() - Public Sub New(contentType As String, ParamArray locations() As String) + Public Sub New(Optional extensions As (Paths As String(), Id As String)() = Nothing, + Optional contentType As String = "Microsoft.VisualStudio.Analyzer") _contentType = contentType - _locations = locations + _extensions = If(extensions, Array.Empty(Of (Paths As String(), Id As String))) End Sub - Public Function GetEnabledExtensionContentLocations(contentTypeName As String) As IEnumerable(Of String) - Assert.Equal(_contentType, contentTypeName) - Return _locations - End Function - Public Iterator Function GetEnabledExtensions(contentTypeName As String) As IEnumerable(Of Object) Assert.Equal(_contentType, contentTypeName) - For Each location In _locations - Dim installedExtensionMock As New Mock(Of IMockInstalledExtension)(MockBehavior.Strict) + For Each extension In _extensions + For Each extensionPath In extension.Paths + Dim installedExtensionMock As New Mock(Of IMockInstalledExtension)(MockBehavior.Strict) - installedExtensionMock.SetupGet(Function(m) m.Content).Returns( + installedExtensionMock.SetupGet(Function(m) m.Content).Returns( New MockContent() { - New MockContent(_contentType, location) + New MockContent(_contentType, extensionPath) }) - installedExtensionMock.Setup(Function(m) m.GetContentLocation(It.IsAny(Of MockContent))).Returns( + installedExtensionMock.Setup(Function(m) m.GetContentLocation(It.IsAny(Of MockContent))).Returns( Function(content As MockContent) If content.RelativePath.IndexOf("$RootFolder$") >= 0 Then Return content.RelativePath.Replace("$RootFolder$", Path.Combine(TempRoot.Root, "ResolvedRootFolder")) @@ -50,12 +44,13 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics End If End Function) - Dim headerMock As New Mock(Of IMockHeader)(MockBehavior.Strict) - headerMock.SetupGet(Function(h) h.LocalizedName).Returns("Vsix") + Dim headerMock As New Mock(Of IMockHeader)(MockBehavior.Strict) + headerMock.SetupGet(Function(h) h.Identifier).Returns(extension.Id) - installedExtensionMock.SetupGet(Function(m) m.Header).Returns(headerMock.Object) + installedExtensionMock.SetupGet(Function(m) m.Header).Returns(headerMock.Object) - Yield installedExtensionMock.Object + Yield installedExtensionMock.Object + Next Next End Function @@ -66,16 +61,16 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics End Interface Public Interface IMockHeader - ReadOnly Property LocalizedName As String + ReadOnly Property Identifier As String End Interface Public Class MockContent Private ReadOnly _contentType As String - Private ReadOnly _location As String + Private ReadOnly _path As String - Public Sub New(contentType As String, location As String) + Public Sub New(contentType As String, path As String) _contentType = contentType - _location = location + _path = path End Sub Public ReadOnly Property ContentTypeName As String @@ -86,7 +81,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Public ReadOnly Property RelativePath As String Get - Return _location + Return _path End Get End Property End Class diff --git a/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb b/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb new file mode 100644 index 0000000000000..2bca62d09f7d7 --- /dev/null +++ b/src/VisualStudio/TestUtilities2/MockVisualStudioDiagnosticAnalyzerProviderFactory.vb @@ -0,0 +1,27 @@ +' 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. + +Imports System.Composition +Imports System.Threading +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics + + + Friend Class MockVisualStudioDiagnosticAnalyzerProviderFactory + Implements IVisualStudioDiagnosticAnalyzerProviderFactory + + Public Property Extensions As (Paths As String(), Id As String)() = Array.Empty(Of (Paths As String(), Id As String))() + + + + 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))) + End Function + End Class +End Namespace diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb index 89bede4b5d7c8..cf93d91c88192 100644 --- a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb @@ -19,12 +19,14 @@ Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.Composition Imports Microsoft.VisualStudio.LanguageServices.Implementation Imports Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel +Imports Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics Imports Microsoft.VisualStudio.LanguageServices.Implementation.Library.ObjectBrowser.Lists Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.CPS Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Legacy Imports Microsoft.VisualStudio.LanguageServices.Implementation.TaskList Imports Microsoft.VisualStudio.LanguageServices.UnitTests.CodeModel +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Imports Microsoft.VisualStudio.Shell Imports Microsoft.VisualStudio.Shell.Interop @@ -57,6 +59,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Fr GetType(MockVisualStudioWorkspace), GetType(MetadataReferences.FileWatchedPortableExecutableReferenceFactory), GetType(VisualStudioProjectFactory), + GetType(MockVisualStudioDiagnosticAnalyzerProviderFactory), GetType(MockServiceProvider), GetType(SolutionEventsBatchScopeCreator), GetType(ProjectCodeModelFactory),