File: src\Analyzers\Core\Analyzers\UseCollectionInitializer\AbstractUseCollectionInitializerDiagnosticAnalyzer.cs
Web Access
Project: src\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.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.CodeStyle;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.UseCollectionExpression;
 
namespace Microsoft.CodeAnalysis.UseCollectionInitializer;
 
internal abstract partial class AbstractUseCollectionInitializerDiagnosticAnalyzer<
    TSyntaxKind,
    TExpressionSyntax,
    TStatementSyntax,
    TObjectCreationExpressionSyntax,
    TMemberAccessExpressionSyntax,
    TInvocationExpressionSyntax,
    TExpressionStatementSyntax,
    TLocalDeclarationStatementSyntax,
    TVariableDeclaratorSyntax,
    TAnalyzer>
    : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    where TSyntaxKind : struct
    where TExpressionSyntax : SyntaxNode
    where TStatementSyntax : SyntaxNode
    where TObjectCreationExpressionSyntax : TExpressionSyntax
    where TMemberAccessExpressionSyntax : TExpressionSyntax
    where TInvocationExpressionSyntax : TExpressionSyntax
    where TExpressionStatementSyntax : TStatementSyntax
    where TLocalDeclarationStatementSyntax : TStatementSyntax
    where TVariableDeclaratorSyntax : SyntaxNode
    where TAnalyzer : AbstractUseCollectionInitializerAnalyzer<
        TExpressionSyntax,
        TStatementSyntax,
        TObjectCreationExpressionSyntax,
        TMemberAccessExpressionSyntax,
        TInvocationExpressionSyntax,
        TExpressionStatementSyntax,
        TLocalDeclarationStatementSyntax,
        TVariableDeclaratorSyntax,
        TAnalyzer>, new()
{
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptorWithId(
        IDEDiagnosticIds.UseCollectionInitializerDiagnosticId,
        EnforceOnBuildValues.UseCollectionInitializer,
        hasAnyCodeStyleOption: true,
        new LocalizableResourceString(nameof(AnalyzersResources.Simplify_collection_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
        new LocalizableResourceString(nameof(AnalyzersResources.Collection_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
        isUnnecessary: false);
 
    private static readonly DiagnosticDescriptor s_unnecessaryCodeDescriptor = CreateDescriptorWithId(
        IDEDiagnosticIds.UseCollectionInitializerDiagnosticId,
        EnforceOnBuildValues.UseCollectionInitializer,
        hasAnyCodeStyleOption: true,
        new LocalizableResourceString(nameof(AnalyzersResources.Simplify_collection_initialization), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
        new LocalizableResourceString(nameof(AnalyzersResources.Collection_initialization_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
        isUnnecessary: true);
 
    protected AbstractUseCollectionInitializerDiagnosticAnalyzer()
        : base([(s_descriptor, CodeStyleOptions2.PreferCollectionInitializer)])
    {
    }
 
    protected abstract ISyntaxFacts SyntaxFacts { get; }
 
    protected abstract bool AreCollectionInitializersSupported(Compilation compilation);
    protected abstract bool AreCollectionExpressionsSupported(Compilation compilation);
    protected abstract bool CanUseCollectionExpression(
        SemanticModel semanticModel,
        TObjectCreationExpressionSyntax objectCreationExpression,
        INamedTypeSymbol? expressionType,
        ImmutableArray<CollectionMatch<SyntaxNode>> preMatches,
        bool allowSemanticsChange,
        CancellationToken cancellationToken,
        out bool changesSemantics);
 
    protected abstract TAnalyzer GetAnalyzer();
 
    protected abstract bool IsValidContainingStatement(TStatementSyntax node);
 
    protected sealed override void InitializeWorker(AnalysisContext context)
        => context.RegisterCompilationStartAction(OnCompilationStart);
 
    private void OnCompilationStart(CompilationStartAnalysisContext context)
    {
        if (!AreCollectionInitializersSupported(context.Compilation))
            return;
 
        var ienumerableType = context.Compilation.IEnumerableType();
        if (ienumerableType is null)
            return;
 
        var syntaxKinds = this.SyntaxFacts.SyntaxKinds;
 
        using var matchKinds = TemporaryArray<TSyntaxKind>.Empty;
        matchKinds.Add(syntaxKinds.Convert<TSyntaxKind>(syntaxKinds.ObjectCreationExpression));
        if (syntaxKinds.ImplicitObjectCreationExpression != null)
            matchKinds.Add(syntaxKinds.Convert<TSyntaxKind>(syntaxKinds.ImplicitObjectCreationExpression.Value));
        var matchKindsArray = matchKinds.ToImmutableAndClear();
 
        // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to
        // get callbacks for object creation expression nodes, but analyze nodes across the entire code block
        // and eventually report fading diagnostics with location outside this 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.
        var expressionType = context.Compilation.ExpressionOfTType();
        context.RegisterCodeBlockStartAction<TSyntaxKind>(blockStartContext =>
            blockStartContext.RegisterSyntaxNodeAction(
                nodeContext => AnalyzeNode(nodeContext, ienumerableType, expressionType),
                matchKindsArray));
    }
 
    private void AnalyzeNode(
        SyntaxNodeAnalysisContext context,
        INamedTypeSymbol ienumerableType,
        INamedTypeSymbol? expressionType)
    {
        var semanticModel = context.SemanticModel;
        var objectCreationExpression = (TObjectCreationExpressionSyntax)context.Node;
        var language = objectCreationExpression.Language;
        var cancellationToken = context.CancellationToken;
 
        var preferInitializerOption = context.GetAnalyzerOptions().PreferCollectionInitializer;
        var preferExpressionOption = context.GetAnalyzerOptions().PreferCollectionExpression;
 
        // not point in analyzing if both options are off.
        if (!preferInitializerOption.Value
            && preferExpressionOption.Value == Shared.CodeStyle.CollectionExpressionPreference.Never
            && !ShouldSkipAnalysis(context.FilterTree, context.Options, context.Compilation.Options,
                    [preferInitializerOption.Notification, preferExpressionOption.Notification],
                    context.CancellationToken))
        {
            return;
        }
 
        // Object creation can only be converted to collection initializer if it implements the IEnumerable type.
        var objectType = context.SemanticModel.GetTypeInfo(objectCreationExpression, cancellationToken);
        if (objectType.Type == null || !objectType.Type.AllInterfaces.Contains(ienumerableType))
            return;
 
        // Analyze the surrounding statements. First, try a broader set of statements if the language supports
        // collection expressions. 
        var syntaxFacts = this.SyntaxFacts;
        using var analyzer = GetAnalyzer();
 
        var containingStatement = objectCreationExpression.FirstAncestorOrSelf<TStatementSyntax>();
        if (containingStatement != null && !IsValidContainingStatement(containingStatement))
            return;
 
        var collectionExpressionMatches = GetCollectionExpressionMatches();
        var collectionInitializerMatches = GetCollectionInitializerMatches();
 
        // if both fail, we have nothing to offer.
        if (collectionExpressionMatches is null && collectionInitializerMatches is null)
            return;
 
        // if one fails, prefer the other.  If both succeed, prefer the one with more matches.
        var (matches, shouldUseCollectionExpression, changesSemantics) =
            collectionExpressionMatches is null ? collectionInitializerMatches!.Value :
            collectionInitializerMatches is null ? collectionExpressionMatches!.Value :
            collectionExpressionMatches.Value.matches.Length >= collectionInitializerMatches.Value.matches.Length
                ? collectionExpressionMatches.Value
                : collectionInitializerMatches.Value;
 
        var nodes = containingStatement is null
            ? ImmutableArray<SyntaxNode>.Empty
            : [containingStatement];
        nodes = nodes.AddRange(matches.Select(static m => m.Node));
        if (syntaxFacts.ContainsInterleavedDirective(nodes, cancellationToken))
            return;
 
        var locations = ImmutableArray.Create(objectCreationExpression.GetLocation());
 
        var notification = shouldUseCollectionExpression ? preferExpressionOption.Notification : preferInitializerOption.Notification;
        var properties = shouldUseCollectionExpression ? UseCollectionInitializerHelpers.UseCollectionExpressionProperties : ImmutableDictionary<string, string?>.Empty;
        if (changesSemantics)
            properties = properties.Add(UseCollectionInitializerHelpers.ChangesSemanticsName, "");
 
        context.ReportDiagnostic(DiagnosticHelper.Create(
            s_descriptor,
            objectCreationExpression.GetFirstToken().GetLocation(),
            notification,
            context.Options,
            additionalLocations: locations,
            properties));
 
        FadeOutCode(context, matches, locations, properties);
 
        return;
 
        (ImmutableArray<CollectionMatch<SyntaxNode>> matches, bool shouldUseCollectionExpression, bool changesSemantics)? GetCollectionInitializerMatches()
        {
            if (containingStatement is null)
                return null;
 
            if (!preferInitializerOption.Value)
                return null;
 
            var (_, matches, changesSemantics) = analyzer.Analyze(semanticModel, syntaxFacts, objectCreationExpression, analyzeForCollectionExpression: false, cancellationToken);
 
            // If analysis failed, we can't change this, no matter what.
            if (matches.IsDefault)
                return null;
 
            return (matches, shouldUseCollectionExpression: false, changesSemantics);
        }
 
        (ImmutableArray<CollectionMatch<SyntaxNode>> matches, bool shouldUseCollectionExpression, bool changesSemantics)? GetCollectionExpressionMatches()
        {
            if (preferExpressionOption.Value == CollectionExpressionPreference.Never)
                return null;
 
            // Don't bother analyzing for the collection expression case if the lang/version doesn't even support it.
            if (!this.AreCollectionExpressionsSupported(context.Compilation))
                return null;
 
            var (preMatches, postMatches, changesSemantics1) = analyzer.Analyze(semanticModel, syntaxFacts, objectCreationExpression, analyzeForCollectionExpression: true, cancellationToken);
 
            // If analysis failed, we can't change this, no matter what.
            if (preMatches.IsDefault || postMatches.IsDefault)
                return null;
 
            // Check if it would actually be legal to use a collection expression here though.
            var allowSemanticsChange = preferExpressionOption.Value == CollectionExpressionPreference.WhenTypesLooselyMatch;
            if (!CanUseCollectionExpression(semanticModel, objectCreationExpression, expressionType, preMatches, allowSemanticsChange, cancellationToken, out var changesSemantics2))
                return null;
 
            return ([.. preMatches, .. postMatches], shouldUseCollectionExpression: true, changesSemantics1 || changesSemantics2);
        }
    }
 
    private void FadeOutCode(
        SyntaxNodeAnalysisContext context,
        ImmutableArray<CollectionMatch<SyntaxNode>> matches,
        ImmutableArray<Location> locations,
        ImmutableDictionary<string, string?>? properties)
    {
        var syntaxFacts = this.SyntaxFacts;
 
        foreach (var match in matches)
        {
            var additionalUnnecessaryLocations = UseCollectionInitializerHelpers.GetLocationsToFade(
                syntaxFacts, match);
            if (additionalUnnecessaryLocations.IsDefaultOrEmpty)
                continue;
 
            // Report the diagnostic at the first unnecessary location. This is the location where the code fix
            // will be offered.
            context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
                s_unnecessaryCodeDescriptor,
                additionalUnnecessaryLocations[0],
                NotificationOption2.ForSeverity(s_unnecessaryCodeDescriptor.DefaultSeverity),
                context.Options,
                additionalLocations: locations,
                additionalUnnecessaryLocations: additionalUnnecessaryLocations,
                properties));
        }
    }
}