File: Features\Diagnostics\EngineV2\DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs
Web Access
Project: src\src\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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2
{
    internal partial class DiagnosticIncrementalAnalyzer
    {
        private partial class StateManager
        {
            private readonly struct ProjectAnalyzerStateSets
            {
                public static readonly ProjectAnalyzerStateSets Default = new(
                    [],
                    ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>>.Empty,
                    ImmutableDictionary<DiagnosticAnalyzer, StateSet>.Empty,
                    SkippedHostAnalyzersInfo.Empty);
 
                public readonly IReadOnlyList<AnalyzerReference> AnalyzerReferences;
 
                // maps analyzer reference id to list of analyzers loaded from the reference
                public readonly ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> MapPerReferences;
 
                public readonly ImmutableDictionary<DiagnosticAnalyzer, StateSet> StateSetMap;
 
                public readonly SkippedHostAnalyzersInfo SkippedAnalyzersInfo;
 
                internal ProjectAnalyzerStateSets(
                    IReadOnlyList<AnalyzerReference> analyzerReferences,
                    ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> mapPerReferences,
                    ImmutableDictionary<DiagnosticAnalyzer, StateSet> stateSetMap,
                    SkippedHostAnalyzersInfo skippedAnalyzersInfo)
                {
                    AnalyzerReferences = analyzerReferences;
                    MapPerReferences = mapPerReferences;
                    StateSetMap = stateSetMap;
                    SkippedAnalyzersInfo = skippedAnalyzersInfo;
                }
            }
 
            public IEnumerable<StateSet> GetAllProjectStateSets()
            {
                // return existing state sets
                // No need to use _projectAnalyzerStateMapGuard during reads of _projectAnalyzerStateMap
                return _projectAnalyzerStateMap.Values.SelectManyAsArray(e => e.StateSetMap.Values);
            }
 
            private ProjectAnalyzerStateSets? TryGetProjectStateSets(Project project)
            {
                // check if the analyzer references have changed since the last time we updated the map:
                // No need to use _projectAnalyzerStateMapGuard during reads of _projectAnalyzerStateMap
                if (_projectAnalyzerStateMap.TryGetValue(project.Id, out var entry) &&
                    entry.AnalyzerReferences.SequenceEqual(project.AnalyzerReferences))
                {
                    return entry;
                }
 
                return null;
            }
 
            private async Task<ProjectAnalyzerStateSets> GetOrCreateProjectStateSetsAsync(Project project, CancellationToken cancellationToken)
                => TryGetProjectStateSets(project) ?? await UpdateProjectStateSetsAsync(project, cancellationToken).ConfigureAwait(false);
 
            /// <summary>
            /// Creates a new project state sets.
            /// </summary>
            private ProjectAnalyzerStateSets CreateProjectStateSets(Project project)
            {
                if (project.AnalyzerReferences.Count == 0)
                {
                    return ProjectAnalyzerStateSets.Default;
                }
 
                var hostAnalyzers = project.Solution.SolutionState.Analyzers;
                var analyzersPerReference = hostAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(project);
                if (analyzersPerReference.Count == 0)
                {
                    return ProjectAnalyzerStateSets.Default;
                }
 
                var newMap = CreateStateSetMap(project.Language, analyzersPerReference.Values, [], includeWorkspacePlaceholderAnalyzers: false);
                var skippedAnalyzersInfo = project.GetSkippedAnalyzersInfo(_analyzerInfoCache);
                return new ProjectAnalyzerStateSets(project.AnalyzerReferences, analyzersPerReference, newMap, skippedAnalyzersInfo);
            }
 
            /// <summary>
            /// Updates the map to the given project snapshot.
            /// </summary>
            private async Task<ProjectAnalyzerStateSets> UpdateProjectStateSetsAsync(Project project, CancellationToken cancellationToken)
            {
                ProjectAnalyzerReferenceChangedEventArgs? analyzerReferenceChangedEventArgs = null;
                ProjectAnalyzerStateSets? projectStateSets;
 
                // This code is called concurrently for a project, so the guard prevents duplicated effort calculating StateSets.
                using (await _projectAnalyzerStateMapGuard.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
                {
                    projectStateSets = TryGetProjectStateSets(project);
 
                    if (projectStateSets == null)
                    {
                        projectStateSets = CreateProjectStateSets(project);
 
                        analyzerReferenceChangedEventArgs = GetProjectAnalyzerReferenceChangedEventArgs(project, projectStateSets.Value.MapPerReferences, projectStateSets.Value.StateSetMap);
 
                        // update cache. 
                        _projectAnalyzerStateMap = _projectAnalyzerStateMap.SetItem(project.Id, projectStateSets.Value);
                    }
                }
 
                if (analyzerReferenceChangedEventArgs != null)
                    RaiseProjectAnalyzerReferenceChanged(analyzerReferenceChangedEventArgs);
 
                return projectStateSets.Value;
            }
 
            private ProjectAnalyzerReferenceChangedEventArgs? GetProjectAnalyzerReferenceChangedEventArgs(
                Project project,
                ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> newMapPerReference,
                ImmutableDictionary<DiagnosticAnalyzer, StateSet> newMap)
            {
                // No need to use _projectAnalyzerStateMapGuard during reads of _projectAnalyzerStateMap
                if (!_projectAnalyzerStateMap.TryGetValue(project.Id, out var entry))
                {
                    // no previous references and we still don't have any references
                    if (newMap.Count == 0)
                    {
                        return null;
                    }
 
                    // new reference added
                    return new ProjectAnalyzerReferenceChangedEventArgs(project, newMap.Values.ToImmutableArrayOrEmpty(), []);
                }
 
                Debug.Assert(!entry.AnalyzerReferences.Equals(project.AnalyzerReferences));
 
                // there has been change. find out what has changed
                var addedStates = DiffStateSets(project.AnalyzerReferences.Except(entry.AnalyzerReferences), newMapPerReference, newMap);
                var removedStates = DiffStateSets(entry.AnalyzerReferences.Except(project.AnalyzerReferences), entry.MapPerReferences, entry.StateSetMap);
 
                // nothing has changed
                if (addedStates.Length == 0 && removedStates.Length == 0)
                {
                    return null;
                }
 
                return new ProjectAnalyzerReferenceChangedEventArgs(project, addedStates, removedStates);
            }
 
            private static ImmutableArray<StateSet> DiffStateSets(
                IEnumerable<AnalyzerReference> references,
                ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> mapPerReference,
                ImmutableDictionary<DiagnosticAnalyzer, StateSet> map)
            {
                if (mapPerReference.Count == 0 || map.Count == 0)
                {
                    // nothing to diff
                    return [];
                }
 
                var builder = ImmutableArray.CreateBuilder<StateSet>();
                foreach (var reference in references)
                {
                    // check duplication
                    if (!mapPerReference.TryGetValue(reference.Id, out var analyzers))
                    {
                        continue;
                    }
 
                    // okay, this is real reference. get stateset
                    foreach (var analyzer in analyzers)
                    {
                        if (!map.TryGetValue(analyzer, out var set))
                        {
                            continue;
                        }
 
                        builder.Add(set);
                    }
                }
 
                return builder.ToImmutableAndClear();
            }
        }
    }
}