File: ExternalAccess\UnitTesting\SolutionCrawler\UnitTestingSolutionCrawlerRegistrationService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LegacySolutionEvents;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler
{
    [ExportWorkspaceService(typeof(IUnitTestingSolutionCrawlerRegistrationService), ServiceLayer.Host), Shared]
    internal sealed partial class UnitTestingSolutionCrawlerRegistrationService : IUnitTestingSolutionCrawlerRegistrationService
    {
        private const string Default = "*";
 
        private readonly object _gate = new();
        private readonly UnitTestingSolutionCrawlerProgressReporter _progressReporter = new();
 
        private readonly IAsynchronousOperationListener _listener;
        private readonly Dictionary<(string workspaceKind, SolutionServices services), UnitTestingWorkCoordinator> _documentWorkCoordinatorMap = [];
 
        private ImmutableDictionary<string, ImmutableArray<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>>> _analyzerProviders;
 
        /// <summary>
        /// The last solution we've heard about from the <see cref="ILegacySolutionEventsListener"/>.  This is used by
        /// our <see cref="UnitTestingWorkCoordinator"/> to represent the equivalent entity that <see
        /// cref="Workspace.CurrentSolution"/> normally represents.
        /// </summary>
        private Solution _lastReportedSolution = null!;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public UnitTestingSolutionCrawlerRegistrationService(
            [ImportMany] IEnumerable<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>> analyzerProviders,
            IAsynchronousOperationListenerProvider listenerProvider)
        {
            _analyzerProviders = analyzerProviders.GroupBy(kv => kv.Metadata.Name).ToImmutableDictionary(g => g.Key, g => g.ToImmutableArray());
            AssertAnalyzerProviders(_analyzerProviders);
 
            _listener = listenerProvider.GetListener(FeatureAttribute.SolutionCrawlerUnitTesting);
        }
 
        /// <summary>
        /// make sure solution cralwer is registered for the given workspace.
        /// </summary>
        public IUnitTestingWorkCoordinator Register(Solution solution)
        {
            var workspaceKind = solution.WorkspaceKind;
            var solutionServices = solution.Services;
            Contract.ThrowIfNull(workspaceKind);
 
            var correlationId = CorrelationIdFactory.GetNextId();
 
            UnitTestingWorkCoordinator? coordinator;
            lock (_gate)
            {
                _lastReportedSolution = solution;
                if (!_documentWorkCoordinatorMap.TryGetValue((workspaceKind, solutionServices), out coordinator))
                {
                    coordinator = new UnitTestingWorkCoordinator(
                        _listener,
                        GetAnalyzerProviders(workspaceKind),
                        new UnitTestingRegistration(this, correlationId, workspaceKind, solutionServices, _progressReporter));
 
                    _documentWorkCoordinatorMap.Add((workspaceKind, solutionServices), coordinator);
                }
            }
 
            UnitTestingSolutionCrawlerLogger.LogRegistration(correlationId, workspaceKind);
            return coordinator;
        }
 
        public bool HasRegisteredAnalyzerProviders
        {
            get
            {
                lock (_gate)
                {
                    return _analyzerProviders.Count > 0;
                }
            }
        }
 
        public void AddAnalyzerProvider(IUnitTestingIncrementalAnalyzerProvider provider, UnitTestingIncrementalAnalyzerProviderMetadata metadata)
        {
            // now update all existing work coordinator
            lock (_gate)
            {
                var lazyProvider = new Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>(() => provider, metadata);
 
                // update existing map for future solution crawler registration - no need for interlock but this makes add or update easier
                ImmutableInterlocked.AddOrUpdate(ref _analyzerProviders, metadata.Name, n => [lazyProvider], (n, v) => v.Add(lazyProvider));
 
                // assert map integrity
                AssertAnalyzerProviders(_analyzerProviders);
 
                // find existing coordinator to update
                var lazyProviders = _analyzerProviders[metadata.Name];
                foreach (var ((workspaceKind, solutionServices), coordinator) in _documentWorkCoordinatorMap)
                {
                    Contract.ThrowIfNull(workspaceKind);
 
                    if (!TryGetProvider(workspaceKind, lazyProviders, out var picked) || picked != lazyProvider)
                    {
                        // check whether new provider belong to current workspace
                        continue;
                    }
 
                    var analyzer = lazyProvider.Value.CreateIncrementalAnalyzer();
                    if (analyzer != null)
                    {
                        coordinator.AddAnalyzer(analyzer);
                    }
                }
            }
        }
 
        public void Reanalyze(string? workspaceKind, SolutionServices services, IUnitTestingIncrementalAnalyzer analyzer, IEnumerable<ProjectId>? projectIds, IEnumerable<DocumentId>? documentIds)
        {
            Contract.ThrowIfNull(workspaceKind);
 
            lock (_gate)
            {
                if (!_documentWorkCoordinatorMap.TryGetValue((workspaceKind, services), out var coordinator))
                {
                    // this can happen if solution crawler is already unregistered from workspace.
                    // one of those example will be VS shutting down so roslyn package is disposed but there is a pending
                    // async operation.
                    return;
                }
 
                // no specific projects or documents provided
                if (projectIds == null && documentIds == null)
                {
                    var solution = coordinator.Registration.GetSolutionToAnalyze();
                    coordinator.Reanalyze(analyzer, new UnitTestingReanalyzeScope(solution.Id));
                    return;
                }
 
                coordinator.Reanalyze(analyzer, new UnitTestingReanalyzeScope(projectIds, documentIds));
            }
        }
 
        private IEnumerable<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>> GetAnalyzerProviders(string workspaceKind)
        {
            foreach (var (_, lazyProviders) in _analyzerProviders)
            {
                // try get provider for the specific workspace kind
                if (TryGetProvider(workspaceKind, lazyProviders, out var lazyProvider))
                {
                    yield return lazyProvider;
                    continue;
                }
 
                // try get default provider
                if (TryGetProvider(Default, lazyProviders, out lazyProvider))
                {
                    yield return lazyProvider;
                }
            }
        }
 
        private static bool TryGetProvider(
            string kind,
            ImmutableArray<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>> lazyProviders,
            [NotNullWhen(true)] out Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>? lazyProvider)
        {
            // set out param
            lazyProvider = null;
 
            // try find provider for specific workspace kind
            if (kind != Default)
            {
                foreach (var provider in lazyProviders)
                {
                    if (provider.Metadata.WorkspaceKinds.Any(wk => wk == kind))
                    {
                        lazyProvider = provider;
                        return true;
                    }
                }
 
                return false;
            }
 
            // try find default provider
            foreach (var provider in lazyProviders)
            {
                if (IsDefaultProvider(provider.Metadata))
                {
                    lazyProvider = provider;
                    return true;
                }
 
                return false;
            }
 
            return false;
        }
 
        [Conditional("DEBUG")]
        private static void AssertAnalyzerProviders(
            ImmutableDictionary<string, ImmutableArray<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>>> analyzerProviders)
        {
#if DEBUG
            // make sure there is duplicated provider defined for same workspace.
            var set = new HashSet<string>();
            foreach (var kv in analyzerProviders)
            {
                foreach (var lazyProvider in kv.Value)
                {
                    if (IsDefaultProvider(lazyProvider.Metadata))
                    {
                        Debug.Assert(set.Add(Default));
                        continue;
                    }
 
                    foreach (var kind in lazyProvider.Metadata.WorkspaceKinds)
                    {
                        Debug.Assert(set.Add(kind));
                    }
                }
 
                set.Clear();
            }
#endif
        }
 
        private static bool IsDefaultProvider(UnitTestingIncrementalAnalyzerProviderMetadata providerMetadata)
            => providerMetadata.WorkspaceKinds is [];
 
        internal TestAccessor GetTestAccessor()
        {
            return new TestAccessor(this);
        }
 
        internal readonly struct TestAccessor
        {
            private readonly UnitTestingSolutionCrawlerRegistrationService _solutionCrawlerRegistrationService;
 
            internal TestAccessor(UnitTestingSolutionCrawlerRegistrationService solutionCrawlerRegistrationService)
            {
                _solutionCrawlerRegistrationService = solutionCrawlerRegistrationService;
            }
 
            internal ref ImmutableDictionary<string, ImmutableArray<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>>> AnalyzerProviders
                => ref _solutionCrawlerRegistrationService._analyzerProviders;
        }
 
        internal sealed class UnitTestingRegistration(
            UnitTestingSolutionCrawlerRegistrationService owner,
            int correlationId,
            string workspaceKind,
            SolutionServices solutionServices,
            UnitTestingSolutionCrawlerProgressReporter progressReporter)
        {
            private readonly UnitTestingSolutionCrawlerRegistrationService _owner = owner;
 
            public readonly int CorrelationId = correlationId;
            public readonly string WorkspaceKind = workspaceKind;
            public readonly SolutionServices Services = solutionServices;
            public readonly UnitTestingSolutionCrawlerProgressReporter ProgressReporter = progressReporter;
 
            public Solution GetSolutionToAnalyze()
            {
                lock (_owner._gate)
                    return _owner._lastReportedSolution;
            }
        }
    }
}