| File: Microsoft.NetCore.Analyzers\Runtime\AvoidZeroLengthArrayAllocations.cs | Web Access |
| Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Immutable; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Runtime { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA1825: <inheritdoc cref="AvoidZeroLengthArrayAllocationsTitle"/> /// Base type for an analyzer that looks for empty array allocations and recommends their replacement. /// </summary> public abstract class AvoidZeroLengthArrayAllocationsAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA1825"; /// <summary>The name of the Empty method on System.Array.</summary> internal const string ArrayEmptyMethodName = "Empty"; private static readonly SymbolDisplayFormat ReportFormat = new( memberOptions: SymbolDisplayMemberOptions.IncludeContainingType | SymbolDisplayMemberOptions.IncludeParameters, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes); /// <summary>The diagnostic descriptor used when Array.Empty should be used instead of a new array allocation.</summary> internal static readonly DiagnosticDescriptor UseArrayEmptyDescriptor = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(AvoidZeroLengthArrayAllocationsTitle)), CreateLocalizableResourceString(nameof(AvoidZeroLengthArrayAllocationsMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, description: null, isPortedFxCopRule: false, isDataflowRule: false); /// <summary>Gets the set of supported diagnostic descriptors from this analyzer.</summary> public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(UseArrayEmptyDescriptor); public sealed override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); // When compilation begins, check whether Array.Empty<T> is available. // Only if it is, register the syntax node action provided by the derived implementations. context.RegisterCompilationStartAction(context => { INamedTypeSymbol typeSymbol = context.Compilation.GetSpecialType(SpecialType.System_Array); if (typeSymbol.DeclaredAccessibility == Accessibility.Public) { if (typeSymbol.GetMembers(ArrayEmptyMethodName).FirstOrDefault() is IMethodSymbol methodSymbol && methodSymbol.DeclaredAccessibility == Accessibility.Public && methodSymbol.IsStatic && methodSymbol.Arity == 1 && methodSymbol.Parameters.IsEmpty) { var linqExpressionType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1); context.RegisterOperationAction(c => AnalyzeOperation(c, methodSymbol, linqExpressionType), OperationKind.ArrayCreation); } } }); } private void AnalyzeOperation(OperationAnalysisContext context, IMethodSymbol arrayEmptyMethodSymbol, INamedTypeSymbol? linqExpressionType) { AnalyzeOperation(context, arrayEmptyMethodSymbol, linqExpressionType, IsAttributeSyntax, IsCollectionExpressionSyntax); } private static void AnalyzeOperation(OperationAnalysisContext context, IMethodSymbol arrayEmptyMethodSymbol, INamedTypeSymbol? linqExpressionType, Func<SyntaxNode, bool> isAttributeSyntax, Func<SyntaxNode, bool> isCollectionExpressionSyntax) { IArrayCreationOperation arrayCreationExpression = (IArrayCreationOperation)context.Operation; // Bail out for compiler-generated array creations synthesized // during lowering of collection expressions (e.g. `List<int> x = [1, 2, 3]`). if (isCollectionExpressionSyntax(arrayCreationExpression.Syntax)) { return; } // We can't replace array allocations in attributes, as they're persisted to metadata // TODO: Once we have operation walkers, we can replace this syntactic check with an operation-based check. if (arrayCreationExpression.Syntax.AncestorsAndSelf().Any(isAttributeSyntax)) { return; } if (arrayCreationExpression.IsWithinExpressionTree(linqExpressionType)) { return; } if (arrayCreationExpression.DimensionSizes.Length == 1) { IOperation dimensionSize = arrayCreationExpression.DimensionSizes[0]; if (dimensionSize.HasConstantValue(0)) { // Workaround for https://github.com/dotnet/roslyn/issues/10214 // Bail out for compiler generated params array creation. if (IsCompilerGeneratedParamsArray(arrayCreationExpression, context)) { return; } // pointers can't be used as generic arguments var elementType = arrayCreationExpression.GetElementType(); if (elementType == null) { return; } if (elementType.TypeKind != TypeKind.Pointer) { var constructed = arrayEmptyMethodSymbol.Construct(elementType); string typeName = constructed.ToDisplayString(ReportFormat); context.ReportDiagnostic(arrayCreationExpression.Syntax.CreateDiagnostic(UseArrayEmptyDescriptor, typeName)); } } } } private static bool IsCompilerGeneratedParamsArray(IArrayCreationOperation arrayCreationExpression, OperationAnalysisContext context) { var model = arrayCreationExpression.SemanticModel!; // Compiler generated array creation seems to just use the syntax from the parent. var parent = model.GetOperation(arrayCreationExpression.Syntax, context.CancellationToken); if (parent == null) { return false; } ISymbol? targetSymbol = null; var arguments = ImmutableArray<IArgumentOperation>.Empty; if (parent is IInvocationOperation invocation) { targetSymbol = invocation.TargetMethod; arguments = invocation.Arguments; } else { if (parent is IObjectCreationOperation objectCreation) { targetSymbol = objectCreation.Constructor; arguments = objectCreation.Arguments; } else if (parent is IPropertyReferenceOperation propertyReference) { targetSymbol = propertyReference.Property; arguments = propertyReference.Arguments; } } if (targetSymbol == null) { return false; } var parameters = targetSymbol.GetParameters(); if (parameters.IsEmpty || !parameters[^1].IsParams) { return false; } // At this point the array creation is known to be compiler synthesized as part of a call // to a method with a params parameter, and so it is probably sound to return true at this point. // As a sanity check, verify that the last argument to the call is equivalent to the array creation. // (Comparing for object identity does not work because the semantic model can return a fresh operation tree.) var lastArgument = arguments.LastOrDefault(); return lastArgument != null && lastArgument.Value.Syntax == arrayCreationExpression.Syntax && AreEquivalentZeroLengthArrayCreations(arrayCreationExpression, lastArgument.Value as IArrayCreationOperation); } private static bool AreEquivalentZeroLengthArrayCreations(IArrayCreationOperation? first, IArrayCreationOperation? second) { if (first == null || second == null) { return false; } ImmutableArray<IOperation> sizes = first.DimensionSizes; if (sizes.Length != 1 || !sizes[0].HasConstantValue(0)) { return false; } sizes = second.DimensionSizes; if (sizes.Length != 1 || !sizes[0].HasConstantValue(0)) { return false; } return first.Type?.Equals(second.Type) == true; } protected abstract bool IsAttributeSyntax(SyntaxNode node); /// <summary> /// Checks whether the given syntax node represents a collection expression (e.g. <c>[1, 2, 3]</c> in C#). /// </summary> protected abstract bool IsCollectionExpressionSyntax(SyntaxNode node); } }