File: src\roslyn\src\Analyzers\Core\Analyzers\UseConditionalExpression\UseConditionalExpressionHelpers.cs
Web Access
Project: src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.UseConditionalExpression;

internal static partial class UseConditionalExpressionHelpers
{
    public const string CanSimplifyName = nameof(CanSimplifyName);

    public static readonly ImmutableDictionary<string, string?> CanSimplifyProperties =
        ImmutableDictionary<string, string?>.Empty.Add(CanSimplifyName, CanSimplifyName);

    public static bool CanConvert(
        ISyntaxFacts syntaxFacts, IConditionalOperation ifOperation,
        IOperation whenTrue, IOperation whenFalse, CancellationToken cancellationToken)
    {
        // Will likely not work as intended if the if directive spans any preprocessor directives. So
        // do not offer for now.  Note: we pass in both the node for the ifOperation and the
        // whenFalse portion.  The whenFalse portion isn't necessary under the ifOperation.  For
        // example in:
        //
        //  ```c#
        //  #if DEBUG
        //  if (check)
        //      return 3;
        //  #endif  
        //  return 2;
        //  ```
        //
        // In this case, we want to see that this cross the `#endif`
        if (syntaxFacts.SpansPreprocessorDirective(ifOperation.Syntax, whenFalse.Syntax))
            return false;

        // User may have comments on the when-true/when-false statements.  These statements can
        // be very important. Often they indicate why the true/false branches are important in
        // the first place.  We don't have any place to put these, so we don't offer here.
        if (HasRegularComments(syntaxFacts, whenTrue.Syntax) ||
            HasRegularComments(syntaxFacts, whenFalse.Syntax))
        {
            return false;
        }

        // Can't convert if the two branches declare variables with the same name (for example, with an `out var`).
        // These will then collide when we convert to a conditional expression.
        var semanticModel = ifOperation.SemanticModel;
        Contract.ThrowIfNull(semanticModel);

        var whenTrueSymbols = semanticModel.GetAllDeclaredSymbols(whenTrue.Syntax, cancellationToken, n => !syntaxFacts.IsAnonymousOrLocalFunction(n));
        var whenFalseSymbols = semanticModel.GetAllDeclaredSymbols(whenFalse.Syntax, cancellationToken, n => !syntaxFacts.IsAnonymousOrLocalFunction(n));

        if (whenTrueSymbols.Select(s => s.Name).Intersect(whenFalseSymbols.Select(s => s.Name)).Any())
            return false;

        return true;
    }

    /// <summary>
    /// Will unwrap a block with a single statement in it to just that block.  Used so we can
    /// support both <c>if (expr) { statement }</c> and <c>if (expr) statement</c>
    /// </summary>
    [return: NotNullIfNotNull(nameof(statement))]
    public static IOperation? UnwrapSingleStatementBlock(IOperation? statement)
        => statement is IBlockOperation { Operations: [var operationInBlock] }
            ? operationInBlock
            : statement;

    public static bool HasRegularComments(ISyntaxFacts syntaxFacts, SyntaxNode syntax)
        => HasRegularCommentTrivia(syntaxFacts, syntax.GetLeadingTrivia()) ||
           HasRegularCommentTrivia(syntaxFacts, syntax.GetTrailingTrivia());

    public static bool HasRegularCommentTrivia(ISyntaxFacts syntaxFacts, SyntaxTriviaList triviaList)
    {
        foreach (var trivia in triviaList)
        {
            if (syntaxFacts.IsRegularComment(trivia))
                return true;
        }

        return false;
    }

    public static bool HasInconvertibleThrowStatement(
        ISyntaxFacts syntaxFacts, bool isRef,
        IThrowOperation? trueThrow, IThrowOperation? falseThrow)
    {
        // Can't convert to `x ? throw ... : throw ...` as there's no best common type between the two (even when
        // throwing the same exception type).
        if (trueThrow != null && falseThrow != null)
            return true;

        var anyThrow = trueThrow ?? falseThrow;

        if (anyThrow != null)
        {
            // can only convert to a conditional expression if the lang supports throw-exprs.
            if (!syntaxFacts.SupportsThrowExpression(anyThrow.Syntax.SyntaxTree.Options))
                return true;

            // `ref` can't be used with `throw`.
            if (isRef)
                return true;
        }

        return false;
    }

    public static bool IsBooleanLiteral(IOperation trueValue, bool val)
        => trueValue is ILiteralOperation { ConstantValue: { HasValue: true, Value: bool value } } && value == val;

    public static bool CanSimplify(IOperation trueValue, IOperation falseValue, bool isRef, out bool negate)
    {
        // If we are going to generate "expr ? true : false" then just generate "expr"
        // instead.
        //
        // If we are going to generate "expr ? false : true" then just generate "!expr"
        // instead.
        if (!isRef)
        {
            if (IsBooleanLiteral(trueValue, true) && IsBooleanLiteral(falseValue, false))
            {
                negate = false;
                return true;
            }

            if (IsBooleanLiteral(trueValue, false) && IsBooleanLiteral(falseValue, true))
            {
                negate = true;
                return true;
            }
        }

        negate = false;
        return false;
    }
}