File: src\roslyn\src\Analyzers\CSharp\Analyzers\HiddenExplicitCast\CSharpHiddenExplicitCastDiagnosticAnalyzer.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 System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeAnalysis.CSharp.Analyzers.HiddenExplicitCast;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpHiddenExplicitCastDiagnosticAnalyzer()
    : AbstractBuiltInCodeStyleDiagnosticAnalyzer(
          diagnosticId: IDEDiagnosticIds.HiddenExplicitCastDiagnosticId,
          EnforceOnBuildValues.HiddenExplicitCast,
          CodeStyleOptions2.PreferNonHiddenExplicitCastInSource,
          title: new LocalizableResourceString(nameof(AnalyzersResources.Add_explicit_cast), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
          messageFormat: new LocalizableResourceString(nameof(AnalyzersResources._0_implicitly_converts_1_to_2_Add_an_explicit_cast_to_make_intent_clearer_as_it_may_fail_at_runtime), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
{
    public const string Type = nameof(Type);

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

    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.CastExpression);

    private static IConversionOperation? GetInitialOperation(
        SemanticModel semanticModel,
        CastExpressionSyntax castExpression,
        CancellationToken cancellationToken)
    {
        // Due to issues getting IConversionOperations from the semantic model directly from a CastExpressionSyntax (see
        // https://github.com/dotnet/roslyn/issues/81781).  To work around this, we instead get the IOp for the casted
        // inner expression instead, and then walk up the IOperation tree to find the highest explicit conversion
        // corresponding to this cast expression node.

        // IOp tree does not return operations for parentheses or suppressions, so walk down those first.
        var currentExpression = castExpression.Expression;
        while (true)
        {
            var inner = currentExpression.WalkDownParentheses().WalkDownSuppressions();
            if (inner == currentExpression)
                break;

            currentExpression = inner;
        }

        var innerOperation = semanticModel.GetOperation(currentExpression, cancellationToken);
        if (innerOperation is null)
            return null;

        // Keep walking upwards through conversions.  We want the highest one that is explicit and corresponds to this
        // cast expression.
        IConversionOperation? highestExplicitConversion = null;
        for (var current = innerOperation.Parent;
             current is IConversionOperation conversionOperation;
             current = current.Parent)
        {
            if (conversionOperation.GetConversion().IsExplicit && conversionOperation.Syntax == castExpression)
                highestExplicitConversion = conversionOperation;
        }

        return highestExplicitConversion;
    }

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

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

        var outerConversionOperation = GetInitialOperation(semanticModel, castExpression, cancellationToken);
        if (outerConversionOperation is null)
            return;

        // This cast needs to be an explicit conversion.  We're looking for the case where this explicit conversion in
        // source causes an inner explicit conversion to be emitted in a hidden fashion by the compiler.
        var outerConversion = outerConversionOperation.GetConversion();
        if (!outerConversion.IsExplicit)
            return;

        // Has to be multiple conversions.
        if (outerConversionOperation.Operand is not IConversionOperation innerConversionOperation)
            return;

        // The inner one has to be generated by the compiler, not the user.
        if (!innerConversionOperation.IsImplicit)
            return;

        // The inner one has to be explicit as well.  This is the hidden explicit cast we're looking for.
        var innerConversion = innerConversionOperation.GetConversion();
        if (!innerConversion.IsExplicit)
            return;

        if (outerConversionOperation.Type is null || innerConversionOperation.Type is null || innerConversionOperation.Operand.Type is null)
            return;

        var typeToInsertCastFor = innerConversionOperation.Type.ToMinimalDisplayString(semanticModel, castExpression.SpanStart);
        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            castExpression.GetLocation(),
            option.Notification,
            context.Options,
            additionalLocations: null,
            properties: ImmutableDictionary<string, string?>.Empty.Add(Type, typeToInsertCastFor),
            $"({outerConversionOperation.Type.ToMinimalDisplayString(semanticModel, castExpression.SpanStart)})",
            innerConversionOperation.Operand.Type.ToMinimalDisplayString(semanticModel, castExpression.SpanStart),
            typeToInsertCastFor));
    }
}