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;
        }
    }
}