File: src\roslyn\src\Analyzers\Core\Analyzers\ForEachCast\AbstractForEachCastDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.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;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ForEachCast;

internal static class ForEachCastHelpers
{
    public const string IsFixable = nameof(IsFixable);
}

internal abstract class AbstractForEachCastDiagnosticAnalyzer<TSyntaxKind, TForEachStatementSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    where TSyntaxKind : struct, Enum
    where TForEachStatementSyntax : SyntaxNode
{
    public static readonly ImmutableDictionary<string, string?> s_isFixableProperties =
        ImmutableDictionary<string, string?>.Empty.Add(ForEachCastHelpers.IsFixable, ForEachCastHelpers.IsFixable);

    protected AbstractForEachCastDiagnosticAnalyzer()
        : base(
              diagnosticId: IDEDiagnosticIds.ForEachCastDiagnosticId,
              EnforceOnBuildValues.ForEachCast,
              CodeStyleOptions2.ForEachExplicitCastInSource,
              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)))
    {
    }

    protected abstract ISyntaxFacts SyntaxFacts { get; }
    protected abstract ImmutableArray<TSyntaxKind> GetSyntaxKinds();
    protected abstract (CommonConversion conversion, ITypeSymbol? collectionElementType) GetForEachInfo(SemanticModel semanticModel, TForEachStatementSyntax node);

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

    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(context => AnalyzeSyntax(context), GetSyntaxKinds());
    }

    private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
    {
        var semanticModel = context.SemanticModel;
        var cancellationToken = context.CancellationToken;
        if (context.Node is not TForEachStatementSyntax node)
            return;

        var option = context.GetAnalyzerOptions().ForEachExplicitCastInSource;
        Contract.ThrowIfFalse(option.Value is ForEachExplicitCastInSourcePreference.Always or ForEachExplicitCastInSourcePreference.WhenStronglyTyped);

        if (ShouldSkipAnalysis(context, option.Notification))
            return;

        if (semanticModel.GetOperation(node, cancellationToken) is not IForEachLoopOperation loopOperation)
            return;

        if (loopOperation.LoopControlVariable is not IVariableDeclaratorOperation variableDeclarator ||
            variableDeclarator.Symbol.Type is not ITypeSymbol iterationType)
        {
            return;
        }

        var syntaxFacts = this.SyntaxFacts;
        var collectionType = semanticModel.GetTypeInfo(syntaxFacts.GetExpressionOfForeachStatement(node), cancellationToken).Type;
        if (collectionType is null)
            return;

        var (conversion, collectionElementType) = GetForEachInfo(semanticModel, node);
        if (collectionElementType is null)
            return;

        // Don't bother checking conversions that are problematic for other reasons.  The user will already have a
        // compiler error telling them the foreach is in error.
        if (!conversion.Exists)
            return;

        // Consider:
        // public class C : IEnumerable<Match>
        // {
        //     public IEnumerator GetEnumerator() => null; // compiler picks this for the foreach loop.
        //
        //     IEnumerator<Match> IEnumerable<Match>.GetEnumerator() => null; // compiler doesn't use this.
        // }

        // This collection have GetEnumerator method that returns non-strongly-typed IEnumerator, but also implements strongly-typed IEnumerable<T> explicitly.
        // In this case, the compiler chooses the non-strongly-typed GetEnumerator and adds explicit cast for `foreach (Match m in new C())`.
        // This cast can fail if the collection is badly implemented such that the strongly-typed and non-strongly-typed GetEnumerator implemetations return different types.
        // Given it's very rare that implementation can be bad, and to reduce false positives, we adjust collectionElementType for the case above to be `Match` instead of object.
        if (collectionElementType.SpecialType == SpecialType.System_Object)
        {
            var ienumerableOfT = collectionType.AllInterfaces.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
            if (ienumerableOfT is not null)
            {
                collectionElementType = ienumerableOfT.TypeArguments[0];
                conversion = context.Compilation.ClassifyCommonConversion(collectionElementType, iterationType);
            }
        }

        // If the conversion was implicit, then everything is ok.  Implicit conversions are safe and do not throw at runtime.
        if (conversion.IsImplicit)
            return;

        // An implicit legal conversion still shows up as explicit conversion in the object model.  But this is fine
        // to keep as is since being an implicit-conversion means the API indicates it should always be safe to
        // happen at runtime.
        if (conversion.IsUserDefined && conversion.MethodSymbol is { Name: WellKnownMemberNames.ImplicitConversionName })
            return;

        // We had a conversion that was explicit.  These are potentially unsafe as they can throw at runtime.
        // Generally, we would like to notify the user about these.  However, we have different policies depending
        // on if we think this is a legacy API or not.  Legacy APIs are those built before generics, and thus often
        // have APIs that will just return `objects` and thus always need some sort of cast to get them to the right
        // type.  A good example of that is S.T.RegularExpressions.CaptureCollection.  Users will almost always
        // write this was `foreach (Capture capture in match.Captures)` and it would be annoying to force them to
        // change this.
        //
        // What we do want to warn on are things like: `foreach (IUnrelatedInterface iface in stronglyTypedCollection)`.
        //
        // So, to detect if we're in a legacy situation, we look for iterations that are returning an object-type
        // where the collection itself didn't implement `IEnumerable<T>` in some way.
        if (option.Value == ForEachExplicitCastInSourcePreference.WhenStronglyTyped &&
            !IsStronglyTyped(collectionType, collectionElementType))
        {
            return;
        }

        // The user either always wants to write these casts explicitly, or they were calling a non-legacy API.
        // report the issue so they can insert the appropriate cast.

        // We can only fix this issue if the collection type implemented ienumerable and we have
        // System.Linq.Enumerable available.  Then we can add a .Cast call to their collection explicitly.
        var isFixable = collectionType.SpecialType == SpecialType.System_Collections_IEnumerable || collectionType.AllInterfaces.Any(static i => i.SpecialType == SpecialType.System_Collections_IEnumerable) &&
            semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!) != null;

        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            node.GetFirstToken().GetLocation(),
            option.Notification,
            context.Options,
            additionalLocations: null,
            properties: isFixable ? s_isFixableProperties : null,
            node.GetFirstToken().ToString(),
            collectionElementType.ToDisplayString(),
            iterationType.ToDisplayString()));
    }

    private static bool IsStronglyTyped(ITypeSymbol collectionType, ITypeSymbol collectionElementType)
        => collectionElementType.SpecialType != SpecialType.System_Object ||
           collectionType.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T ||
           collectionType.AllInterfaces.Any(static i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
}