File: src\Analyzers\Core\Analyzers\UseCollectionInitializer\AbstractUseCollectionInitializerAnalyzer.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.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.UseCollectionExpression;
 
namespace Microsoft.CodeAnalysis.UseCollectionInitializer;
 
internal abstract class AbstractUseCollectionInitializerAnalyzer<
    TExpressionSyntax,
    TStatementSyntax,
    TObjectCreationExpressionSyntax,
    TMemberAccessExpressionSyntax,
    TInvocationExpressionSyntax,
    TExpressionStatementSyntax,
    TLocalDeclarationStatementSyntax,
    TVariableDeclaratorSyntax,
    TAnalyzer> : AbstractObjectCreationExpressionAnalyzer<
        TExpressionSyntax,
        TStatementSyntax,
        TObjectCreationExpressionSyntax,
        TLocalDeclarationStatementSyntax,
        TVariableDeclaratorSyntax,
        CollectionMatch<SyntaxNode>, TAnalyzer>
    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()
{
    protected bool _analyzeForCollectionExpression;
 
    protected abstract bool IsComplexElementInitializer(SyntaxNode expression, out int initializerElementCount);
 
    protected abstract bool HasExistingInvalidInitializerForCollection();
    protected abstract bool AnalyzeMatchesAndCollectionConstructorForCollectionExpression(
        ArrayBuilder<CollectionMatch<SyntaxNode>> preMatches,
        ArrayBuilder<CollectionMatch<SyntaxNode>> postMatches,
        out bool mayChangeSemantics,
        CancellationToken cancellationToken);
 
    protected abstract IUpdateExpressionSyntaxHelper<TExpressionSyntax, TStatementSyntax> SyntaxHelper { get; }
 
    protected override void Clear()
    {
        base.Clear();
        _analyzeForCollectionExpression = false;
    }
 
    public AnalysisResult Analyze(
        SemanticModel semanticModel,
        ISyntaxFacts syntaxFacts,
        TObjectCreationExpressionSyntax objectCreationExpression,
        bool analyzeForCollectionExpression,
        CancellationToken cancellationToken)
    {
        _analyzeForCollectionExpression = analyzeForCollectionExpression;
        var state = TryInitializeState(semanticModel, syntaxFacts, objectCreationExpression, cancellationToken);
 
        // If we didn't find something we're assigned to, then we normally can't continue.  However, we always support
        // converting a `new List<int>()` collection over to a collection expression.  We just won't analyze later
        // statements.  
        if (state.ValuePattern == default && !analyzeForCollectionExpression)
            return default;
 
        this.Initialize(state, objectCreationExpression);
        var (preMatches, postMatches, mayChangeSemantics) = this.AnalyzeWorker(cancellationToken);
 
        // If analysis failed entirely, immediately bail out.
        if (preMatches.IsDefault || postMatches.IsDefault)
            return default;
 
        // Analysis succeeded, but the result may be empty or non empty.
        //
        // For collection expressions, it's fine for this result to be empty.  In other words, it's ok to offer
        // changing `new List<int>() { 1 }` (on its own) to `[1]`.
        //
        // However, for collection initializers we always want at least one element to add to the initializer.  In
        // other words, we don't want to suggest changing `new List<int>()` to `new List<int>() { }` as that's just
        // noise.  So convert empty results to an invalid result here.
        if (analyzeForCollectionExpression)
            return new(preMatches, postMatches, mayChangeSemantics);
 
        // Downgrade an empty result to a failure for the normal collection-initializer case.
        return postMatches.IsEmpty ? default : new(preMatches, postMatches, mayChangeSemantics);
    }
 
    protected sealed override bool TryAddMatches(
        ArrayBuilder<CollectionMatch<SyntaxNode>> preMatches,
        ArrayBuilder<CollectionMatch<SyntaxNode>> postMatches,
        out bool mayChangeSemantics,
        CancellationToken cancellationToken)
    {
        mayChangeSemantics = false;
        var seenInvocation = false;
        var seenIndexAssignment = false;
 
        var initializer = this.SyntaxFacts.GetInitializerOfBaseObjectCreationExpression(_objectCreationExpression);
        if (initializer != null)
        {
            var initializerExpressions = this.SyntaxFacts.GetExpressionsOfObjectCollectionInitializer(initializer);
            if (initializerExpressions is [var firstInit, ..])
            {
                // if we have an object creation, and it *already* has an initializer in it (like `new T { { x, y } }`)
                // this can't legally become a collection expression.  Unless there are exactly two elements in the
                // initializer, and we support k:v elements.
                if (_analyzeForCollectionExpression && this.IsComplexElementInitializer(firstInit, out var initializerElementCount))
                {
                    if (initializerElementCount != 2 || !this.SyntaxFacts.SupportsKeyValuePairElement(_objectCreationExpression.SyntaxTree.Options))
                        return false;
                }
 
                seenIndexAssignment = this.SyntaxFacts.IsElementAccessInitializer(firstInit);
                seenInvocation = !seenIndexAssignment;
 
                // An indexer can't be used with a collection expression (except for dictionary expressions).  So fail
                // out immediately if we see that.
                if (_analyzeForCollectionExpression && seenIndexAssignment && !this.SyntaxFacts.SupportsKeyValuePairElement(_objectCreationExpression.SyntaxTree.Options))
                    return false;
            }
        }
 
        if (State.ValuePattern != default)
        {
            foreach (var statement in this.State.GetSubsequentStatements())
            {
                cancellationToken.ThrowIfCancellationRequested();
                var match = TryAnalyzeStatement(statement, ref seenInvocation, ref seenIndexAssignment, cancellationToken);
                if (match is null)
                    break;
 
                postMatches.Add(match.Value);
            }
        }
 
        if (_analyzeForCollectionExpression)
        {
            return AnalyzeMatchesAndCollectionConstructorForCollectionExpression(
                preMatches, postMatches, out mayChangeSemantics, cancellationToken);
        }
 
        return true;
    }
 
    private CollectionMatch<SyntaxNode>? TryAnalyzeStatement(
        TStatementSyntax statement, ref bool seenInvocation, ref bool seenIndexAssignment, CancellationToken cancellationToken)
    {
        return _analyzeForCollectionExpression
            ? State.TryAnalyzeStatementForCollectionExpression(this.SyntaxHelper, statement, cancellationToken)
            : TryAnalyzeStatementForCollectionInitializer(statement, ref seenInvocation, ref seenIndexAssignment, cancellationToken);
    }
 
    private CollectionMatch<SyntaxNode>? TryAnalyzeStatementForCollectionInitializer(
        TStatementSyntax statement, ref bool seenInvocation, ref bool seenIndexAssignment, CancellationToken cancellationToken)
    {
        // At least one of these has to be false.
        Contract.ThrowIfTrue(seenInvocation && seenIndexAssignment);
 
        if (statement is not TExpressionStatementSyntax expressionStatement)
            return null;
 
        // Can't mix Adds and indexing.
        if (!seenIndexAssignment)
        {
            // Look for a call to Add or AddRange
            if (this.State.TryAnalyzeAddInvocation(
                    (TExpressionSyntax)this.SyntaxFacts.GetExpressionOfExpressionStatement(expressionStatement),
                    requiredArgumentName: null,
                    forCollectionExpression: false,
                    cancellationToken,
                    out var instance,
                    out var useKeyValue) &&
                this.State.ValuePatternMatches(instance))
            {
                seenInvocation = true;
                return new(expressionStatement, UseSpread: false, useKeyValue);
            }
        }
 
        if (!seenInvocation)
        {
            if (this.State.TryAnalyzeIndexAssignment(expressionStatement, cancellationToken, out var instance) &&
                this.State.ValuePatternMatches(instance))
            {
                seenIndexAssignment = true;
                return new(expressionStatement, UseSpread: false, UseKeyValue: this.State.SyntaxFacts.SupportsKeyValuePairElement(statement.SyntaxTree.Options));
            }
        }
 
        return null;
    }
 
    protected sealed override bool ShouldAnalyze(CancellationToken cancellationToken)
    {
        if (this.HasExistingInvalidInitializerForCollection())
            return false;
 
        return GetAddMethods(cancellationToken).Any();
    }
 
    protected ImmutableArray<IMethodSymbol> GetAddMethods(CancellationToken cancellationToken)
    {
        var type = this.SemanticModel.GetTypeInfo(_objectCreationExpression, cancellationToken).Type;
        if (type == null)
            return [];
 
        var addMethods = this.SemanticModel.LookupSymbols(
            _objectCreationExpression.SpanStart,
            container: type,
            name: WellKnownMemberNames.CollectionInitializerAddMethodName,
            includeReducedExtensionMethods: true);
        return addMethods.SelectAsArray(s => s is IMethodSymbol { Parameters: [_, ..] }, s => (IMethodSymbol)s);
    }
}