File: Features\Diagnostics\EngineV2\DiagnosticIncrementalAnalyzer.StateManager.cs
Web Access
Project: ..\..\..\src\Features\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2
{
    internal partial class DiagnosticIncrementalAnalyzer
    {
        /// <summary>
        /// This is in charge of anything related to <see cref="StateSet"/>
        /// </summary>
        private partial class StateManager
        {
            private readonly Workspace _workspace;
            private readonly DiagnosticAnalyzerInfoCache _analyzerInfoCache;
 
            /// <summary>
            /// Analyzers supplied by the host (IDE). These are built-in to the IDE, the compiler, or from an installed IDE extension (VSIX). 
            /// Maps language name to the analyzers and their state.
            /// </summary>
            private ImmutableDictionary<HostAnalyzerStateSetKey, HostAnalyzerStateSets> _hostAnalyzerStateMap;
 
            /// <summary>
            /// Analyzers referenced by the project via a PackageReference.
            /// </summary>
            private readonly ConcurrentDictionary<ProjectId, ProjectAnalyzerStateSets> _projectAnalyzerStateMap;
 
            /// <summary>
            /// This will be raised whenever <see cref="StateManager"/> finds <see cref="Project.AnalyzerReferences"/> change
            /// </summary>
            public event EventHandler<ProjectAnalyzerReferenceChangedEventArgs>? ProjectAnalyzerReferenceChanged;
 
            public StateManager(Workspace workspace, DiagnosticAnalyzerInfoCache analyzerInfoCache)
            {
                _workspace = workspace;
                _analyzerInfoCache = analyzerInfoCache;
 
                _hostAnalyzerStateMap = ImmutableDictionary<HostAnalyzerStateSetKey, HostAnalyzerStateSets>.Empty;
                _projectAnalyzerStateMap = new ConcurrentDictionary<ProjectId, ProjectAnalyzerStateSets>(concurrencyLevel: 2, capacity: 10);
            }
 
            /// <summary>
            /// Return all <see cref="StateSet"/>.
            /// This will never create new <see cref="StateSet"/> but will return ones already created.
            /// </summary>
            public IEnumerable<StateSet> GetAllStateSets()
                => GetAllHostStateSets().Concat(GetAllProjectStateSets());
 
            /// <summary>
            /// Return <see cref="StateSet"/>s for the given <see cref="ProjectId"/>. 
            /// This will never create new <see cref="StateSet"/> but will return ones already created.
            /// </summary>
            public IEnumerable<StateSet> GetStateSets(ProjectId projectId)
            {
                var hostStateSets = GetAllHostStateSets();
 
                return _projectAnalyzerStateMap.TryGetValue(projectId, out var entry)
                    ? hostStateSets.Concat(entry.StateSetMap.Values)
                    : hostStateSets;
            }
 
            /// <summary>
            /// Return <see cref="StateSet"/>s for the given <see cref="Project"/>.
            /// This will never create new <see cref="StateSet"/> but will return ones already created.
            /// Difference with <see cref="GetStateSets(ProjectId)"/> is that 
            /// this will only return <see cref="StateSet"/>s that have same language as <paramref name="project"/>.
            /// </summary>
            public IEnumerable<StateSet> GetStateSets(Project project)
                => GetStateSets(project.Id).Where(s => s.Language == project.Language);
 
            /// <summary>
            /// Return <see cref="StateSet"/>s for the given <see cref="Project"/>. 
            /// This will either return already created <see cref="StateSet"/>s for the specific snapshot of <see cref="Project"/> or
            /// It will create new <see cref="StateSet"/>s for the <see cref="Project"/> and update internal state.
            /// 
            /// since this has a side-effect, this should never be called concurrently. and incremental analyzer (solution crawler) should guarantee that.
            /// </summary>
            public ImmutableArray<StateSet> GetOrUpdateStateSets(Project project)
            {
                var projectStateSets = GetOrUpdateProjectStateSets(project);
                return GetOrCreateHostStateSets(project, projectStateSets).OrderedStateSets.AddRange(projectStateSets.StateSetMap.Values);
            }
 
            /// <summary>
            /// Return <see cref="StateSet"/>s for the given <see cref="Project"/>. 
            /// This will either return already created <see cref="StateSet"/>s for the specific snapshot of <see cref="Project"/> or
            /// It will create new <see cref="StateSet"/>s for the <see cref="Project"/>.
            /// Unlike <see cref="GetOrUpdateStateSets(Project)"/>, this has no side effect.
            /// </summary>
            public IEnumerable<StateSet> GetOrCreateStateSets(Project project)
            {
                var projectStateSets = GetOrCreateProjectStateSets(project);
                return GetOrCreateHostStateSets(project, projectStateSets).OrderedStateSets.Concat(projectStateSets.StateSetMap.Values);
            }
 
            /// <summary>
            /// Return <see cref="StateSet"/> for the given <see cref="DiagnosticAnalyzer"/> in the context of <see cref="Project"/>.
            /// This will either return already created <see cref="StateSet"/> for the specific snapshot of <see cref="Project"/> or
            /// It will create new <see cref="StateSet"/> for the <see cref="Project"/>.
            /// This will not have any side effect.
            /// </summary>
            public StateSet? GetOrCreateStateSet(Project project, DiagnosticAnalyzer analyzer)
            {
                var projectStateSets = GetOrCreateProjectStateSets(project);
                if (projectStateSets.StateSetMap.TryGetValue(analyzer, out var stateSet))
                {
                    return stateSet;
                }
 
                var hostStateSetMap = GetOrCreateHostStateSets(project, projectStateSets).StateSetMap;
                if (hostStateSetMap.TryGetValue(analyzer, out stateSet))
                {
                    return stateSet;
                }
 
                return null;
            }
 
            /// <summary>
            /// Return <see cref="StateSet"/>s that are added as the given <see cref="Project"/>'s AnalyzerReferences.
            /// This will never create new <see cref="StateSet"/> but will return ones already created.
            /// </summary>
            public ImmutableArray<StateSet> CreateBuildOnlyProjectStateSet(Project project)
            {
                var projectStateSets = project.SupportsCompilation
                    ? GetOrUpdateProjectStateSets(project)
                    : ProjectAnalyzerStateSets.Default;
                var hostStateSets = GetOrCreateHostStateSets(project, projectStateSets);
 
                if (!project.SupportsCompilation)
                {
                    // languages which don't use our compilation model but diagnostic framework,
                    // all their analyzer should be host analyzers. return all host analyzers
                    // for the language
                    return hostStateSets.OrderedStateSets;
                }
 
                var hostStateSetMap = hostStateSets.StateSetMap;
 
                // create project analyzer reference identity map
                var projectAnalyzerReferenceIds = project.AnalyzerReferences.Select(r => r.Id).ToSet();
 
                // create build only stateSet array
                var stateSets = ImmutableArray.CreateBuilder<StateSet>();
 
                // include compiler analyzer in build only state, if available
                StateSet? compilerStateSet = null;
                var hostAnalyzers = project.Solution.State.Analyzers;
                var compilerAnalyzer = hostAnalyzers.GetCompilerDiagnosticAnalyzer(project.Language);
                if (compilerAnalyzer != null && hostStateSetMap.TryGetValue(compilerAnalyzer, out compilerStateSet))
                {
                    stateSets.Add(compilerStateSet);
                }
 
                // now add all project analyzers
                stateSets.AddRange(projectStateSets.StateSetMap.Values);
 
                // now add analyzers that exist in both host and project
                var hostAnalyzersById = hostAnalyzers.GetOrCreateHostDiagnosticAnalyzersPerReference(project.Language);
                foreach (var (identity, analyzers) in hostAnalyzersById)
                {
                    if (!projectAnalyzerReferenceIds.Contains(identity))
                    {
                        // it is from host analyzer package rather than project analyzer reference
                        // which build doesn't have
                        continue;
                    }
 
                    // if same analyzer exists both in host (vsix) and in analyzer reference,
                    // we include it in build only analyzer.
                    foreach (var analyzer in analyzers)
                    {
                        if (hostStateSetMap.TryGetValue(analyzer, out var stateSet) && stateSet != compilerStateSet)
                        {
                            stateSets.Add(stateSet);
                        }
                    }
                }
 
                return stateSets.ToImmutable();
            }
 
            /// <summary>
            /// Determines if any of the state sets in <see cref="GetAllHostStateSets()"/> match a specified predicate.
            /// </summary>
            /// <remarks>
            /// This method avoids the performance overhead of calling <see cref="GetAllHostStateSets()"/> for the
            /// specific case where the result is only used for testing if any element meets certain conditions.
            /// </remarks>
            public bool HasAnyHostStateSet<TArg>(Func<StateSet, TArg, bool> match, TArg arg)
            {
                foreach (var (_, hostStateSet) in _hostAnalyzerStateMap)
                {
                    foreach (var stateSet in hostStateSet.OrderedStateSets)
                    {
                        if (match(stateSet, arg))
                            return true;
                    }
                }
 
                return false;
            }
 
            /// <summary>
            /// Determines if any of the state sets in <see cref="_projectAnalyzerStateMap"/> for a specific project
            /// match a specified predicate.
            /// </summary>
            /// <remarks>
            /// <para>This method avoids the performance overhead of calling <see cref="GetStateSets(Project)"/> for the
            /// specific case where the result is only used for testing if any element meets certain conditions.</para>
            ///
            /// <para>Note that host state sets (i.e. ones retured by <see cref="GetAllHostStateSets()"/> are not tested
            /// by this method.</para>
            /// </remarks>
            public bool HasAnyProjectStateSet<TArg>(ProjectId projectId, Func<StateSet, TArg, bool> match, TArg arg)
            {
                if (_projectAnalyzerStateMap.TryGetValue(projectId, out var entry))
                {
                    foreach (var (_, stateSet) in entry.StateSetMap)
                    {
                        if (match(stateSet, arg))
                            return true;
                    }
                }
 
                return false;
            }
 
            public bool OnProjectRemoved(IEnumerable<StateSet> stateSets, ProjectId projectId)
            {
                var removed = false;
                foreach (var stateSet in stateSets)
                {
                    removed |= stateSet.OnProjectRemoved(projectId);
                }
 
                _projectAnalyzerStateMap.TryRemove(projectId, out _);
                return removed;
            }
 
            private void RaiseProjectAnalyzerReferenceChanged(ProjectAnalyzerReferenceChangedEventArgs args)
                => ProjectAnalyzerReferenceChanged?.Invoke(this, args);
 
            private static ImmutableDictionary<DiagnosticAnalyzer, StateSet> CreateStateSetMap(
                string language,
                IEnumerable<ImmutableArray<DiagnosticAnalyzer>> analyzerCollection,
                bool includeWorkspacePlaceholderAnalyzers)
            {
                var builder = ImmutableDictionary.CreateBuilder<DiagnosticAnalyzer, StateSet>();
 
                if (includeWorkspacePlaceholderAnalyzers)
                {
                    builder.Add(FileContentLoadAnalyzer.Instance, new StateSet(language, FileContentLoadAnalyzer.Instance, PredefinedBuildTools.Live));
                    builder.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance, new StateSet(language, GeneratorDiagnosticsPlaceholderAnalyzer.Instance, PredefinedBuildTools.Live));
                }
 
                foreach (var analyzers in analyzerCollection)
                {
                    foreach (var analyzer in analyzers)
                    {
                        Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
 
                        // TODO: 
                        // #1, all de-duplication should move to DiagnosticAnalyzerInfoCache
                        // #2, not sure whether de-duplication of analyzer itself makes sense. this can only happen
                        //     if user deliberately put same analyzer twice.
                        if (builder.ContainsKey(analyzer))
                        {
                            continue;
                        }
 
                        var buildToolName = analyzer.IsBuiltInAnalyzer() ?
                            PredefinedBuildTools.Live : analyzer.GetAnalyzerAssemblyName();
 
                        builder.Add(analyzer, new StateSet(language, analyzer, buildToolName));
                    }
                }
 
                return builder.ToImmutable();
            }
 
            [Conditional("DEBUG")]
            private static void VerifyUniqueStateNames(IEnumerable<StateSet> stateSets)
            {
                // Ensure diagnostic state name is indeed unique.
                var set = new HashSet<ValueTuple<string, string>>();
 
                foreach (var stateSet in stateSets)
                {
                    Contract.ThrowIfFalse(set.Add((stateSet.Language, stateSet.StateName)));
                }
            }
 
            [Conditional("DEBUG")]
            private void VerifyProjectDiagnosticStates(IEnumerable<StateSet> stateSets)
            {
                // We do not de-duplicate analyzer instances across host and project analyzers.
                var projectAnalyzers = stateSets.Select(state => state.Analyzer).ToImmutableHashSet();
 
                var hostStates = GetAllHostStateSets().Where(state => !projectAnalyzers.Contains(state.Analyzer));
 
                VerifyUniqueStateNames(hostStates.Concat(stateSets));
            }
 
            private readonly struct HostAnalyzerStateSetKey : IEquatable<HostAnalyzerStateSetKey>
            {
                public HostAnalyzerStateSetKey(string language, IReadOnlyList<AnalyzerReference> analyzerReferences)
                {
                    Language = language;
                    AnalyzerReferences = analyzerReferences;
                }
 
                public string Language { get; }
                public IReadOnlyList<AnalyzerReference> AnalyzerReferences { get; }
 
                public bool Equals(HostAnalyzerStateSetKey other)
                    => Language == other.Language && AnalyzerReferences == other.AnalyzerReferences;
 
                public override bool Equals(object? obj)
                    => obj is HostAnalyzerStateSetKey key && Equals(key);
 
                public override int GetHashCode()
                    => Hash.Combine(Language.GetHashCode(), AnalyzerReferences.GetHashCode());
            }
        }
    }
}