File: src\Analyzers\CSharp\Analyzers\UsePatternMatching\CSharpIsAndCastCheckDiagnosticAnalyzer.cs
Web Access
Project: src\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 System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
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.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching;
 
/// <summary>
/// Looks for code of the form:
/// 
///     if (expr is Type)
///     {
///         var v = (Type)expr;
///     }
///     
/// and converts it to:
/// 
///     if (expr is Type v)
///     {
///     }
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class CSharpIsAndCastCheckDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    public static readonly CSharpIsAndCastCheckDiagnosticAnalyzer Instance = new();
 
    public CSharpIsAndCastCheckDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.InlineIsTypeCheckId,
               EnforceOnBuildValues.InlineIsType,
               CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck,
               new LocalizableResourceString(
                   nameof(CSharpAnalyzersResources.Use_pattern_matching), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterCompilationStartAction(context =>
        {
            // "x is Type y" is only available in C# 7.0 and above.  Don't offer this refactoring
            // in projects targeting a lesser version.
            if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp7)
                return;
 
            // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to
            // get callbacks for 'is' expression nodes, but analyze nodes across the entire code block
            // and eventually report a diagnostic on the local declaration statement node.
            // Without the containing CodeBlockStartAction, our reported diagnostic would be classified
            // as a non-local diagnostic and would not participate in lightbulb for computing code fixes.
            context.RegisterCodeBlockStartAction<SyntaxKind>(blockStartContext =>
                blockStartContext.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.IsExpression));
        });
    }
 
    private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext)
    {
        var styleOption = syntaxContext.GetCSharpAnalyzerOptions().PreferPatternMatchingOverIsWithCastCheck;
        if (!styleOption.Value || ShouldSkipAnalysis(syntaxContext, styleOption.Notification))
        {
            // Bail immediately if the user has disabled this feature.
            return;
        }
 
        var isExpression = (BinaryExpressionSyntax)syntaxContext.Node;
 
        if (!TryGetPatternPieces(isExpression,
                out var ifStatement, out var localDeclarationStatement,
                out var declarator, out var castExpression))
        {
            return;
        }
 
        // Bail out if the potential diagnostic location is outside the analysis span.
        if (!syntaxContext.ShouldAnalyzeSpan(localDeclarationStatement.Span))
            return;
 
        // It's of the form:
        //
        //     if (expr is Type)
        //     {
        //         var v = (Type)expr;
        //     }
 
        // Make sure that moving 'v' to the outer scope won't cause any conflicts.
 
        var ifStatementScope = ifStatement.Parent.IsKind(SyntaxKind.Block)
            ? ifStatement.Parent
            : ifStatement;
 
        if (ContainsVariableDeclaration(ifStatementScope, declarator))
        {
            // can't switch to using a pattern here as it would cause a scoping
            // problem.
            //
            // TODO(cyrusn): Consider allowing the user to do this, but giving 
            // them an error preview.
            return;
        }
 
        var cancellationToken = syntaxContext.CancellationToken;
        var semanticModel = syntaxContext.SemanticModel;
        var localSymbol = (ILocalSymbol)semanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken);
        var isType = semanticModel.GetTypeInfo(castExpression.Type).Type;
 
        if (isType.IsNullable())
        {
            // not legal to write "if (x is int? y)"
            return;
        }
 
        if (isType?.TypeKind == TypeKind.Dynamic)
        {
            // Not legal to use dynamic in a pattern.
            return;
        }
 
        if (!localSymbol.Type.Equals(isType))
        {
            // we have something like:
            //
            //      if (x is DerivedType)
            //      {
            //          BaseType b = (DerivedType)x;
            //      }
            //
            // It's not necessarily safe to convert this to:
            //
            //      if (x is DerivedType b) { ... }
            //
            // That's because there may be later code that wants to do something like assign a 
            // 'BaseType' into 'b'.  As we've now claimed that it must be DerivedType, that 
            // won't work.  This might also cause unintended changes like changing overload
            // resolution.  So, we conservatively do not offer the change in a situation like this.
            return;
        }
 
        // Looks good!
        var additionalLocations = ImmutableArray.Create(
            ifStatement.GetLocation(),
            localDeclarationStatement.GetLocation());
 
        // Put a diagnostic with the appropriate severity on the declaration-statement itself.
        syntaxContext.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            localDeclarationStatement.GetLocation(),
            styleOption.Notification,
            syntaxContext.Options,
            additionalLocations,
            properties: null));
    }
 
    public static bool TryGetPatternPieces(
        BinaryExpressionSyntax isExpression,
        [NotNullWhen(true)] out IfStatementSyntax? ifStatement,
        [NotNullWhen(true)] out LocalDeclarationStatementSyntax? localDeclarationStatement,
        [NotNullWhen(true)] out VariableDeclaratorSyntax? declarator,
        [NotNullWhen(true)] out CastExpressionSyntax? castExpression)
    {
        localDeclarationStatement = null;
        declarator = null;
        castExpression = null;
 
        // The is check has to be in an if check: "if (x is Type)
        if (!isExpression.Parent.IsKind(SyntaxKind.IfStatement, out ifStatement))
            return false;
 
        if (ifStatement.Statement is not BlockSyntax block)
            return false;
 
        if (block.Statements is [TryStatementSyntax tryStatement, ..])
            block = tryStatement.Block;
 
        if (block.Statements.Count == 0)
            return false;
 
        var firstStatement = block.Statements[0];
        if (!firstStatement.IsKind(SyntaxKind.LocalDeclarationStatement, out localDeclarationStatement))
            return false;
 
        if (localDeclarationStatement.Declaration.Variables.Count != 1)
            return false;
 
        declarator = localDeclarationStatement.Declaration.Variables[0];
        if (declarator.Initializer == null)
            return false;
 
        var declaratorValue = declarator.Initializer.Value.WalkDownParentheses();
        if (!declaratorValue.IsKind(SyntaxKind.CastExpression, out castExpression))
            return false;
 
        if (!SyntaxFactory.AreEquivalent(isExpression.Left.WalkDownParentheses(), castExpression.Expression.WalkDownParentheses(), topLevel: false) ||
            !SyntaxFactory.AreEquivalent(isExpression.Right.WalkDownParentheses(), castExpression.Type, topLevel: false))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool ContainsVariableDeclaration(
        SyntaxNode scope, VariableDeclaratorSyntax variable)
    {
        var variableName = variable.Identifier.ValueText;
 
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var stack);
        stack.Push(scope);
 
        while (stack.TryPop(out var current))
        {
            if (current == variable)
                continue;
 
            if (current is VariableDeclaratorSyntax declarator &&
                declarator.Identifier.ValueText.Equals(variableName))
            {
                return true;
            }
            else if (current is SingleVariableDesignationSyntax designation &&
                designation.Identifier.ValueText.Equals(variableName))
            {
                return true;
            }
 
            foreach (var child in current.ChildNodesAndTokens())
            {
                if (child.AsNode(out var childNode))
                    stack.Push(childNode);
            }
        }
 
        return false;
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
}