File: src\roslyn\src\Analyzers\CSharp\Analyzers\UsePatternMatching\CSharpAsAndMemberAccessDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.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 Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching;

/// <summary>
/// Looks for code of the forms:
/// <code>
///     (x as Y)?.Prop == constant
/// </code>
/// and converts it to:
/// <code>
///     x is Y { Prop: constant }
/// </code>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed partial class CSharpAsAndMemberAccessDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    public CSharpAsAndMemberAccessDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.UsePatternMatchingAsAndMemberAccessDiagnosticId,
               EnforceOnBuildValues.UsePatternMatchingAsAndMemberAccess,
               CSharpCodeStyleOptions.PreferPatternMatchingOverAsWithNullCheck,
               new LocalizableResourceString(
                    nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }

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

    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterCompilationStartAction(context =>
        {
            // Recursive patterns (`is X { Prop: Y }`) is only available in C# 8.0 and above. Don't offer this
            // refactoring in projects targeting a lesser version.
            if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8)
                return;

            context.RegisterSyntaxNodeAction(context => AnalyzeAsExpression(context), SyntaxKind.AsExpression);
        });
    }

    private void AnalyzeAsExpression(SyntaxNodeAnalysisContext context)
    {
        var styleOption = context.GetCSharpAnalyzerOptions().PreferPatternMatchingOverAsWithNullCheck;
        if (!styleOption.Value || ShouldSkipAnalysis(context, styleOption.Notification))
        {
            // Bail immediately if the user has disabled this feature.
            return;
        }

        var cancellationToken = context.CancellationToken;
        var semanticModel = context.SemanticModel;
        var asExpression = (BinaryExpressionSyntax)context.Node;

        if (!UsePatternMatchingHelpers.TryGetPartsOfAsAndMemberAccessCheck(
                asExpression, out var conditionalAccessExpression, out var binaryExpression, out var isPatternExpression, out var requiredLanguageVersion))
        {
            return;
        }

        if (context.Compilation.LanguageVersion() < requiredLanguageVersion)
            return;

        if (!IsSafeToConvert())
            return;

        // Looks good!

        // Put a diagnostic with the appropriate severity on the declaration-statement itself.
        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            asExpression.GetLocation(),
            styleOption.Notification,
            context.Options,
            additionalLocations: null,
            properties: null));

        bool IsSafeToConvert()
        {
            if (binaryExpression != null)
            {
                // `(expr as T)?... == other_expr
                //
                // in this case we can only convert if other_expr is a constant.
                var constantValue = semanticModel.GetConstantValue(binaryExpression.Right, cancellationToken);
                if (!constantValue.HasValue)
                    return false;

                if (binaryExpression.Kind() is SyntaxKind.EqualsExpression)
                {
                    // `(a as T)?.Prop == null` does *not* have the same semantics as `a is T { Prop: null }`
                    // (specifically, when the type check fails)
                    if (constantValue.Value is null)
                        return false;

                    // `(a as T)?.Prop == constant` does* have the same semantics as `a is T { Prop: constant }`
                    return true;
                }
                else if (binaryExpression.Kind() is SyntaxKind.NotEqualsExpression)
                {
                    // `(a as T)?.Prop != constant` *does not* have the same semantics as `a is T { Prop: not constant }`
                    // (specifically, when the type check fails)
                    if (constantValue.Value is not null)
                        return false;

                    // `(a as T)?.Prop != null` *does* have the same semantics as `a is T { Prop: not null }`.
                    //
                    // However, that's still only allowed if `Prop` is not a value type.
                    var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol();
                    if (symbol.GetMemberType().IsNonNullableValueType())
                        return false;

                    return true;
                }

                // don't need to check the other relational comparisons. These comparisons do a null check themselves,
                // so it's safe to add a null-check with the 'is'.
                return true;
            }
            else
            {
                Contract.ThrowIfNull(isPatternExpression);

                // similar to the binary cases above.

                if (isPatternExpression.Pattern is ConstantPatternSyntax { Expression: var expression1 })
                {
                    var constantValue = semanticModel.GetConstantValue(expression1, cancellationToken);
                    if (!constantValue.HasValue)
                        return false;

                    // `(a as T)?.Prop is null` does *not* have the same semantics as `a is T { Prop: null }`
                    // (specifically, when the type check fails)
                    if (constantValue.Value is null)
                        return false;

                    // `(a as T)?.Prop is constant` does* have the same semantics as `a is T { Prop: constant }`
                    return true;
                }
                else if (isPatternExpression.Pattern is UnaryPatternSyntax { Pattern: ConstantPatternSyntax { Expression: var expression2 } })
                {
                    var constantValue = semanticModel.GetConstantValue(expression2, cancellationToken);
                    if (!constantValue.HasValue)
                        return false;

                    // `(a as T)?.Prop is not constant` *does not* have the same semantics as `a is T { Prop: not constant }`
                    // (specifically, when the type check fails)
                    if (constantValue.Value is not null)
                        return false;

                    // `(a as T)?.Prop is not null` *does* have the same semantics as `a is T { Prop: not null }`.
                    //
                    // However, that's still only allowed if `Prop` is not a value type.
                    var symbol = semanticModel.GetSymbolInfo(conditionalAccessExpression.WhenNotNull, cancellationToken).GetAnySymbol();
                    if (symbol.GetMemberType().IsNonNullableValueType())
                        return false;

                    return true;
                }

                return true;
            }
        }
    }
}