diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForArrayCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForArrayCodeFixProvider.cs index fbaa5d268d355..ef6b1078e3e9e 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForArrayCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForArrayCodeFixProvider.cs @@ -25,130 +25,99 @@ namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression; using static UseCollectionExpressionHelpers; [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionExpressionForArray), Shared] -internal partial class CSharpUseCollectionExpressionForArrayCodeFixProvider : SyntaxEditorBasedCodeFixProvider +internal partial class CSharpUseCollectionExpressionForArrayCodeFixProvider + : ForkingSyntaxEditorBasedCodeFixProvider { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpUseCollectionExpressionForArrayCodeFixProvider() + : base(CSharpCodeFixesResources.Use_collection_expression, + IDEDiagnosticIds.UseCollectionExpressionForArrayDiagnosticId) { } public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(IDEDiagnosticIds.UseCollectionExpressionForArrayDiagnosticId); - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Use_collection_expression, IDEDiagnosticIds.UseCollectionExpressionForArrayDiagnosticId); - return Task.CompletedTask; - } - - protected sealed override async Task FixAllAsync( + protected sealed override async Task FixAsync( Document document, - ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, + ExpressionSyntax arrayCreationExpression, + ImmutableDictionary properties, CancellationToken cancellationToken) { var services = document.Project.Solution.Services; var syntaxFacts = document.GetRequiredLanguageService(); var originalRoot = editor.OriginalRoot; - var arrayExpressions = new Stack(); - foreach (var diagnostic in diagnostics) + if (arrayCreationExpression + is not ArrayCreationExpressionSyntax + and not ImplicitArrayCreationExpressionSyntax + and not InitializerExpressionSyntax) { - var expression = (ExpressionSyntax)originalRoot.FindNode( - diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); - arrayExpressions.Push(expression); + return; } - // We're going to be continually editing this tree. Track all the nodes we - // care about so we can find them across each edit. - var semanticDocument = await SemanticDocument.CreateAsync( - document.WithSyntaxRoot(originalRoot.TrackNodes(arrayExpressions)), - cancellationToken).ConfigureAwait(false); - - while (arrayExpressions.Count > 0) + if (arrayCreationExpression is InitializerExpressionSyntax initializer) { - var arrayCreationExpression = semanticDocument.Root.GetCurrentNodes(arrayExpressions.Pop()).Single(); - if (arrayCreationExpression - is not ArrayCreationExpressionSyntax - and not ImplicitArrayCreationExpressionSyntax - and not InitializerExpressionSyntax) - { - continue; - } - - var subEditor = new SyntaxEditor(semanticDocument.Root, services); - - if (arrayCreationExpression is InitializerExpressionSyntax initializer) - { - subEditor.ReplaceNode( + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + editor.ReplaceNode( + initializer, + ConvertInitializerToCollectionExpression( initializer, - ConvertInitializerToCollectionExpression( - initializer, - IsOnSingleLine(semanticDocument.Text, initializer))); - } - else - { - var matches = GetMatches(semanticDocument, arrayCreationExpression); - if (matches.IsDefault) - continue; - - var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( - semanticDocument.Document, - fallbackOptions, - arrayCreationExpression, - matches, - static e => e switch - { - ArrayCreationExpressionSyntax arrayCreation => arrayCreation.Initializer, - ImplicitArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.Initializer, - _ => throw ExceptionUtilities.Unreachable(), - }, - static (e, i) => e switch - { - ArrayCreationExpressionSyntax arrayCreation => arrayCreation.WithInitializer(i), - ImplicitArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.WithInitializer(i), - _ => throw ExceptionUtilities.Unreachable(), - }, - cancellationToken).ConfigureAwait(false); - - subEditor.ReplaceNode(arrayCreationExpression, collectionExpression); - foreach (var match in matches) - subEditor.RemoveNode(match.Statement); - } - - semanticDocument = await semanticDocument.WithSyntaxRootAsync( - subEditor.GetChangedRoot(), cancellationToken).ConfigureAwait(false); + IsOnSingleLine(text, initializer))); + } + else + { + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var matches = GetMatches(semanticModel, arrayCreationExpression); + if (matches.IsDefault) + return; + + var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( + document, + fallbackOptions, + arrayCreationExpression, + matches, + static e => e switch + { + ArrayCreationExpressionSyntax arrayCreation => arrayCreation.Initializer, + ImplicitArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.Initializer, + _ => throw ExceptionUtilities.Unreachable(), + }, + static (e, i) => e switch + { + ArrayCreationExpressionSyntax arrayCreation => arrayCreation.WithInitializer(i), + ImplicitArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.WithInitializer(i), + _ => throw ExceptionUtilities.Unreachable(), + }, + cancellationToken).ConfigureAwait(false); + + editor.ReplaceNode(arrayCreationExpression, collectionExpression); + foreach (var match in matches) + editor.RemoveNode(match.Statement); } - editor.ReplaceNode(originalRoot, semanticDocument.Root); return; static bool IsOnSingleLine(SourceText sourceText, SyntaxNode node) => sourceText.AreOnSameLine(node.GetFirstToken(), node.GetLastToken()); - ImmutableArray GetMatches(SemanticDocument document, ExpressionSyntax expression) - { - switch (expression) + ImmutableArray GetMatches(SemanticModel semanticModel, ExpressionSyntax expression) + => expression switch { - case ImplicitArrayCreationExpressionSyntax: - // if we have `new[] { ... }` we have no subsequent matches to add to the collection. All values come - // from within the initializer. - return ImmutableArray.Empty; - - case ArrayCreationExpressionSyntax arrayCreation: - // we have `stackalloc T[...] ...;` defer to analyzer to find the items that follow that may need to - // be added to the collection expression. - return CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.TryGetMatches( - document.SemanticModel, arrayCreation, cancellationToken); - - default: - // We validated this is unreachable in the caller. - throw ExceptionUtilities.Unreachable(); - } - } + // if we have `new[] { ... }` we have no subsequent matches to add to the collection. All values come + // from within the initializer. + ImplicitArrayCreationExpressionSyntax + => ImmutableArray.Empty, + + // we have `stackalloc T[...] ...;` defer to analyzer to find the items that follow that may need to + // be added to the collection expression. + ArrayCreationExpressionSyntax arrayCreation + => CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.TryGetMatches(semanticModel, arrayCreation, cancellationToken), + + // We validated this is unreachable in the caller. + _ => throw ExceptionUtilities.Unreachable(), + }; } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs index 7dd1efcf7b46f..3a96e082c0c30 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs @@ -22,9 +22,9 @@ namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression; using static SyntaxFactory; [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionExpressionForEmpty), Shared] -internal partial class CSharpUseCollectionExpressionForEmptyCodeFixProvider : SyntaxEditorBasedCodeFixProvider +internal sealed partial class CSharpUseCollectionExpressionForEmptyCodeFixProvider : SyntaxEditorBasedCodeFixProvider { - private static readonly CollectionExpressionSyntax EmptyCollection = CollectionExpression(); + private static readonly CollectionExpressionSyntax s_emptyCollection = CollectionExpression(); [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -57,7 +57,7 @@ protected override async Task FixAllAsync( var expression = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken); editor.ReplaceNode( expression, - (current, _) => EmptyCollection.WithTriviaFrom(current)); + (current, _) => s_emptyCollection.WithTriviaFrom(current)); } return; diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocCodeFixProvider.cs index 78b1d3e6d6fea..50e1944b8f391 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpUseCollectionExpressionForStackAllocCodeFixProvider.cs @@ -3,10 +3,8 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; @@ -15,117 +13,81 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression; [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionExpressionForStackAlloc), Shared] -internal partial class CSharpUseCollectionExpressionForStackAllocCodeFixProvider : SyntaxEditorBasedCodeFixProvider +internal partial class CSharpUseCollectionExpressionForStackAllocCodeFixProvider + : ForkingSyntaxEditorBasedCodeFixProvider { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpUseCollectionExpressionForStackAllocCodeFixProvider() + : base(CSharpCodeFixesResources.Use_collection_expression, + IDEDiagnosticIds.UseCollectionExpressionForStackAllocDiagnosticId) { } public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(IDEDiagnosticIds.UseCollectionExpressionForStackAllocDiagnosticId); - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, CSharpCodeFixesResources.Use_collection_expression, IDEDiagnosticIds.UseCollectionExpressionForStackAllocDiagnosticId); - return Task.CompletedTask; - } - - protected sealed override async Task FixAllAsync( + protected sealed override async Task FixAsync( Document document, - ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, + ExpressionSyntax stackAllocExpression, + ImmutableDictionary properties, CancellationToken cancellationToken) { - var services = document.Project.Solution.Services; - var syntaxFacts = document.GetRequiredLanguageService(); - var originalRoot = editor.OriginalRoot; - - var stackallocExpressions = new Stack(); - foreach (var diagnostic in diagnostics) - { - var expression = (ExpressionSyntax)originalRoot.FindNode( - diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); - stackallocExpressions.Push(expression); - } - - // We're going to be continually editing this tree. Track all the nodes we - // care about so we can find them across each edit. - var semanticDocument = await SemanticDocument.CreateAsync( - document.WithSyntaxRoot(originalRoot.TrackNodes(stackallocExpressions)), + if (stackAllocExpression is not StackAllocArrayCreationExpressionSyntax and not ImplicitStackAllocArrayCreationExpressionSyntax) + return; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var matches = GetMatches(); + if (matches.IsDefault) + return; + + var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( + document, + fallbackOptions, + stackAllocExpression, + matches, + static e => e switch + { + StackAllocArrayCreationExpressionSyntax arrayCreation => arrayCreation.Initializer, + ImplicitStackAllocArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.Initializer, + _ => throw ExceptionUtilities.Unreachable(), + }, + static (e, i) => e switch + { + StackAllocArrayCreationExpressionSyntax arrayCreation => arrayCreation.WithInitializer(i), + ImplicitStackAllocArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.WithInitializer(i), + _ => throw ExceptionUtilities.Unreachable(), + }, cancellationToken).ConfigureAwait(false); - while (stackallocExpressions.Count > 0) - { - var stackAllocExpression = semanticDocument.Root.GetCurrentNodes(stackallocExpressions.Pop()).Single(); - if (stackAllocExpression is not StackAllocArrayCreationExpressionSyntax and not ImplicitStackAllocArrayCreationExpressionSyntax) - continue; - - var matches = GetMatches(semanticDocument, stackAllocExpression); - if (matches.IsDefault) - continue; - - var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync( - semanticDocument.Document, - fallbackOptions, - stackAllocExpression, - matches, - static e => e switch - { - StackAllocArrayCreationExpressionSyntax arrayCreation => arrayCreation.Initializer, - ImplicitStackAllocArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.Initializer, - _ => throw ExceptionUtilities.Unreachable(), - }, - static (e, i) => e switch - { - StackAllocArrayCreationExpressionSyntax arrayCreation => arrayCreation.WithInitializer(i), - ImplicitStackAllocArrayCreationExpressionSyntax implicitArrayCreation => implicitArrayCreation.WithInitializer(i), - _ => throw ExceptionUtilities.Unreachable(), - }, - cancellationToken).ConfigureAwait(false); - - var subEditor = new SyntaxEditor(semanticDocument.Root, services); - subEditor.ReplaceNode(stackAllocExpression, collectionExpression); + editor.ReplaceNode(stackAllocExpression, collectionExpression); - foreach (var match in matches) - subEditor.RemoveNode(match.Statement); + foreach (var match in matches) + editor.RemoveNode(match.Statement); - semanticDocument = await semanticDocument.WithSyntaxRootAsync( - subEditor.GetChangedRoot(), cancellationToken).ConfigureAwait(false); - } - - editor.ReplaceNode(originalRoot, semanticDocument.Root); return; - ImmutableArray GetMatches(SemanticDocument document, ExpressionSyntax stackAllocExpression) - { - switch (stackAllocExpression) + ImmutableArray GetMatches() + => stackAllocExpression switch { - case ImplicitStackAllocArrayCreationExpressionSyntax: - // if we have `stackalloc[] { ... }` we have no subsequent matches to add to the collection. All values come - // from within the initializer. - return ImmutableArray.Empty; - - case StackAllocArrayCreationExpressionSyntax arrayCreation: - // we have `stackalloc T[...] ...;` defer to analyzer to find the items that follow that may need to - // be added to the collection expression. - return CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.TryGetMatches( - document.SemanticModel, arrayCreation, cancellationToken); - - default: - // We validated this is unreachable in the caller. - throw ExceptionUtilities.Unreachable(); - } - } + // if we have `stackalloc[] { ... }` we have no subsequent matches to add to the collection. All values come + // from within the initializer. + ImplicitStackAllocArrayCreationExpressionSyntax + => ImmutableArray.Empty, + + // we have `stackalloc T[...] ...;` defer to analyzer to find the items that follow that may need to + // be added to the collection expression. + StackAllocArrayCreationExpressionSyntax arrayCreation + => CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.TryGetMatches(semanticModel, arrayCreation, cancellationToken), + + // We validated this is unreachable in the caller. + _ => throw ExceptionUtilities.Unreachable(), + }; } } diff --git a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs index 264a002452e47..696db4779be2c 100644 --- a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs @@ -2,9 +2,7 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Analyzers.UseCollectionInitializer; @@ -12,7 +10,6 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.ExtractMethod; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -31,7 +28,7 @@ internal abstract class AbstractUseCollectionInitializerCodeFixProvider< TIfStatementSyntax, TVariableDeclaratorSyntax, TAnalyzer> - : SyntaxEditorBasedCodeFixProvider + : ForkingSyntaxEditorBasedCodeFixProvider where TSyntaxKind : struct where TExpressionSyntax : SyntaxNode where TStatementSyntax : SyntaxNode @@ -54,6 +51,12 @@ internal abstract class AbstractUseCollectionInitializerCodeFixProvider< TVariableDeclaratorSyntax, TAnalyzer>, new() { + protected AbstractUseCollectionInitializerCodeFixProvider() + : base(AnalyzersResources.Collection_initialization_can_be_simplified, + nameof(AnalyzersResources.Collection_initialization_can_be_simplified)) + { + } + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(IDEDiagnosticIds.UseCollectionInitializerDiagnosticId); @@ -62,80 +65,41 @@ public sealed override ImmutableArray FixableDiagnosticIds protected abstract Task GetNewStatementAsync( Document document, CodeActionOptionsProvider fallbackOptions, TStatementSyntax statement, TObjectCreationExpressionSyntax objectCreation, bool useCollectionExpression, ImmutableArray> matches, CancellationToken cancellationToken); - protected sealed override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) - => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - - public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) - { - RegisterCodeFix(context, AnalyzersResources.Collection_initialization_can_be_simplified, nameof(AnalyzersResources.Collection_initialization_can_be_simplified)); - return Task.CompletedTask; - } - - protected sealed override async Task FixAllAsync( + protected sealed override async Task FixAsync( Document document, - ImmutableArray diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, + TObjectCreationExpressionSyntax objectCreation, + ImmutableDictionary properties, CancellationToken cancellationToken) { - // Fix-All for this feature is somewhat complicated. As Collection-Initializers - // could be arbitrarily nested, we have to make sure that any edits we make - // to one Collection-Initializer are seen by any higher ones. In order to do this - // we actually process each object-creation-node, one at a time, rewriting - // the tree for each node. In order to do this effectively, we use the '.TrackNodes' - // feature to keep track of all the object creation nodes as we make edits to - // the tree. If we didn't do this, then we wouldn't be able to find the - // second object-creation-node after we make the edit for the first one. + // Fix-All for this feature is somewhat complicated. As Collection-Initializers could be arbitrarily + // nested, we have to make sure that any edits we make to one Collection-Initializer are seen by any higher + // ones. In order to do this we actually process each object-creation-node, one at a time, rewriting the + // tree for each node. In order to do this effectively, we use the '.TrackNodes' feature to keep track of + // all the object creation nodes as we make edits to the tree. If we didn't do this, then we wouldn't be + // able to find the second object-creation-node after we make the edit for the first one. var syntaxFacts = document.GetRequiredLanguageService(); - var originalRoot = editor.OriginalRoot; - - var originalObjectCreationNodes = new Stack<(TObjectCreationExpressionSyntax objectCreationExpression, bool useCollectionExpression)>(); - foreach (var diagnostic in diagnostics) - { - var objectCreation = (TObjectCreationExpressionSyntax)originalRoot.FindNode( - diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); - originalObjectCreationNodes.Push((objectCreation, diagnostic.Properties?.ContainsKey(UseCollectionInitializerHelpers.UseCollectionExpressionName) is true)); - } - - var solutionServices = document.Project.Solution.Services; - - // We're going to be continually editing this tree. Track all the nodes we - // care about so we can find them across each edit. - var semanticDocument = await SemanticDocument.CreateAsync( - document.WithSyntaxRoot(originalRoot.TrackNodes(originalObjectCreationNodes.Select(static t => t.objectCreationExpression))), - cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); using var analyzer = GetAnalyzer(); - while (originalObjectCreationNodes.Count > 0) - { - var (originalObjectCreation, useCollectionExpression) = originalObjectCreationNodes.Pop(); - var currentRoot = semanticDocument.Root; - var objectCreation = currentRoot.GetCurrentNodes(originalObjectCreation).Single(); - - var matches = analyzer.Analyze( - semanticDocument.SemanticModel, syntaxFacts, objectCreation, useCollectionExpression, cancellationToken); - - if (matches.IsDefault) - continue; - - var statement = objectCreation.FirstAncestorOrSelf(); - Contract.ThrowIfNull(statement); - - var newStatement = await GetNewStatementAsync( - semanticDocument.Document, fallbackOptions, statement, objectCreation, useCollectionExpression, matches, cancellationToken).ConfigureAwait(false); + var useCollectionExpression = properties.ContainsKey(UseCollectionInitializerHelpers.UseCollectionExpressionName) is true; + var matches = analyzer.Analyze( + semanticModel, syntaxFacts, objectCreation, useCollectionExpression, cancellationToken); - var subEditor = new SyntaxEditor(currentRoot, solutionServices); + if (matches.IsDefault) + return; - subEditor.ReplaceNode(statement, newStatement); - foreach (var match in matches) - subEditor.RemoveNode(match.Statement, SyntaxRemoveOptions.KeepUnbalancedDirectives); + var statement = objectCreation.FirstAncestorOrSelf(); + Contract.ThrowIfNull(statement); - semanticDocument = await semanticDocument.WithSyntaxRootAsync( - subEditor.GetChangedRoot(), cancellationToken).ConfigureAwait(false); - } + var newStatement = await GetNewStatementAsync( + document, fallbackOptions, statement, objectCreation, useCollectionExpression, matches, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(originalRoot, semanticDocument.Root); + editor.ReplaceNode(statement, newStatement); + foreach (var match in matches) + editor.RemoveNode(match.Statement, SyntaxRemoveOptions.KeepUnbalancedDirectives); } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/ForkingSyntaxEditorBasedCodeFixProvider.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/ForkingSyntaxEditorBasedCodeFixProvider.cs new file mode 100644 index 0000000000000..7efa68191bf83 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/ForkingSyntaxEditorBasedCodeFixProvider.cs @@ -0,0 +1,115 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CodeFixes; + +/// +/// Helper type for s that need to provide 'fix all' support in a document, by operate by +/// applying one fix at a time, then recomputing the work to do after that fix is applied. While this is not generally +/// desirable from a performance perspective (due to the costs of forking a document after each fix), it is sometimes +/// necessary as individual fixes can impact the code so substantially that successive fixes may no longer apply, or may +/// have dramatically different data to work with before the fix. For example, if one fix removes statements entirely +/// that another fix was contained in. +/// +internal abstract class ForkingSyntaxEditorBasedCodeFixProvider + : SyntaxEditorBasedCodeFixProvider + where TDiagnosticNode : SyntaxNode +{ + private readonly string _title; + private readonly string _equivalenceKey; + + protected ForkingSyntaxEditorBasedCodeFixProvider( + string title, string equivalenceKey) + { + _title = title; + _equivalenceKey = equivalenceKey; + } + + /// + /// Subclasses must override this to actually provide the fix for a particular diagnostic. The implementation will + /// be passed the current (containing the changes from all prior fixes), the + /// the in that document, for the current diagnostic being fixed. And the for that diagnostic. The diagnostic itself is not passed along as it was + /// computed with respect to the original user document, and as such its and will not be correct. + /// + protected abstract Task FixAsync( + Document document, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + TDiagnosticNode diagnosticNode, + ImmutableDictionary properties, + CancellationToken cancellationToken); + + protected sealed override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + // Never try to fix the secondary diagnostics that were produced just to fade out code. + => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + RegisterCodeFix(context, _title, _equivalenceKey); + return Task.CompletedTask; + } + + protected sealed override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) + { + var originalRoot = editor.OriginalRoot; + + var originalNodes = new Stack<(TDiagnosticNode diagnosticNode, Diagnostic diagnostic)>(); + foreach (var diagnostic in diagnostics) + { + var diagnosticNode = (TDiagnosticNode)originalRoot.FindNode( + diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); + originalNodes.Push((diagnosticNode, diagnostic)); + } + + var solutionServices = document.Project.Solution.Services; + + // We're going to be continually editing this tree. Track all the nodes we care about so we can find them + // across each edit. + var semanticDocument = await SemanticDocument.CreateAsync( + document.WithSyntaxRoot(originalRoot.TrackNodes(originalNodes.Select(static t => t.diagnosticNode))), + cancellationToken).ConfigureAwait(false); + + while (originalNodes.Count > 0) + { + var (originalDiagnosticNode, diagnostic) = originalNodes.Pop(); + var currentRoot = semanticDocument.Root; + var diagnosticNode = currentRoot.GetCurrentNodes(originalDiagnosticNode).Single(); + + var subEditor = new SyntaxEditor(currentRoot, solutionServices); + + await FixAsync( + semanticDocument.Document, + subEditor, + fallbackOptions, + diagnosticNode, + diagnostic.Properties, + cancellationToken).ConfigureAwait(false); + + var changedRoot = subEditor.GetChangedRoot(); + if (currentRoot == changedRoot) + continue; + + semanticDocument = await semanticDocument.WithSyntaxRootAsync( + changedRoot, cancellationToken).ConfigureAwait(false); + } + + editor.ReplaceNode(originalRoot, semanticDocument.Root); + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems index e5c33a810526d..4b8f0541d3101 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems @@ -12,6 +12,7 @@ +