|
// 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 Microsoft.CodeAnalysis.Shared.Extensions;
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_statement_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.RegisterCompilationStartAction(context =>
{
var compilation = context.Compilation;
var ienumerableType = compilation.IEnumerableType();
var ienumerableOfTType = compilation.IEnumerableOfTType();
if (ienumerableType != null && ienumerableOfTType != null)
context.RegisterSyntaxNodeAction(context => AnalyzeSyntax(context, ienumerableType, ienumerableOfTType), GetSyntaxKinds());
});
}
protected void AnalyzeSyntax(
SyntaxNodeAnalysisContext context, INamedTypeSymbol ienumerableType, INamedTypeSymbol ienumerableOfTType)
{
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 (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);
// 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;
// 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;
if (collectionElementType is null)
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(ienumerableOfTType, 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.Equals(ienumerableType) || collectionType.AllInterfaces.Any(static (i, ienumerableType) => i.Equals(ienumerableType), ienumerableType) &&
semanticModel.Compilation.GetBestTypeByMetadataName(typeof(Enumerable).FullName!) != null;
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
node.GetFirstToken().GetLocation(),
option.Notification.Severity,
additionalLocations: null,
properties: isFixable ? s_isFixableProperties : null,
node.GetFirstToken().ToString(),
collectionElementType.ToDisplayString(),
iterationType.ToDisplayString()));
}
private static bool IsStronglyTyped(INamedTypeSymbol ienumerableOfTType, ITypeSymbol collectionType, ITypeSymbol collectionElementType)
=> collectionElementType.SpecialType != SpecialType.System_Object ||
collectionType.OriginalDefinition.Equals(ienumerableOfTType) ||
collectionType.AllInterfaces.Any(static (i, ienumerableOfTType) => i.OriginalDefinition.Equals(ienumerableOfTType), ienumerableOfTType);
}
}
|