File: src\roslyn\src\Analyzers\Core\Analyzers\UseCoalesceExpression\AbstractUseCoalesceExpressionForIfNullCheckDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\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.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Analyzers.UseCoalesceExpression;

internal abstract class AbstractUseCoalesceExpressionForIfNullStatementCheckDiagnosticAnalyzer<
    TSyntaxKind,
    TExpressionSyntax,
    TStatementSyntax,
    TVariableDeclarator,
    TIfStatementSyntax>() : AbstractBuiltInCodeStyleDiagnosticAnalyzer(
        IDEDiagnosticIds.UseCoalesceExpressionForIfNullCheckDiagnosticId,
        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)))
    where TSyntaxKind : struct
    where TExpressionSyntax : SyntaxNode
    where TStatementSyntax : SyntaxNode
    where TVariableDeclarator : SyntaxNode
    where TIfStatementSyntax : TStatementSyntax
{
    protected abstract TSyntaxKind IfStatementKind { get; }
    protected abstract ISyntaxFacts SyntaxFacts { get; }

    protected abstract bool IsSingle(TVariableDeclarator declarator);
    protected abstract bool IsNullCheck(TExpressionSyntax condition, [NotNullWhen(true)] out TExpressionSyntax? checkedExpression);
    protected abstract bool HasElseBlock(TIfStatementSyntax ifStatement);

    protected abstract SyntaxNode GetDeclarationNode(TVariableDeclarator declarator);
    protected abstract TExpressionSyntax GetConditionOfIfStatement(TIfStatementSyntax ifStatement);
    protected abstract bool TryGetEmbeddedStatement(TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TStatementSyntax? whenTrueStatement);

    protected abstract TStatementSyntax? TryGetPreviousStatement(TIfStatementSyntax ifStatement);

    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterSyntaxNodeAction(AnalyzeSyntax, this.IfStatementKind);

    private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
    {
        var cancellationToken = context.CancellationToken;
        var ifStatement = (TIfStatementSyntax)context.Node;
        var semanticModel = context.SemanticModel;

        var option = context.GetAnalyzerOptions().PreferCoalesceExpression;
        if (!option.Value || ShouldSkipAnalysis(context, option.Notification))
            return;

        var syntaxFacts = this.SyntaxFacts;
        var condition = GetConditionOfIfStatement(ifStatement);

        if (!IsNullCheck(condition, out var checkedExpression))
            return;

        var previousStatement = TryGetPreviousStatement(ifStatement);
        if (previousStatement is null)
            return;

        if (HasElseBlock(ifStatement))
            return;

        if (!TryGetEmbeddedStatement(ifStatement, out var whenTrueStatement))
            return;

        if (syntaxFacts.IsThrowStatement(whenTrueStatement))
        {
            if (!syntaxFacts.SupportsThrowExpression(ifStatement.SyntaxTree.Options))
                return;

            var thrownExpression = syntaxFacts.GetExpressionOfThrowStatement(whenTrueStatement);
            if (thrownExpression is null)
                return;
        }

        if (syntaxFacts.ContainsInterleavedDirective([previousStatement, ifStatement], cancellationToken))
            return;

        // Don't offer the refactoring if the if-statement has directives that would be lost when we remove it.
        if (ifStatement.GetFirstToken().ContainsDirectives)
            return;

        // Same with the inner statement we're removing.
        if (whenTrueStatement.GetFirstToken().ContainsDirectives)
            return;

        TExpressionSyntax? expressionToCoalesce;

        if (syntaxFacts.IsLocalDeclarationStatement(previousStatement))
        {
            // var v = Expr();
            // if (v == null)
            //    ...

            if (!AnalyzeLocalDeclarationForm(previousStatement, out expressionToCoalesce))
                return;
        }
        else if (syntaxFacts.IsSimpleAssignmentStatement(previousStatement))
        {
            // v = Expr();
            // if (v == null)
            //    ...
            if (!AnalyzeAssignmentForm(previousStatement, out expressionToCoalesce))
                return;
        }
        else
        {
            return;
        }

        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            ifStatement.GetFirstToken().GetLocation(),
            option.Notification,
            context.Options,
            [expressionToCoalesce.GetLocation(),
                ifStatement.GetLocation(),
                whenTrueStatement.GetLocation()],
            properties: null));

        return;

        bool CheckExpression([NotNullWhen(true)] TExpressionSyntax? expression)
        {
            if (expression is null)
                return false;

            // if 'Expr()' is a value type, we can't use `??` on it.
            var exprType = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
            if (exprType is null)
                return false;

            if (exprType.IsNonNullableValueType())
                return false;

            // ?? can't be used on a pointer of any sort.
            if (exprType is IPointerTypeSymbol)
                return false;

            return true;
        }

        bool AnalyzeLocalDeclarationForm(
            TStatementSyntax localDeclarationStatement,
            [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce)
        {
            expressionToCoalesce = null;

            // var v = Expr();
            // if (v == null)
            //    ...

            if (!syntaxFacts.IsIdentifierName(checkedExpression))
                return false;

            var conditionIdentifier = syntaxFacts.GetIdentifierOfIdentifierName(checkedExpression).ValueText;

            var declarators = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
            if (declarators.Count != 1)
                return false;

            var declarator = (TVariableDeclarator)declarators[0];
            if (!IsSingle(declarator))
                return false;

            var equalsValue = syntaxFacts.GetInitializerOfVariableDeclarator(declarator);
            if (equalsValue is null)
                return false;

            if (syntaxFacts.GetValueOfEqualsValueClause(equalsValue) is not TExpressionSyntax initializer)
                return false;

            expressionToCoalesce = initializer;

            var variableName = syntaxFacts.GetIdentifierOfVariableDeclarator(declarator).ValueText;
            if (conditionIdentifier != variableName)
                return false;

            if (!CheckExpression(initializer))
                return false;

            if (!IsLegalWhenTrueStatementForAssignment(out var whenPartToAnalyze))
                return false;

            // Looks good.  However, make sure the when-true part doesn't access this symbol.  We can't merge
            // with the assignment then.
            var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(GetDeclarationNode(declarator), cancellationToken);
            foreach (var identifier in whenPartToAnalyze.DescendantNodesAndSelf())
            {
                if (syntaxFacts.IsIdentifierName(identifier) &&
                    syntaxFacts.GetIdentifierOfIdentifierName(identifier).ValueText == localSymbol.Name)
                {
                    var symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).GetAnySymbol();
                    if (Equals(localSymbol, symbol))
                        return false;
                }
            }

            return true;

            bool IsLegalWhenTrueStatementForAssignment([NotNullWhen(true)] out SyntaxNode? whenPartToAnalyze)
            {
                whenPartToAnalyze = whenTrueStatement;

                // var v = Expr();
                // if (v == null)
                //    throw ...
                //
                // can always convert this to `var v = Expr() ?? throw
                if (syntaxFacts.IsThrowStatement(whenTrueStatement))
                    return true;

                // var v = Expr();
                // if (v == null)
                //    v = ...
                //
                // can convert if embedded statement is assigning to same variable
                if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement))
                {
                    syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var left, out var right);
                    if (syntaxFacts.IsIdentifierName(left))
                    {
                        whenPartToAnalyze = right;
                        var leftName = syntaxFacts.GetIdentifierOfIdentifierName(left).ValueText;
                        return leftName == variableName;
                    }
                }

                return false;
            }
        }

        bool AnalyzeAssignmentForm(
            TStatementSyntax assignmentStatement,
            [NotNullWhen(true)] out TExpressionSyntax? expressionToCoalesce)
        {
            expressionToCoalesce = null;

            // expr = Expr();
            // if (expr == null)
            //    ...

            syntaxFacts.GetPartsOfAssignmentStatement(assignmentStatement, out var topAssignmentLeft, out var topAssignmentRight);
            if (!syntaxFacts.AreEquivalent(topAssignmentLeft, checkedExpression))
                return false;

            expressionToCoalesce = topAssignmentRight as TExpressionSyntax;
            if (!CheckExpression(expressionToCoalesce))
                return false;

            // expr = Expr();
            // if (expr == null)
            //    throw ...
            //
            // can always convert this to `var v = Expr() ?? throw
            if (syntaxFacts.IsThrowStatement(whenTrueStatement))
                return true;

            // expr = Expr();
            // if (expr == null)
            //    expr = ...
            //
            // can convert if embedded statement is assigning to same variable
            if (syntaxFacts.IsSimpleAssignmentStatement(whenTrueStatement))
            {
                syntaxFacts.GetPartsOfAssignmentStatement(whenTrueStatement, out var innerAssignmentLeft, out _);
                return syntaxFacts.AreEquivalent(innerAssignmentLeft, checkedExpression);
            }

            return false;
        }
    }
}