File: CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer.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.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseIsNullCheck
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    internal sealed class CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    {
        public CSharpUseNullCheckOverTypeCheckDiagnosticAnalyzer()
            : base(IDEDiagnosticIds.UseNullCheckOverTypeCheckDiagnosticId,
                   EnforceOnBuildValues.UseNullCheckOverTypeCheck,
                   CSharpCodeStyleOptions.PreferNullCheckOverTypeCheck,
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Prefer_null_check_over_type_check), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Null_check_can_be_clarified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
        {
        }
 
        public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
            => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
        protected override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(context =>
            {
                var compilation = context.Compilation;
                if (compilation.LanguageVersion() < LanguageVersion.CSharp9)
                    return;
 
                var expressionType = compilation.ExpressionOfTType();
                context.RegisterOperationAction(c => AnalyzeIsTypeOperation(c, expressionType), OperationKind.IsType);
                context.RegisterOperationAction(c => AnalyzeNegatedPatternOperation(c), OperationKind.NegatedPattern);
            });
        }
 
        private static bool ShouldAnalyze(OperationAnalysisContext context, out ReportDiagnostic severity)
        {
            var option = context.GetCSharpAnalyzerOptions().PreferNullCheckOverTypeCheck;
            if (!option.Value)
            {
                severity = ReportDiagnostic.Default;
                return false;
            }
 
            severity = option.Notification.Severity;
            return true;
        }
 
        private void AnalyzeNegatedPatternOperation(OperationAnalysisContext context)
        {
            if (!ShouldAnalyze(context, out var severity) ||
                context.Operation.Syntax is not UnaryPatternSyntax)
            {
                return;
            }
 
            var negatedPattern = (INegatedPatternOperation)context.Operation;
            // Matches 'x is not MyType'
            // InputType is the type of 'x'
            // MatchedType is 'MyType'
            // We check InheritsFromOrEquals so that we report a diagnostic on the following:
            // 1. x is not object (which is also equivalent to 'is null' check)
            // 2. derivedObj is parentObj (which is the same as the previous point).
            // 3. str is string (where str is a string, this is also equivalent to 'is null' check).
            // This doesn't match `x is not MyType y` because in such case, negatedPattern.Pattern will
            // be `DeclarationPattern`, not `TypePattern`.
            if (negatedPattern.Pattern is ITypePatternOperation typePatternOperation &&
                typePatternOperation.InputType.InheritsFromOrEquals(typePatternOperation.MatchedType))
            {
                context.ReportDiagnostic(
                    DiagnosticHelper.Create(
                        Descriptor, context.Operation.Syntax.GetLocation(), severity, additionalLocations: null, properties: null));
            }
        }
 
        private void AnalyzeIsTypeOperation(OperationAnalysisContext context, INamedTypeSymbol? expressionType)
        {
            var operation = context.Operation;
            var semanticModel = operation.SemanticModel;
            var syntax = operation.Syntax;
 
            Contract.ThrowIfNull(semanticModel);
 
            if (!ShouldAnalyze(context, out var severity) || syntax is not BinaryExpressionSyntax)
                return;
 
            if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, syntax, expressionType, context.CancellationToken))
                return;
 
            var isTypeOperation = (IIsTypeOperation)operation;
 
            // Matches 'x is MyType'
            // isTypeOperation.TypeOperand is 'MyType'
            // isTypeOperation.ValueOperand.Type is the type of 'x'.
            // We check InheritsFromOrEquals for the same reason as stated in AnalyzeNegatedPatternOperation.
            // This doesn't match `x is MyType y` because in such case, we have an IsPattern instead of IsType operation.
            if (isTypeOperation.ValueOperand.Type is not null &&
                isTypeOperation.ValueOperand.Type.InheritsFromOrEquals(isTypeOperation.TypeOperand))
            {
                context.ReportDiagnostic(
                    DiagnosticHelper.Create(
                        Descriptor, syntax.GetLocation(), severity, additionalLocations: null, properties: null));
            }
        }
    }
}