diff --git a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs index 1c186eab3a7d8..34083b2e6f709 100644 --- a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs +++ b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs @@ -40,12 +40,11 @@ internal partial class CodeFixService : ICodeFixService private readonly IDiagnosticAnalyzerService _diagnosticService; private readonly ImmutableArray> _fixers; - private readonly Dictionary>> _fixersPerLanguageMap; + private readonly ImmutableDictionary>> _fixersPerLanguageMap; - private readonly ConditionalWeakTable, ImmutableDictionary>> _projectFixersMap = new(); + private readonly ConditionalWeakTable, ImmutableDictionary>> _projectFixersMap = new(); // Shared by project fixers and workspace fixers. - private readonly Lazy> _lazyFixerToMetadataMap; private readonly ConditionalWeakTable _analyzerReferenceToFixersMap = new(); private readonly ConditionalWeakTable.CreateValueCallback _createProjectCodeFixProvider = r => new ProjectCodeFixProvider(r); private readonly ImmutableDictionary>> _configurationProvidersMap; @@ -56,6 +55,7 @@ internal partial class CodeFixService : ICodeFixService private ImmutableDictionary> _fixerToFixableIdsMap = ImmutableDictionary>.Empty; private ImmutableDictionary _fixAllProviderMap = ImmutableDictionary.Empty; + private ImmutableDictionary _fixerToMetadataMap = ImmutableDictionary.Empty; [ImportingConstructor] [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] @@ -71,13 +71,9 @@ public CodeFixService( _fixers = fixers.ToImmutableArray(); _fixersPerLanguageMap = _fixers.ToPerLanguageMapWithMultipleLanguages(); - _lazyFixerToMetadataMap = new(() => fixers.Where(service => service.IsValueCreated).ToImmutableDictionary(service => service.Value, service => service.Metadata)); - _configurationProvidersMap = GetConfigurationProvidersPerLanguageMap(configurationProviders); } - private ImmutableDictionary FixerToMetadataMap => _lazyFixerToMetadataMap.Value; - public async Task GetMostSevereFixableDiagnosticAsync( Document document, TextSpan range, CancellationToken cancellationToken) { @@ -433,7 +429,7 @@ await AppendFixesOrConfigurationsAsync( getFixes: dxs => { var fixerName = fixer.GetType().Name; - FixerToMetadataMap.TryGetValue(fixer, out var fixerMetadata); + var fixerMetadata = TryGetMetadata(fixer); using (addOperationScope(fixerName)) using (RoslynEventSource.LogInformationalBlock(FunctionId.CodeFixes_GetCodeFixesAsync, fixerName, cancellationToken)) @@ -468,6 +464,27 @@ await AppendFixesOrConfigurationsAsync( } } + private CodeChangeProviderMetadata? TryGetMetadata(CodeFixProvider fixer) + { + return ImmutableInterlocked.GetOrAdd( + ref _fixerToMetadataMap, + fixer, + static (fixer, fixers) => + { + foreach (var lazy in fixers) + { + if (lazy.IsValueCreated && lazy.Value == fixer) + return lazy.Metadata; + } + + // Note: it feels very strange that we could ever not find a fixer in our list. However, this + // occurs in testing scenarios. I'm not sure if the tests represent a bogus potential input, or if + // this is something that can actually occur in practice and we want to keep working. + return null; + }, + _fixers); + } + private static async Task> GetCodeFixesAsync( Document document, TextSpan span, CodeFixProvider fixer, CodeChangeProviderMetadata? fixerMetadata, CodeActionOptions options, ImmutableArray diagnostics, @@ -611,7 +628,7 @@ await diagnosticsWithSameSpan.OrderByDescending(d => d.Severity) } // If the fix provider supports fix all occurrences, then get the corresponding FixAllProviderInfo and fix all context. - var fixAllProviderInfo = extensionManager.PerformFunction(fixer, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, fixer, FixAllProviderInfo.Create), defaultValue: null); + var fixAllProviderInfo = extensionManager.PerformFunction(fixer, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, fixer, FixAllProviderInfo.Create), defaultValue: null); FixAllState? fixAllState = null; var supportedScopes = ImmutableArray.Empty; @@ -840,7 +857,7 @@ private ImmutableDictionary>>(() => { - var mutableMap = new Dictionary>(); + using var _ = PooledDictionary>.GetInstance(out var mutableMap); foreach (var lazyFixer in lazyFixers) { @@ -856,18 +873,12 @@ private ImmutableDictionary new List()); + var list = mutableMap.GetOrAdd(id, static _ => ArrayBuilder.GetInstance()); list.Add(fixer); } } - var immutableMap = ImmutableDictionary.CreateBuilder>(); - foreach (var (diagnosticId, fixers) in mutableMap) - { - immutableMap.Add(diagnosticId, fixers.AsImmutableOrEmpty()); - } - - return immutableMap.ToImmutable(); + return mutableMap.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree()); }, isThreadSafe: true); fixerMap = fixerMap.Add(diagnosticId, lazyMap); @@ -890,7 +901,7 @@ private static ImmutableDictionary GetConfigurationFixProviders(List> languageKindAndFixers) + static ImmutableArray GetConfigurationFixProviders(ImmutableArray> languageKindAndFixers) { using var builderDisposer = ArrayBuilder.GetInstance(out var builder); var orderedLanguageKindAndFixers = ExtensionOrderer.Order(languageKindAndFixers); @@ -932,19 +943,19 @@ private ImmutableDictionary> GetProjectFixers(Project project) + private ImmutableDictionary> GetProjectFixers(Project project) { // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive return project.Solution.Workspace.Kind == WorkspaceKind.Interactive - ? ImmutableDictionary>.Empty + ? ImmutableDictionary>.Empty : _projectFixersMap.GetValue(project.AnalyzerReferences, _ => ComputeProjectFixers(project)); } - private ImmutableDictionary> ComputeProjectFixers(Project project) + private ImmutableDictionary> ComputeProjectFixers(Project project) { var extensionManager = project.Solution.Workspace.Services.GetService(); - var builder = ImmutableDictionary.CreateBuilder>(); + using var _ = PooledDictionary>.GetInstance(out var builder); foreach (var reference in project.AnalyzerReferences) { var projectCodeFixerProvider = _analyzerReferenceToFixersMap.GetValue(reference, _createProjectCodeFixProvider); @@ -956,13 +967,13 @@ private ImmutableDictionary> ComputeProjectF if (string.IsNullOrWhiteSpace(id)) continue; - var list = builder.GetOrAdd(id, static _ => new List()); + var list = builder.GetOrAdd(id, static _ => ArrayBuilder.GetInstance()); list.Add(fixer); } } } - return builder.ToImmutable(); + return builder.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree()); } } } diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ILanguageMetadataExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ILanguageMetadataExtensions.cs index b67e08139188d..2693c87039496 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ILanguageMetadataExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ILanguageMetadataExtensions.cs @@ -41,10 +41,10 @@ public static ImmutableDictionary new KeyValuePair>>(kvp.Key, kvp.Value.ToImmutableAndFree())).ToImmutableDictionary(); } - public static Dictionary>> ToPerLanguageMapWithMultipleLanguages(this IEnumerable> services) + public static ImmutableDictionary>> ToPerLanguageMapWithMultipleLanguages(this IEnumerable> services) where TMetadata : ILanguagesMetadata { - var map = new Dictionary>>(); + using var _ = PooledDictionary>>.GetInstance(out var map); foreach (var service in services) { @@ -52,13 +52,13 @@ public static Dictionary>> ToPerLanguag { if (!string.IsNullOrEmpty(language)) { - var list = map.GetOrAdd(language, _ => new List>()); + var list = map.GetOrAdd(language, _ => ArrayBuilder>.GetInstance()); list.Add(service); } } } - return map; + return map.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree()); } } }