diff --git a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs index c28c4dcc1c907..1c186eab3a7d8 100644 --- a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs +++ b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs @@ -39,25 +39,23 @@ internal partial class CodeFixService : ICodeFixService new((d1, d2) => DiagnosticId.CompareOrdinal(d1.Id, d2.Id)); private readonly IDiagnosticAnalyzerService _diagnosticService; + private readonly ImmutableArray> _fixers; + private readonly Dictionary>> _fixersPerLanguageMap; - private readonly Func>>>> _getWorkspaceFixersMap; - private ImmutableDictionary>>>? _lazyWorkspaceFixersMap; - private readonly ConditionalWeakTable, ImmutableDictionary>> _projectFixersMap; + private readonly ConditionalWeakTable, ImmutableDictionary>> _projectFixersMap = new(); // Shared by project fixers and workspace fixers. - private ImmutableDictionary> _fixerToFixableIdsMap = ImmutableDictionary>.Empty; private readonly Lazy> _lazyFixerToMetadataMap; + private readonly ConditionalWeakTable _analyzerReferenceToFixersMap = new(); + private readonly ConditionalWeakTable.CreateValueCallback _createProjectCodeFixProvider = r => new ProjectCodeFixProvider(r); + private readonly ImmutableDictionary>> _configurationProvidersMap; + private readonly ImmutableArray> _errorLoggers; - private readonly Func>>> _getFixerPriorityMap; + private ImmutableDictionary>>>? _lazyWorkspaceFixersMap; private ImmutableDictionary>>? _lazyFixerPriorityMap; - private readonly ConditionalWeakTable _analyzerReferenceToFixersMap; - private readonly ConditionalWeakTable.CreateValueCallback _createProjectCodeFixProvider; - - private readonly ImmutableDictionary>> _configurationProvidersMap; - private readonly IEnumerable> _errorLoggers; - - private ImmutableDictionary _fixAllProviderMap; + private ImmutableDictionary> _fixerToFixableIdsMap = ImmutableDictionary>.Empty; + private ImmutableDictionary _fixAllProviderMap = ImmutableDictionary.Empty; [ImportingConstructor] [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] @@ -67,24 +65,15 @@ public CodeFixService( [ImportMany] IEnumerable> fixers, [ImportMany] IEnumerable> configurationProviders) { - _errorLoggers = loggers; _diagnosticService = diagnosticAnalyzerService; + _errorLoggers = loggers.ToImmutableArray(); - _lazyFixerToMetadataMap = new(() => fixers.Where(service => service.IsValueCreated).ToImmutableDictionary(service => service.Value, service => service.Metadata)); - var fixersPerLanguageMap = fixers.ToPerLanguageMapWithMultipleLanguages(); - var configurationProvidersPerLanguageMap = configurationProviders.ToPerLanguageMapWithMultipleLanguages(); - - _getWorkspaceFixersMap = workspace => GetFixerPerLanguageMap(fixersPerLanguageMap, workspace); - _configurationProvidersMap = GetConfigurationProvidersPerLanguageMap(configurationProvidersPerLanguageMap); + _fixers = fixers.ToImmutableArray(); + _fixersPerLanguageMap = _fixers.ToPerLanguageMapWithMultipleLanguages(); - // REVIEW: currently, fixer's priority is statically defined by the fixer itself. might considering making it more dynamic or configurable. - _getFixerPriorityMap = workspace => GetFixerPriorityPerLanguageMap(fixersPerLanguageMap, workspace); + _lazyFixerToMetadataMap = new(() => fixers.Where(service => service.IsValueCreated).ToImmutableDictionary(service => service.Value, service => service.Metadata)); - // Per-project fixers - _projectFixersMap = new ConditionalWeakTable, ImmutableDictionary>>(); - _analyzerReferenceToFixersMap = new ConditionalWeakTable(); - _createProjectCodeFixProvider = new ConditionalWeakTable.CreateValueCallback(r => new ProjectCodeFixProvider(r)); - _fixAllProviderMap = ImmutableDictionary.Empty; + _configurationProvidersMap = GetConfigurationProvidersPerLanguageMap(configurationProviders); } private ImmutableDictionary FixerToMetadataMap => _lazyFixerToMetadataMap.Value; @@ -171,7 +160,7 @@ public async Task> GetFixesAsync( // group diagnostics by their diagnostics span // invariant: later code gathers & runs CodeFixProviders for diagnostics with one identical diagnostics span (that gets set later as CodeFixCollection's TextSpan) // order diagnostics by span. - SortedDictionary>? aggregatedDiagnostics = null; + var aggregatedDiagnostics = new SortedDictionary>(); // For 'CodeActionPriorityRequest.Normal' or 'CodeActionPriorityRequest.Low', we do not compute suppression/configuration fixes, // those fixes have a dedicated request priority 'CodeActionPriorityRequest.Lowest'. @@ -196,11 +185,10 @@ public async Task> GetFixesAsync( cancellationToken.ThrowIfCancellationRequested(); - aggregatedDiagnostics ??= new SortedDictionary>(); aggregatedDiagnostics.GetOrAdd(diagnostic.GetTextSpan(), _ => new List()).Add(diagnostic); } - if (aggregatedDiagnostics == null) + if (aggregatedDiagnostics.Count == 0) return ImmutableArray.Empty; // Order diagnostics by DiagnosticId so the fixes are in a deterministic order. @@ -240,10 +228,10 @@ int GetValue(CodeFixCollection c) { // Ensure that we do not register duplicate configuration fixes. using var _ = PooledHashSet.GetInstance(out var registeredConfigurationFixTitles); - foreach (var spanAndDiagnostic in aggregatedDiagnostics) + foreach (var (span, diagnosticList) in aggregatedDiagnostics) { await AppendConfigurationsAsync( - document, spanAndDiagnostic.Key, spanAndDiagnostic.Value, + document, span, diagnosticList, result, registeredConfigurationFixTitles, cancellationToken).ConfigureAwait(false); } } @@ -306,7 +294,7 @@ private bool TryGetWorkspaceFixersMap(Document document, [NotNullWhen(true)] out { if (_lazyWorkspaceFixersMap == null) { - var workspaceFixersMap = _getWorkspaceFixersMap(document.Project.Solution.Workspace); + var workspaceFixersMap = GetFixerPerLanguageMap(document.Project.Solution.Workspace); Interlocked.CompareExchange(ref _lazyWorkspaceFixersMap, workspaceFixersMap, null); } @@ -317,7 +305,7 @@ private bool TryGetWorkspaceFixersPriorityMap(Document document, [NotNullWhen(tr { if (_lazyFixerPriorityMap == null) { - var fixersPriorityByLanguageMap = _getFixerPriorityMap(document.Project.Solution.Workspace); + var fixersPriorityByLanguageMap = GetFixerPriorityPerLanguageMap(document.Project.Solution.Workspace); Interlocked.CompareExchange(ref _lazyFixerPriorityMap, fixersPriorityByLanguageMap, null); } @@ -802,8 +790,6 @@ private bool IsInteractiveCodeFixProvider(CodeFixProvider provider) AddImport.AbstractAddImportCodeFixProvider; } - private static readonly Func> s_createList = _ => new List(); - private ImmutableArray GetFixableDiagnosticIds(CodeFixProvider fixer, IExtensionManager? extensionManager) { // If we are passed a null extension manager it means we do not have access to a document so there is nothing to @@ -846,18 +832,17 @@ private static ImmutableArray GetAndTestFixableDiagnosticIds(CodeFixProv } private ImmutableDictionary>>> GetFixerPerLanguageMap( - Dictionary>> fixersPerLanguage, Workspace workspace) { var fixerMap = ImmutableDictionary.Create>>>(); var extensionManager = workspace.Services.GetService(); - foreach (var languageKindAndFixers in fixersPerLanguage) + foreach (var (diagnosticId, lazyFixers) in _fixersPerLanguageMap) { var lazyMap = new Lazy>>(() => { var mutableMap = new Dictionary>(); - foreach (var lazyFixer in languageKindAndFixers.Value) + foreach (var lazyFixer in lazyFixers) { if (!TryGetWorkspaceFixer(lazyFixer, workspace, logExceptionWithInfoBar: true, out var fixer)) { @@ -871,37 +856,39 @@ private ImmutableDictionary new List()); list.Add(fixer); } } var immutableMap = ImmutableDictionary.CreateBuilder>(); - foreach (var diagnosticIdAndFixers in mutableMap) + foreach (var (diagnosticId, fixers) in mutableMap) { - immutableMap.Add(diagnosticIdAndFixers.Key, diagnosticIdAndFixers.Value.AsImmutableOrEmpty()); + immutableMap.Add(diagnosticId, fixers.AsImmutableOrEmpty()); } return immutableMap.ToImmutable(); }, isThreadSafe: true); - fixerMap = fixerMap.Add(languageKindAndFixers.Key, lazyMap); + fixerMap = fixerMap.Add(diagnosticId, lazyMap); } return fixerMap; } private static ImmutableDictionary>> GetConfigurationProvidersPerLanguageMap( - Dictionary>> configurationProvidersPerLanguage) + IEnumerable> configurationProviders) { - var configurationFixerMap = ImmutableDictionary.Create>>(); - foreach (var languageKindAndFixers in configurationProvidersPerLanguage) + var configurationProvidersPerLanguageMap = configurationProviders.ToPerLanguageMapWithMultipleLanguages(); + + var configurationFixerMap = ImmutableDictionary.CreateBuilder>>(); + foreach (var (diagnosticId, lazyFixers) in configurationProvidersPerLanguageMap) { - var lazyConfigurationFixers = new Lazy>(() => GetConfigurationFixProviders(languageKindAndFixers.Value)); - configurationFixerMap = configurationFixerMap.Add(languageKindAndFixers.Key, lazyConfigurationFixers); + var lazyConfigurationFixers = new Lazy>(() => GetConfigurationFixProviders(lazyFixers)); + configurationFixerMap.Add(diagnosticId, lazyConfigurationFixers); } - return configurationFixerMap; + return configurationFixerMap.ToImmutable(); static ImmutableArray GetConfigurationFixProviders(List> languageKindAndFixers) { @@ -916,19 +903,17 @@ static ImmutableArray GetConfigurationFixProviders(Li } } - private ImmutableDictionary>> GetFixerPriorityPerLanguageMap( - Dictionary>> fixersPerLanguage, - Workspace workspace) + private ImmutableDictionary>> GetFixerPriorityPerLanguageMap(Workspace workspace) { var languageMap = ImmutableDictionary.CreateBuilder>>(); - foreach (var languageAndFixers in fixersPerLanguage) + foreach (var (diagnosticId, lazyFixers) in _fixersPerLanguageMap) { var lazyMap = new Lazy>(() => { var priorityMap = ImmutableDictionary.CreateBuilder(); - var lazyFixers = ExtensionOrderer.Order(languageAndFixers.Value); - for (var i = 0; i < lazyFixers.Count; i++) + var fixers = ExtensionOrderer.Order(lazyFixers); + for (var i = 0; i < fixers.Count; i++) { if (!TryGetWorkspaceFixer(lazyFixers[i], workspace, logExceptionWithInfoBar: false, out var fixer)) { @@ -941,7 +926,7 @@ private ImmutableDictionary> GetProjectFixer // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive return project.Solution.Workspace.Kind == WorkspaceKind.Interactive ? ImmutableDictionary>.Empty - : _projectFixersMap.GetValue(project.AnalyzerReferences, pId => ComputeProjectFixers(project)); + : _projectFixersMap.GetValue(project.AnalyzerReferences, _ => ComputeProjectFixers(project)); } private ImmutableDictionary> ComputeProjectFixers(Project project) { var extensionManager = project.Solution.Workspace.Services.GetService(); - ImmutableDictionary>.Builder? builder = null; + + var builder = ImmutableDictionary.CreateBuilder>(); foreach (var reference in project.AnalyzerReferences) { var projectCodeFixerProvider = _analyzerReferenceToFixersMap.GetValue(reference, _createProjectCodeFixProvider); @@ -968,22 +954,14 @@ private ImmutableDictionary> ComputeProjectF foreach (var id in fixableIds) { if (string.IsNullOrWhiteSpace(id)) - { continue; - } - builder ??= ImmutableDictionary.CreateBuilder>(); - var list = builder.GetOrAdd(id, s_createList); + var list = builder.GetOrAdd(id, static _ => new List()); list.Add(fixer); } } } - if (builder == null) - { - return ImmutableDictionary>.Empty; - } - return builder.ToImmutable(); } }