File: src\Analyzers\CSharp\CodeFixes\UseCompoundAssignment\CSharpUseCompoundCoalesceAssignmentCodeFixProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment;
 
using static CSharpSyntaxTokens;
using static SyntaxFactory;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCompoundCoalesceAssignment), Shared]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed class CSharpUseCompoundCoalesceAssignmentCodeFixProvider() : SyntaxEditorBasedCodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds { get; } =
        [IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId];
 
    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        RegisterCodeFix(context, AnalyzersResources.Use_compound_assignment, nameof(AnalyzersResources.Use_compound_assignment));
        return Task.CompletedTask;
    }
 
    protected override async Task FixAllAsync(
        Document document, ImmutableArray<Diagnostic> diagnostics,
        SyntaxEditor editor, CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var syntaxKinds = syntaxFacts.SyntaxKinds;
 
        foreach (var diagnostic in diagnostics)
        {
            var coalesceOrIfStatement = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
 
            if (coalesceOrIfStatement is IfStatementSyntax ifStatement)
            {
                Contract.ThrowIfFalse(CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.GetWhenTrueAssignment(
                    ifStatement, out var whenTrueStatement, out var assignment));
 
                // we have `if (x is null) x = y;`.  Update `x = y` to be `x ??= y`, then replace the entire
                // if-statement with that assignment statement.
                var newAssignment = AssignmentExpression(
                    SyntaxKind.CoalesceAssignmentExpression,
                    assignment.Left,
                    QuestionQuestionEqualsToken.WithTriviaFrom(assignment.OperatorToken),
                    assignment.Right).WithTriviaFrom(assignment);
 
                var newWhenTrueStatement = whenTrueStatement.ReplaceNode(assignment, newAssignment);
 
                // If there's leading trivia on the original inner statement, then combine that with the leading
                // trivia on the if-statement.  We'll need to add a formatting annotation so that the leading comments
                // are put in the right location.
                if (newWhenTrueStatement.GetLeadingTrivia().Any(t => t.IsSingleOrMultiLineComment()))
                {
                    newWhenTrueStatement = newWhenTrueStatement
                        .WithPrependedLeadingTrivia(ifStatement.GetLeadingTrivia())
                        .WithAdditionalAnnotations(Formatter.Annotation);
                }
                else
                {
                    newWhenTrueStatement = newWhenTrueStatement.WithLeadingTrivia(ifStatement.GetLeadingTrivia());
                }
 
                // If there's trailing comments on the original inner statement, then preserve that.  Otherwise,
                // replace it with the trailing trivia of hte original if-statement.
                if (!newWhenTrueStatement.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment()))
                    newWhenTrueStatement = newWhenTrueStatement.WithTrailingTrivia(ifStatement.GetTrailingTrivia());
 
                editor.ReplaceNode(ifStatement, newWhenTrueStatement);
            }
            else
            {
                var coalesce = coalesceOrIfStatement;
                // changing from `x ?? (x = y)` to `x ??= y` can change the type.  Specifically,
                // with nullable value types (`int?`) it could change from `int?` to `int`.
                //
                // Add an explicit cast to the original type to ensure semantics are preserved. 
                // Simplification engine can then remove it if it's not necessary.
                var type = semanticModel.GetTypeInfo(coalesce, cancellationToken).Type;
 
                editor.ReplaceNode(coalesce,
                    (currentCoalesceNode, generator) =>
                    {
                        var currentCoalesce = (BinaryExpressionSyntax)currentCoalesceNode;
                        var coalesceRight = (ParenthesizedExpressionSyntax)currentCoalesce.Right;
                        var assignment = (AssignmentExpressionSyntax)coalesceRight.Expression;
 
                        var compoundOperator = QuestionQuestionEqualsToken;
                        var finalAssignment = AssignmentExpression(
                            SyntaxKind.CoalesceAssignmentExpression,
                            assignment.Left,
                            compoundOperator.WithTriviaFrom(assignment.OperatorToken),
                            assignment.Right);
 
                        return type == null || type.IsErrorType()
                            ? finalAssignment
                            : generator.CastExpression(type, finalAssignment);
                    });
            }
        }
    }
}