File: CSharpAsAndMemberAccessDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\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;
using Roslyn.Utilities;
 
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 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)
            {
                // 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.Severity,
                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;
                }
            }
        }
    }
}