File: src\Analyzers\Core\Analyzers\UseCoalesceExpression\AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer.cs
Web Access
Project: src\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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 Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
 
namespace Microsoft.CodeAnalysis.UseCoalesceExpression;
 
/// <summary>
/// Looks for code of the form "x == null ? y : x" and offers to convert it to "x ?? y";
/// </summary>
internal abstract class AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer<
    TSyntaxKind,
    TExpressionSyntax,
    TConditionalExpressionSyntax,
    TBinaryExpressionSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    where TSyntaxKind : struct
    where TExpressionSyntax : SyntaxNode
    where TConditionalExpressionSyntax : TExpressionSyntax
    where TBinaryExpressionSyntax : TExpressionSyntax
{
    protected AbstractUseCoalesceExpressionForTernaryConditionalCheckDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.UseCoalesceExpressionForTernaryConditionalCheckDiagnosticId,
               EnforceOnBuildValues.UseCoalesceExpression,
               CodeStyleOptions2.PreferCoalesceExpression,
               new LocalizableResourceString(nameof(AnalyzersResources.Use_coalesce_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
               new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    protected abstract ISyntaxFacts GetSyntaxFacts();
    protected abstract bool IsTargetTyped(SemanticModel semanticModel, TConditionalExpressionSyntax conditional, System.Threading.CancellationToken cancellationToken);
 
    protected override void InitializeWorker(AnalysisContext context)
    {
        var syntaxKinds = GetSyntaxFacts().SyntaxKinds;
        context.RegisterSyntaxNodeAction(AnalyzeSyntax,
            syntaxKinds.Convert<TSyntaxKind>(syntaxKinds.TernaryConditionalExpression));
    }
 
    private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
    {
        var cancellationToken = context.CancellationToken;
        var conditionalExpression = (TConditionalExpressionSyntax)context.Node;
 
        var option = context.GetAnalyzerOptions().PreferCoalesceExpression;
        if (!option.Value || ShouldSkipAnalysis(context, option.Notification))
            return;
 
        var syntaxFacts = GetSyntaxFacts();
        syntaxFacts.GetPartsOfConditionalExpression(
            conditionalExpression, out var conditionNode, out var whenTrueNodeHigh, out var whenFalseNodeHigh);
 
        conditionNode = syntaxFacts.WalkDownParentheses(conditionNode);
        var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh);
        var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh);
 
        if (conditionNode is not TBinaryExpressionSyntax condition)
            return;
 
        var syntaxKinds = syntaxFacts.SyntaxKinds;
        var isEquals = syntaxKinds.ReferenceEqualsExpression == condition.RawKind;
        var isNotEquals = syntaxKinds.ReferenceNotEqualsExpression == condition.RawKind;
        if (!isEquals && !isNotEquals)
            return;
 
        syntaxFacts.GetPartsOfBinaryExpression(condition, out var conditionLeftHigh, out var conditionRightHigh);
 
        var conditionLeftLow = syntaxFacts.WalkDownParentheses(conditionLeftHigh);
        var conditionRightLow = syntaxFacts.WalkDownParentheses(conditionRightHigh);
 
        var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeftLow);
        var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRightLow);
 
        if (conditionRightIsNull && conditionLeftIsNull)
        {
            // null == null    nothing to do here.
            return;
        }
 
        if (!conditionRightIsNull && !conditionLeftIsNull)
            return;
 
        if (!syntaxFacts.AreEquivalent(
                conditionRightIsNull ? conditionLeftLow : conditionRightLow,
                isEquals ? whenFalseNodeLow : whenTrueNodeLow))
        {
            return;
        }
 
        // Coalesce expression cannot be target typed.  So if we had a ternary that was target typed
        // that means the individual parts themselves had no best common type, which would not work
        // for a coalesce expression.
        var semanticModel = context.SemanticModel;
        if (IsTargetTyped(semanticModel, conditionalExpression, cancellationToken))
            return;
 
        var conditionType = semanticModel.GetTypeInfo(
            conditionLeftIsNull ? conditionRightLow : conditionLeftLow, cancellationToken).Type;
        if (conditionType != null &&
            !conditionType.IsReferenceType)
        {
            // Note: it is intentional that we do not support nullable types here.  If you have:
            //
            //  int? x;
            //  var z = x == null ? y : x;
            //  
            // then that's not the same as:   x ?? y.   ?? will unwrap the nullable, producing a 
            // int and not an int? like we have in the above code.  
            //
            // Note: we could look for:  x == null ? y : x.Value, and simplify that in the future.
            return;
        }
 
        var conditionPartToCheck = conditionRightIsNull ? conditionLeftHigh : conditionRightHigh;
        var whenPartToCheck = isEquals ? whenTrueNodeHigh : whenFalseNodeHigh;
        var locations = ImmutableArray.Create(
            conditionalExpression.GetLocation(),
            conditionPartToCheck.GetLocation(),
            whenPartToCheck.GetLocation());
 
        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            conditionalExpression.GetLocation(),
            option.Notification,
            context.Options,
            locations,
            properties: null));
    }
}