Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CA1861 Avoid constant arrays as arguments #5383

Merged
merged 82 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5d0fd6a
AvoidConstArrays analyzer drafted
steveberdy Aug 17, 2021
8ea2352
Renamed rule id
steveberdy Aug 17, 2021
6d63e0b
Edited comments in resources
steveberdy Aug 17, 2021
86c559e
Remove formatting of full document
steveberdy Aug 17, 2021
aded7e0
Renamed analyzer
steveberdy Aug 17, 2021
59e968a
Combined unit tests
steveberdy Aug 17, 2021
39e2526
Clean up analyzer literal array evaluation
steveberdy Aug 17, 2021
e416a18
Added tests for trivia
steveberdy Aug 18, 2021
fe7abab
Tests pass for argument trailing trivia
steveberdy Aug 18, 2021
038c55e
Edited headers for analyzer files
steveberdy Aug 18, 2021
da49630
VB extension method tests added, not passing yet
steveberdy Aug 19, 2021
ac36584
Tests pass for VB extension method usage
steveberdy Aug 20, 2021
c03ce9a
Added tests for extractions
steveberdy Aug 20, 2021
5b2c843
Run dotnet msbuild /t:Pack
jeffhandley Aug 20, 2021
cfdd176
Added checks for ReadOnlySpan types
steveberdy Aug 22, 2021
97e9261
Added comments for warning suppression
steveberdy Aug 25, 2021
317ed22
Clean up parameter name property assignment
steveberdy Aug 25, 2021
124afdc
Update analyzer name in shipment doc
steveberdy Sep 2, 2021
b606194
Use symbol equality for ReadOnlySpan type checks
steveberdy Sep 8, 2021
b07a941
Merge branch 'release/7.0.1xx' into ca1848
steveberdy Sep 10, 2021
a74af29
Added comments for better readability
steveberdy Sep 17, 2021
5c5c96a
Merge branch 'release/7.0.1xx' of https://github.com/dotnet/roslyn-an…
steveberdy Sep 21, 2021
cd3b32f
Goodbye to the '49er
steveberdy Sep 21, 2021
aa34565
Addressed issues
steveberdy Sep 21, 2021
d0e7692
Merge branch 'main' into ca1848
steveberdy Oct 10, 2021
89d8e3d
Addressed style according to #5406
steveberdy Oct 10, 2021
b882ffb
Added test case, fixed formatting
steveberdy Oct 10, 2021
92ad53b
Update test case
steveberdy Oct 11, 2021
1acb9d7
Ran msbuild
steveberdy Oct 12, 2021
3ddf816
Merge branch 'ca1848' of https://github.com/steveberdy/roslyn-analyze…
steveberdy Oct 12, 2021
f606594
Updated absolute .All check with .Any check
steveberdy Oct 12, 2021
461e0ef
Member name from type reflects original definition
steveberdy Oct 12, 2021
274733c
Tests pass for lambda arguments
steveberdy Oct 13, 2021
4a199ae
Removed unnecessary check for action type, combined logic, etc
steveberdy Oct 13, 2021
5b8e57f
Shadowed context
steveberdy Oct 13, 2021
7176fd5
Remove duplicate expensive calls
steveberdy Oct 13, 2021
4d8fad0
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Oct 14, 2021
e811a00
Update localizable strings
steveberdy Oct 15, 2021
156c8a8
Ran msbuild
steveberdy Oct 16, 2021
3e4c7d2
Update MicrosoftNetCoreAnalyzersResources.resx
steveberdy Oct 27, 2021
a89d710
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Nov 18, 2021
74e6f15
Update analyzer id
steveberdy Nov 18, 2021
b4dc60c
Updated xlf files
steveberdy Nov 19, 2021
47a0232
Applied feedback, updated xlf files
steveberdy Nov 19, 2021
52fddfd
Merge branch 'ca1848' of https://github.com/steveberdy/roslyn-analyze…
steveberdy Nov 19, 2021
cc88e84
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Nov 26, 2021
97667ae
Uh... I forgot what this commit was about
steveberdy Dec 17, 2021
0f55125
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Dec 17, 2021
ab10a36
Update src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/Avoi…
steveberdy Apr 1, 2022
766135a
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Apr 26, 2022
75a6e2e
Update rule id
steveberdy Apr 26, 2022
99b2927
Remove MyCodeAction
steveberdy Apr 26, 2022
85ae961
Added assembly reference for globalization
steveberdy Apr 26, 2022
3075114
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy May 25, 2022
7146c43
Update to ignore params arguments
steveberdy May 25, 2022
d0aad93
Added tests for params arguments
steveberdy May 25, 2022
95a805f
Split tests between CS and VB
steveberdy May 25, 2022
72c183a
Separated tests by topic
steveberdy May 26, 2022
4e61c9d
Edited tests, ran dotnet msbuild /t:pack
steveberdy May 26, 2022
ea6c620
Update extraction, apply feedback
steveberdy May 27, 2022
71c4c43
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy May 27, 2022
e8ae4a8
Ran dotnet msbuild /t:Pack
steveberdy May 27, 2022
02759e6
Added batch fix test
steveberdy May 28, 2022
4174f60
Update GetAccessibility
steveberdy May 30, 2022
3c275fc
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Jun 4, 2022
06794e9
Updated rule id
steveberdy Jun 4, 2022
de370cb
Merge branch 'main' into ca1848
steveberdy Jun 27, 2022
41ddd20
Add namespace tests
steveberdy Nov 26, 2022
5d6ee7e
Merge branch 'ca1848' of https://github.com/steveberdy/roslyn-analyze…
steveberdy Nov 26, 2022
2fc1c91
Resolve merge conflicts
steveberdy Nov 26, 2022
de8d4b7
Linting
steveberdy Nov 26, 2022
0680bd7
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Mar 15, 2023
a2e54cd
updated rule id
steveberdy Mar 15, 2023
cca975d
formatting to fix linting
steveberdy Mar 15, 2023
f239689
fix CI errors pt 1
steveberdy Mar 15, 2023
ea8dd59
fix CI errors pt 2
steveberdy Mar 15, 2023
f15b53f
fix CI errors pt 3
steveberdy Mar 15, 2023
042fefe
perf improvement and added namespaces to all tests
steveberdy Mar 15, 2023
8a3cd50
Merge branch 'main' of https://github.com/dotnet/roslyn-analyzers int…
steveberdy Mar 15, 2023
fec9a2f
added exclusion for readonly field assignments, more tests
steveberdy Mar 17, 2023
fc8992d
Fix issues in fixer, do not warn for static constructor and readonly …
buyaa-n May 5, 2023
bbf1ebe
Apply feedbacks
buyaa-n May 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CA1857 | Performance | Warning | ConstantExpectedAnalyzer, [Documentation](https
CA1858 | Performance | Info | UseStartsWithInsteadOfIndexOfComparisonWithZero, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858)
CA1859 | Performance | Info | UseConcreteTypeAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
CA1860 | Performance | Info | PreferLengthCountIsEmptyOverAnyAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1860)
CA1861 | Performance | Info | AvoidConstArrays, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861)
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)

### Removed Rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@
<data name="AvoidUnsealedAttributesMessage" xml:space="preserve">
<value>Avoid unsealed attributes</value>
</data>
<data name="AvoidConstArraysTitle" xml:space="preserve">
<value>Avoid constant arrays as arguments</value>
</data>
<data name="AvoidConstArraysCodeFixTitle" xml:space="preserve">
<value>Extract to static readonly field</value>
</data>
<data name="AvoidConstArraysDescription" xml:space="preserve">
<value>Constant arrays passed as arguments are not reused which implies a performance overhead. Consider extracting them to 'static readonly' fields to improve performance.</value>
<comment>{Locked="static readonly"}</comment>
</data>
<data name="AvoidConstArraysMessage" xml:space="preserve">
<value>Prefer 'static readonly' fields over local constant array arguments</value>
<comment>{Locked="static readonly"}</comment>
</data>
steveberdy marked this conversation as resolved.
Show resolved Hide resolved
<data name="TestForEmptyStringsUsingStringLengthTitle" xml:space="preserve">
<value>Test for empty strings using string length</value>
</data>
Expand Down Expand Up @@ -2040,4 +2054,4 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="PreferIsEmptyOverAnyMessage" xml:space="preserve">
<value>Prefer an 'IsEmpty' check rather than using 'Any()', both for clarity and for performance</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Globalization;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
/// <summary>
/// CA1861: Avoid constant arrays as arguments. Replace with static readonly arrays.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public sealed class AvoidConstArraysFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(AvoidConstArraysAnalyzer.RuleId);

private static readonly ImmutableArray<string> s_collectionMemberEndings = ImmutableArray.Create("array", "collection", "enumerable", "list");

// See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Document document = context.Document;
SyntaxNode root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
SyntaxNode node = root.FindNode(context.Span);

context.RegisterCodeFix(CodeAction.Create(
MicrosoftNetCoreAnalyzersResources.AvoidConstArraysCodeFixTitle,
async ct => await ExtractConstArrayAsync(document, root, node, context.Diagnostics[0].Properties, ct).ConfigureAwait(false),
equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.AvoidConstArraysCodeFixTitle)),
context.Diagnostics);
}

private static async Task<Document> ExtractConstArrayAsync(Document document, SyntaxNode root, SyntaxNode node,
ImmutableDictionary<string, string?> properties, CancellationToken cancellationToken)
{
SemanticModel model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
SyntaxGenerator generator = editor.Generator;
IArrayCreationOperation arrayArgument = GetArrayCreationOperation(node, model, cancellationToken, out bool isInvoked);
INamedTypeSymbol containingType = model.GetEnclosingSymbol(node.SpanStart, cancellationToken).ContainingType;

// Get a valid member name for the extracted constant
string newMemberName = GetExtractedMemberName(containingType.MemberNames, properties["paramName"] ?? GetMemberNameFromType(arrayArgument));

// Get method containing the symbol that is being diagnosed
IOperation? methodContext = arrayArgument.GetAncestor<IMethodBodyOperation>(OperationKind.MethodBody);
methodContext ??= arrayArgument.GetAncestor<IBlockOperation>(OperationKind.Block); // VB methods have a different structure than CS methods
steveberdy marked this conversation as resolved.
Show resolved Hide resolved

// Create the new member
SyntaxNode newMember = generator.FieldDeclaration(
newMemberName,
generator.TypeExpression(arrayArgument.Type),
GetAccessibility(methodContext is null ? null : model.GetEnclosingSymbol(methodContext.Syntax.SpanStart, cancellationToken)),
DeclarationModifiers.Static | DeclarationModifiers.ReadOnly,
arrayArgument.Syntax.WithoutTrailingTrivia() // don't include extra trivia before the end of the declaration
);

// Add any additional formatting
if (methodContext is not null)
{
newMember = newMember.FormatForExtraction(methodContext.Syntax);
}

ISymbol lastFieldOrPropertySymbol = containingType.GetMembers().LastOrDefault(x => x is IFieldSymbol or IPropertySymbol);
if (lastFieldOrPropertySymbol is not null)
{
var span = lastFieldOrPropertySymbol.Locations.First().SourceSpan;
if (root.FullSpan.Contains(span))
{
// Insert after fields or properties
SyntaxNode lastFieldOrPropertyNode = root.FindNode(span);
editor.InsertAfter(generator.GetDeclaration(lastFieldOrPropertyNode), newMember);
}
else
{
// Span not found
editor.InsertBefore(methodContext?.Syntax, newMember);
}
}
else
{
// No fields or properties, insert right before the containing method for simplicity
editor.InsertBefore(methodContext?.Syntax, newMember);
}

// Replace argument with a reference to our new member
SyntaxNode identifier = generator.IdentifierName(newMemberName);
if (isInvoked)
{
editor.ReplaceNode(node, generator.WithExpression(identifier, node));
}
else
{
// add any extra trivia that was after the original argument
editor.ReplaceNode(node, generator.Argument(identifier).WithTriviaFrom(arrayArgument.Syntax));
}

// Return changed document
return editor.GetChangedDocument();
}

private static IArrayCreationOperation GetArrayCreationOperation(SyntaxNode node, SemanticModel model, CancellationToken cancellationToken, out bool isInvoked)
{
// The analyzer only passes a diagnostic for two scenarios, each having an IArrayCreationOperation:
// 1. The node is an IArgumentOperation that is a direct parent of an IArrayCreationOperation
// 2. The node is an IArrayCreationOperation already, as it was pulled from an
// invocation, like with LINQ extension methods

// If this is a LINQ invocation, the node is already an IArrayCreationOperation
if (model.GetOperation(node, cancellationToken) is IArrayCreationOperation arrayCreation)
{
isInvoked = true;
return arrayCreation;
}
// Otherwise, we'll get the IArrayCreationOperation from the argument node's child
isInvoked = false;
return (IArrayCreationOperation)model.GetOperation(node.ChildNodes().First(), cancellationToken);
}

private static string GetExtractedMemberName(IEnumerable<string> memberNames, string parameterName)
{
bool hasCollectionEnding = s_collectionMemberEndings.Any(x => parameterName.EndsWith(x, true, CultureInfo.InvariantCulture));

if (parameterName == "source" // for LINQ, "sourceArray" is clearer than "source"
|| (memberNames.Contains(parameterName) && !hasCollectionEnding))
{
parameterName += "Array";
}

if (memberNames.Contains(parameterName))
{
int suffix = 0;
while (memberNames.Contains(parameterName + suffix))
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
{
suffix++;
}

return parameterName + suffix;
}

return parameterName;
}

private static string GetMemberNameFromType(IArrayCreationOperation arrayCreationOperation)
{
#pragma warning disable CA1308 // Normalize strings to uppercase
return ((IArrayTypeSymbol)arrayCreationOperation.Type).ElementType.OriginalDefinition.Name.ToLowerInvariant() + "Array";
#pragma warning restore CA1308 // Normalize strings to uppercase
}

private static Accessibility GetAccessibility(ISymbol? methodSymbol)
{
// If private or public, return private since public accessibility not wanted for fields by default
return methodSymbol?.GetResultantVisibility() switch
{
// Return internal if internal
SymbolVisibility.Internal => Accessibility.Internal,
_ => Accessibility.Private
};
}
}

internal static class SyntaxNodeExtensions
{
internal static SyntaxNode FormatForExtraction(this SyntaxNode node, SyntaxNode previouslyContainingNode)
{
return node.HasTrailingTrivia ? node : node.WithTrailingTrivia(previouslyContainingNode.GetTrailingTrivia());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
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>
/// CA1861: Avoid constant arrays as arguments. Replace with static readonly arrays.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class AvoidConstArraysAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1861";

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(RuleId,
CreateLocalizableResourceString(nameof(AvoidConstArraysTitle)),
CreateLocalizableResourceString(nameof(AvoidConstArraysMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(AvoidConstArraysDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
INamedTypeSymbol? readonlySpanType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1);
INamedTypeSymbol? functionType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemFunc2);

// Analyzes an argument operation
context.RegisterOperationAction(context =>
{
IArgumentOperation? argumentOperation;

if (context.ContainingSymbol is IMethodSymbol method && method.MethodKind == MethodKind.StaticConstructor)
{
return;
}

if (context.Operation is IArrayCreationOperation arrayCreationOperation) // For arrays passed as arguments
{
argumentOperation = arrayCreationOperation.GetAncestor<IArgumentOperation>(OperationKind.Argument);
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved

// If no argument, return
// If argument is passed as a params array but isn't itself an array, return
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
if (argumentOperation is null || (argumentOperation.Parameter.IsParams && arrayCreationOperation.IsImplicit))
{
return;
}
}
else if (context.Operation is IInvocationOperation invocationOperation) // For arrays passed in extension methods, like in LINQ
{
IEnumerable<IOperation> invocationDescendants = invocationOperation.Descendants();
if (invocationDescendants.Any(x => x is IArrayCreationOperation)
&& invocationDescendants.Any(x => x is IArgumentOperation))
{
// This is an invocation that contains an array as an argument
// This will get caught by the first case in another cycle
return;
}

argumentOperation = invocationOperation.Arguments.FirstOrDefault();
if (argumentOperation is not null)
{
if (argumentOperation.Value is not IConversionOperation conversionOperation
|| conversionOperation.Operand is not IArrayCreationOperation arrayCreation)
{
return;
}

arrayCreationOperation = arrayCreation;
}
else // An invocation, extension or regular, has an argument, unless it's a VB extension method call
{
// For VB extension method invocations, find a matching child
arrayCreationOperation = (IArrayCreationOperation)invocationDescendants
.FirstOrDefault(x => x is IArrayCreationOperation);
if (arrayCreationOperation is null)
{
return;
}
}
}
else
{
return;
}

// Must be literal array
if (arrayCreationOperation.Initializer.ElementValues.Any(x => x is not ILiteralOperation))
{
return;
}

string? paramName = null;
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
if (argumentOperation is not null)
{
IFieldInitializerOperation? fieldInitializer = argumentOperation.GetAncestor<IFieldInitializerOperation>(
OperationKind.FieldInitializer, f => f.InitializedFields.Any(x => x.IsReadOnly));
IPropertyInitializerOperation? propertyInitializer = argumentOperation.GetAncestor<IPropertyInitializerOperation>(
OperationKind.PropertyInitializer, p => p.InitializedProperties.Any(x => x.IsReadOnly));

if (fieldInitializer is not null || propertyInitializer is not null)
{
return;
}

ITypeSymbol originalDefinition = argumentOperation.Parameter.Type.OriginalDefinition;

// Can't be a ReadOnlySpan, as those are already optimized
if (SymbolEqualityComparer.Default.Equals(readonlySpanType, originalDefinition))
{
return;
}

// Check if the parameter is a function so the name can be set to null
// Otherwise, the parameter name doesn't reflect the array creation as well
bool isDirectlyInsideLambda = originalDefinition.Equals(functionType);

// Parameter shouldn't have same containing type as the context, to prevent naming ambiguity
// Ignore parameter name if we're inside a lambda function
if (!isDirectlyInsideLambda && !argumentOperation.Parameter.ContainingType.Equals(context.ContainingSymbol.ContainingType))
{
paramName = argumentOperation.Parameter.Name;
}
}

ImmutableDictionary<string, string?>.Builder properties = ImmutableDictionary.CreateBuilder<string, string?>();
properties.Add("paramName", paramName);

context.ReportDiagnostic(arrayCreationOperation.CreateDiagnostic(Rule, properties.ToImmutable()));
},
OperationKind.ArrayCreation,
OperationKind.Invocation);
steveberdy marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}
Loading