File: src\Analyzers\Core\CodeFixes\UseConditionalExpression\ForAssignment\AbstractUseConditionalExpressionForAssignmentCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionCodeFixHelpers;
 
namespace Microsoft.CodeAnalysis.UseConditionalExpression;
 
internal abstract class AbstractUseConditionalExpressionForAssignmentCodeFixProvider<
    TStatementSyntax,
    TIfStatementSyntax,
    TLocalDeclarationStatementSyntax,
    TVariableDeclaratorSyntax,
    TExpressionSyntax,
    TConditionalExpressionSyntax>
    : AbstractUseConditionalExpressionCodeFixProvider<TStatementSyntax, TIfStatementSyntax, TExpressionSyntax, TConditionalExpressionSyntax>
    where TStatementSyntax : SyntaxNode
    where TIfStatementSyntax : TStatementSyntax
    where TLocalDeclarationStatementSyntax : TStatementSyntax
    where TVariableDeclaratorSyntax : SyntaxNode
    where TExpressionSyntax : SyntaxNode
    where TConditionalExpressionSyntax : TExpressionSyntax
{
    protected abstract TVariableDeclaratorSyntax WithInitializer(TVariableDeclaratorSyntax variable, TExpressionSyntax value);
    protected abstract TVariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator);
    protected abstract TLocalDeclarationStatementSyntax AddSimplificationToType(TLocalDeclarationStatementSyntax updatedLocalDeclaration);
 
    public override ImmutableArray<string> FixableDiagnosticIds
        => [IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId];
 
    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName)
            ? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check))
            : (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression));
 
        RegisterCodeFix(context, title, key);
        return Task.CompletedTask;
    }
 
    /// <summary>
    /// Returns 'true' if a multi-line conditional was created, and thus should be
    /// formatted specially.
    /// </summary>
    protected override async Task FixOneAsync(
        Document document, Diagnostic diagnostic,
        SyntaxEditor editor, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var ifStatement = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!;
 
        if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern(
                syntaxFacts, ifOperation, out var isRef,
                out var trueStatement, out var falseStatement,
                out var trueAssignment, out var falseAssignment))
        {
            return;
        }
 
        var conditionalExpression = await CreateConditionalExpressionAsync(
            document, ifOperation,
            trueStatement, falseStatement,
            trueAssignment?.Value ?? trueStatement,
            falseAssignment?.Value ?? falseStatement,
            isRef,
            formattingOptions,
            cancellationToken).ConfigureAwait(false);
 
        // See if we're assigning to a variable declared directly above the if statement. If so,
        // try to inline the conditional directly into the initializer for that variable.
        if (TryConvertWhenAssignmentToLocalDeclaredImmediateAbove(
                syntaxFacts, editor, ifOperation,
                trueAssignment, falseAssignment, conditionalExpression))
        {
            return;
 
        }
 
        // If not, just replace the if-statement with a single assignment of the new
        // conditional.
        ConvertOnlyIfToConditionalExpression(
            editor, ifOperation, (trueAssignment ?? falseAssignment)!, conditionalExpression);
    }
 
    private void ConvertOnlyIfToConditionalExpression(
        SyntaxEditor editor,
        IConditionalOperation ifOperation,
        ISimpleAssignmentOperation assignment,
        TExpressionSyntax conditionalExpression)
    {
        var generator = editor.Generator;
        var ifStatement = (TIfStatementSyntax)ifOperation.Syntax;
        var expressionStatement = (TStatementSyntax)generator.ExpressionStatement(
            generator.AssignmentStatement(
                assignment.Target.Syntax,
                conditionalExpression)).WithTriviaFrom(ifStatement);
 
        editor.ReplaceNode(
            ifOperation.Syntax,
            WrapWithBlockIfAppropriate(ifStatement, expressionStatement));
    }
 
    private bool TryConvertWhenAssignmentToLocalDeclaredImmediateAbove(
        ISyntaxFactsService syntaxFacts, SyntaxEditor editor, IConditionalOperation ifOperation,
        ISimpleAssignmentOperation? trueAssignment,
        ISimpleAssignmentOperation? falseAssignment,
        TExpressionSyntax conditionalExpression)
    {
        if (!TryFindMatchingLocalDeclarationImmediatelyAbove(
                ifOperation, trueAssignment, falseAssignment,
                out var localDeclarationOperation, out var declarator))
        {
            return false;
        }
 
        // We found a valid local declaration right above the if-statement.
        var localDeclaration = localDeclarationOperation.Syntax;
        var variable = GetDeclaratorSyntax(declarator);
 
        // Initialize that variable with the conditional expression.
        var updatedVariable = WithInitializer(variable, conditionalExpression);
 
        // Because we merged the initialization and the variable, the variable may now be able
        // to use 'var' (c#), or elide its type (vb).  Add the simplification annotation
        // appropriately so that can happen later down the line.
        var updatedLocalDeclaration = localDeclaration.ReplaceNode(variable, updatedVariable);
        updatedLocalDeclaration = AddSimplificationToType(
            (TLocalDeclarationStatementSyntax)updatedLocalDeclaration);
 
        editor.ReplaceNode(localDeclaration, updatedLocalDeclaration);
        editor.RemoveNode(ifOperation.Syntax, GetRemoveOptions(syntaxFacts, ifOperation.Syntax));
        return true;
    }
 
    private static bool TryFindMatchingLocalDeclarationImmediatelyAbove(
        IConditionalOperation ifOperation,
        ISimpleAssignmentOperation? trueAssignment,
        ISimpleAssignmentOperation? falseAssignment,
        [NotNullWhen(true)] out IVariableDeclarationGroupOperation? localDeclaration,
        [NotNullWhen(true)] out IVariableDeclaratorOperation? declarator)
    {
        localDeclaration = null;
        declarator = null;
 
        ILocalSymbol? local = null;
        if (trueAssignment != null)
        {
            if (trueAssignment.Target is not ILocalReferenceOperation trueLocal)
                return false;
 
            local = trueLocal.Local;
        }
 
        if (falseAssignment != null)
        {
            if (falseAssignment.Target is not ILocalReferenceOperation falseLocal)
                return false;
 
            // See if both assignments are to the same local.
            if (local != null && !Equals(local, falseLocal.Local))
                return false;
 
            local = falseLocal.Local;
        }
 
        // We weren't assigning to a local.
        if (local == null)
            return false;
 
        // If so, see if that local was declared immediately above the if-statement.
        if (ifOperation.Parent is not IBlockOperation parentBlock)
        {
            return false;
        }
 
        var ifIndex = parentBlock.Operations.IndexOf(ifOperation);
        if (ifIndex <= 0)
        {
            return false;
        }
 
        localDeclaration = parentBlock.Operations[ifIndex - 1] as IVariableDeclarationGroupOperation;
        if (localDeclaration == null)
        {
            return false;
        }
 
        if (localDeclaration.IsImplicit)
        {
            return false;
        }
 
        if (localDeclaration.Declarations.Length != 1)
        {
            return false;
        }
 
        var declaration = localDeclaration.Declarations[0];
        var declarators = declaration.Declarators;
        if (declarators.Length != 1)
        {
            return false;
        }
 
        declarator = declarators[0];
        var variable = declarator.Symbol;
        if (!Equals(variable, local))
        {
            // wasn't a declaration of the local we're assigning to.
            return false;
        }
 
        var variableInitializer = declarator.Initializer ?? declaration.Initializer;
        if (variableInitializer?.Value != null)
        {
            var unwrapped = variableInitializer.Value.UnwrapImplicitConversion();
            // the variable has to either not have an initializer, or it needs to be basic
            // literal/default expression.
            if (unwrapped is not ILiteralOperation and
                not IDefaultValueOperation)
            {
                return false;
            }
        }
 
        // If the variable is referenced in the condition of the 'if' block, we can't merge the
        // declaration and assignments.
        return !ReferencesLocalVariable(ifOperation.Condition, variable);
    }
 
    private static bool ReferencesLocalVariable(IOperation operation, ILocalSymbol variable)
    {
        if (operation is ILocalReferenceOperation localReference &&
            Equals(variable, localReference.Local))
        {
            return true;
        }
 
        foreach (var child in operation.ChildOperations)
        {
            if (ReferencesLocalVariable(child, variable))
            {
                return true;
            }
        }
 
        return false;
    }
}