File: src\roslyn\src\Analyzers\CSharp\Analyzers\UseCollectionExpression\CSharpUseCollectionExpressionForNewDiagnosticAnalyzer.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.CodeStyle;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

using static UseCollectionExpressionHelpers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed partial class CSharpUseCollectionExpressionForNewDiagnosticAnalyzer()
    : AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer(
        IDEDiagnosticIds.UseCollectionExpressionForNewDiagnosticId,
        EnforceOnBuildValues.UseCollectionExpressionForNew)
{
    protected override void InitializeWorker(CodeBlockStartAnalysisContext<SyntaxKind> context, INamedTypeSymbol? expressionType)
    {
        context.RegisterSyntaxNodeAction(context => AnalyzeObjectCreationExpression(context, expressionType), SyntaxKind.ObjectCreationExpression);
        context.RegisterSyntaxNodeAction(context => AnalyzeImplicitObjectCreationExpression(context, expressionType), SyntaxKind.ImplicitObjectCreationExpression);
    }

    private void AnalyzeObjectCreationExpression(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType)
        => AnalyzeBaseObjectCreationExpression(context, (BaseObjectCreationExpressionSyntax)context.Node, expressionType);

    private void AnalyzeImplicitObjectCreationExpression(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType)
        => AnalyzeBaseObjectCreationExpression(context, (BaseObjectCreationExpressionSyntax)context.Node, expressionType);

    private void AnalyzeBaseObjectCreationExpression(
        SyntaxNodeAnalysisContext context, BaseObjectCreationExpressionSyntax objectCreationExpression, INamedTypeSymbol? expressionType)
    {
        var semanticModel = context.SemanticModel;
        var compilation = semanticModel.Compilation;
        var syntaxTree = semanticModel.SyntaxTree;
        var cancellationToken = context.CancellationToken;

        if (objectCreationExpression is not { ArgumentList.Arguments: [var argument], Initializer: null })
            return;

        // no point in analyzing if the option is off.
        var option = context.GetAnalyzerOptions().PreferCollectionExpression;
        if (option.Value is CollectionExpressionPreference.Never || ShouldSkipAnalysis(context, option.Notification))
            return;

        var symbol = semanticModel.GetSymbolInfo(objectCreationExpression, cancellationToken).Symbol;
        if (symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor, Parameters: [var constructorParameter] } ||
            constructorParameter.Type.Name != nameof(IEnumerable<>))
        {
            return;
        }

        if (!Equals(compilation.IEnumerableOfTType(), constructorParameter.Type.OriginalDefinition))
            return;

        if (!IsArgumentCompatibleWithIEnumerableOfT(semanticModel, argument, out var unwrapArgument, out var useSpread, cancellationToken))
            return;

        // Make sure we can actually use a collection expression in place of the full invocation.
        var allowSemanticsChange = option.Value is CollectionExpressionPreference.WhenTypesLooselyMatch;
        if (!CanReplaceWithCollectionExpression(
                semanticModel, objectCreationExpression, expressionType, isSingletonInstance: false, allowSemanticsChange, skipVerificationForReplacedNode: true, cancellationToken, out var changesSemantics))
        {
            return;
        }

        // Because we want to replace `new X(enumerable)` with `[.. enumerable]` we need to make sure that the final
        // type supports the collection initialization pattern (specifically that it exposes an Add method that takes
        // the element type).  This prevents us from working on certain types that do allow the former form but not the
        // latter.
        // If the constructor took an IEnumerable<T>, ensure we find a `public Add(T)` method on the type.
        var constructorParameterTypeArg = constructorParameter.Type.GetTypeArguments().Single();
        if (!symbol.ContainingType
                .GetMembers(nameof(IList.Add))
                .OfType<IMethodSymbol>()
                .Any(m => m is { DeclaredAccessibility: Accessibility.Public, IsStatic: false, Parameters: [var addParameter] } &&
                        addParameter.Type.Equals(constructorParameterTypeArg)))
        {
            return;
        }

        var locations = ImmutableArray.Create(objectCreationExpression.GetLocation());
        var properties = GetDiagnosticProperties(unwrapArgument, useSpread, changesSemantics);

        context.ReportDiagnostic(DiagnosticHelper.Create(
            Descriptor,
            objectCreationExpression.NewKeyword.GetLocation(),
            option.Notification,
            context.Options,
            additionalLocations: locations,
            properties));

        var additionalUnnecessaryLocations = ImmutableArray.Create(
            syntaxTree.GetLocation(TextSpan.FromBounds(
                objectCreationExpression.SpanStart,
                objectCreationExpression.ArgumentList.OpenParenToken.Span.End)),
            objectCreationExpression.ArgumentList.CloseParenToken.GetLocation());

        context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
            UnnecessaryCodeDescriptor,
            additionalUnnecessaryLocations[0],
            NotificationOption2.ForSeverity(UnnecessaryCodeDescriptor.DefaultSeverity),
            context.Options,
            additionalLocations: locations,
            additionalUnnecessaryLocations: additionalUnnecessaryLocations,
            properties));
    }
}