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

Add analyzer/fixer to suggest changing code like ImmutableArray.Create(1, 2, 3) to [1, 2, 3] #69473

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
066f617
Initial stubs
CyrusNajmabadi Aug 10, 2023
c3a37b4
Finish analyzer
CyrusNajmabadi Aug 10, 2023
e61fa3c
in progrss
CyrusNajmabadi Aug 10, 2023
dad4a88
Fix
CyrusNajmabadi Aug 10, 2023
013cdf0
Merge branch 'forkedFixProvider' into useCollectionExpressionForCreate
CyrusNajmabadi Aug 10, 2023
83b1fcf
Merge branch 'forkedFixProvider' into useCollectionExpressionForCreate
CyrusNajmabadi Aug 10, 2023
025a344
in progress
CyrusNajmabadi Aug 10, 2023
d5511e6
in progress
CyrusNajmabadi Aug 10, 2023
3255e54
in progress
CyrusNajmabadi Aug 10, 2023
3d3d625
Match work
CyrusNajmabadi Aug 10, 2023
c7bff97
in progress
CyrusNajmabadi Aug 10, 2023
0af0eac
Fixer
CyrusNajmabadi Aug 10, 2023
a13c1ba
Tests
CyrusNajmabadi Aug 10, 2023
1cbc7c9
Working test
CyrusNajmabadi Aug 10, 2023
6282a80
Add tests
CyrusNajmabadi Aug 10, 2023
d26b35f
Fixes
CyrusNajmabadi Aug 10, 2023
1cbe5bb
Fixes
CyrusNajmabadi Aug 10, 2023
7728f38
almost htere
CyrusNajmabadi Aug 10, 2023
06a1876
Closer
CyrusNajmabadi Aug 10, 2023
7bb6fb1
Proper fix
CyrusNajmabadi Aug 10, 2023
d44d2b7
Proper tab handling
CyrusNajmabadi Aug 10, 2023
c6c59e3
Update src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpC…
CyrusNajmabadi Aug 10, 2023
1dfec04
Merge remote-tracking branch 'upstream/main' into useCollectionExpres…
CyrusNajmabadi Aug 10, 2023
88a9f2f
Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Form…
CyrusNajmabadi Aug 10, 2023
ac740db
move
CyrusNajmabadi Aug 10, 2023
0261327
Merge branch 'useCollectionExpressionForCreate' of https://github.com…
CyrusNajmabadi Aug 10, 2023
e09a1f6
UPdate tests
CyrusNajmabadi Aug 11, 2023
d961d4e
Fix rules
CyrusNajmabadi Aug 11, 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/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CollectionExpressionMatch.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\UseCollectionExpressionHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionInitializer\CSharpUseCollectionInitializerAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private static void AnalyzeArrayCreationExpression(SyntaxNodeAnalysisContext con
ReportArrayCreationDiagnostics(context, syntaxTree, option, arrayCreationExpression);
}

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches(
public static ImmutableArray<CollectionExpressionMatch<StatementSyntax>> TryGetMatches(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type became generic as we don't have statements when converting expressions (like ImmutableArray.Create(1, 2, 3)) over to collection literals.

SemanticModel semanticModel,
ArrayCreationExpressionSyntax expression,
CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed partial class CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer
: AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
private const string CreateName = nameof(ImmutableArray.Create);
private const string CreateRangeName = nameof(ImmutableArray.CreateRange);

public const string UnwrapArgument = nameof(UnwrapArgument);

private static readonly ImmutableDictionary<string, string?> s_unwrapArgumentProperties =
ImmutableDictionary<string, string?>.Empty.Add(UnwrapArgument, UnwrapArgument);

public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptorWithId(
IDEDiagnosticIds.UseCollectionExpressionForCreateDiagnosticId,
EnforceOnBuildValues.UseCollectionExpressionForCreate,
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.UseCollectionExpressionForCreateDiagnosticId,
EnforceOnBuildValues.UseCollectionExpressionForCreate,
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);

public CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer()
: base(ImmutableDictionary<DiagnosticDescriptor, IOption2>.Empty
.Add(s_descriptor, CodeStyleOptions2.PreferCollectionExpression)
.Add(s_unnecessaryCodeDescriptor, CodeStyleOptions2.PreferCollectionExpression))
{
}

protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterCompilationStartAction(context =>
{
var compilation = context.Compilation;
if (!compilation.LanguageVersion().SupportsCollectionExpressions())
return;

var collectionBuilderAttribute = compilation.CollectionBuilderAttribute();
if (collectionBuilderAttribute is null)
return;

// 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.
context.RegisterCodeBlockStartAction<SyntaxKind>(context =>
{
context.RegisterSyntaxNodeAction(
context => AnalyzeInvocationExpression(context, collectionBuilderAttribute),
SyntaxKind.InvocationExpression);
});
});

private static void AnalyzeInvocationExpression(
SyntaxNodeAnalysisContext context,
INamedTypeSymbol collectionBuilderAttribute)
{
var semanticModel = context.SemanticModel;
var compilation = semanticModel.Compilation;
var syntaxTree = semanticModel.SyntaxTree;
var invocationExpression = (InvocationExpressionSyntax)context.Node;
var cancellationToken = context.CancellationToken;

// no point in analyzing if the option is off.
var option = context.GetAnalyzerOptions().PreferCollectionExpression;
if (!option.Value)
return;

// Looking for `XXX.Create(...)`
if (invocationExpression.Expression is not MemberAccessExpressionSyntax
{
RawKind: (int)SyntaxKind.SimpleMemberAccessExpression,
Name.Identifier.Value: CreateName or CreateRangeName,
} memberAccessExpression)
{
return;
}

var createMethod = semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol as IMethodSymbol;
if (createMethod is not { IsStatic: true })
return;

var factoryType = semanticModel.GetSymbolInfo(memberAccessExpression.Expression, cancellationToken).Symbol as INamedTypeSymbol;
if (factoryType is null)
return;

// The pattern is a type like `ImmutableArray` (non-generic), returning an instance of `ImmutableArray<T>`. The
// actual collection type (`ImmutableArray<T>`) has to have a `[CollectionBuilder(...)]` attribute on it that
// then points at the factory type.
var collectionBuilderAttributeData = createMethod.ReturnType.OriginalDefinition
.GetAttributes()
.FirstOrDefault(a => collectionBuilderAttribute.Equals(a.AttributeClass));
if (collectionBuilderAttributeData?.ConstructorArguments is not [{ Value: ITypeSymbol collectionBuilderType }, { Value: CreateName }])
return;

if (!factoryType.OriginalDefinition.Equals(collectionBuilderType.OriginalDefinition))
return;

// Ok, this is type that has a collection-builder option available. We can switch over if the current method
// we're calling has one of the following signatures:
//
// `Create()`. Trivial case, can be replaced with `[]`.
// `Create(T), Create(T, T), Create(T, T, T)` etc.
// `Create(params T[])` (passing as individual elements, or an array with an initializer)
// `Create(ReadOnlySpan<T>)` (passing as a stack-alloc with an initializer)
// `Create(IEnumerable<T>)` (passing as something with an initializer.
if (!IsCompatibleSignatureAndArguments(
compilation, invocationExpression, createMethod.OriginalDefinition,
out var unwrapArgument, cancellationToken))
{
return;
}

// Make sure we can actually use a collection expression in place of the full invocation.
if (!UseCollectionExpressionHelpers.CanReplaceWithCollectionExpression(
semanticModel, invocationExpression, skipVerificationForReplacedNode: true, cancellationToken))
{
return;
}

var locations = ImmutableArray.Create(invocationExpression.GetLocation());
var properties = unwrapArgument ? s_unwrapArgumentProperties : null;

context.ReportDiagnostic(DiagnosticHelper.Create(
s_descriptor,
memberAccessExpression.Name.Identifier.GetLocation(),
option.Notification.Severity,
additionalLocations: locations,
properties));

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

context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
s_unnecessaryCodeDescriptor,
additionalUnnecessaryLocations[0],
ReportDiagnostic.Default,
additionalLocations: locations,
additionalUnnecessaryLocations: additionalUnnecessaryLocations,
properties));
}

private static bool IsCompatibleSignatureAndArguments(
Compilation compilation,
InvocationExpressionSyntax invocationExpression,
IMethodSymbol originalCreateMethod,
out bool unwrapArgument,
CancellationToken cancellationToken)
{
unwrapArgument = false;

var arguments = invocationExpression.ArgumentList.Arguments;

// Don't bother offering if any of the arguments are named. It's unlikely for this to occur in practice, and it
// means we do not have to worry about order of operations.
if (arguments.Any(static a => a.NameColon != null))
return false;

if (originalCreateMethod.Name is CreateRangeName)
{
// If we have `CreateRange<T>(IEnumerable<T> values)` this is legal if we have an array, or no-arg object creation.
if (originalCreateMethod.Parameters is [
{
Type: INamedTypeSymbol
{
Name: nameof(IEnumerable<int>),
TypeArguments: [ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method }]
} enumerableType
}] && enumerableType.OriginalDefinition.Equals(compilation.IEnumerableOfTType()))
{
var argExpression = arguments[0].Expression;
if (argExpression
is ArrayCreationExpressionSyntax { Initializer: not null }
or ImplicitArrayCreationExpressionSyntax)
{
unwrapArgument = true;
return true;
}

if (argExpression is ObjectCreationExpressionSyntax objectCreation)
{
// Can't have any arguments, as we cannot preserve them once we grab out all the elements.
if (objectCreation.ArgumentList != null && objectCreation.ArgumentList.Arguments.Count > 0)
return false;

// If it's got an initializer, it has to be a collection initializer (or an empty object initializer);
if (objectCreation.Initializer.IsKind(SyntaxKind.ObjectCreationExpression) && objectCreation.Initializer.Expressions.Count > 0)
return false;

unwrapArgument = true;
return true;
}
}
}
else if (originalCreateMethod.Name is CreateName)
{
// `XXX.Create()` can be converted to `[]`
if (originalCreateMethod.Parameters.Length == 0)
return arguments.Count == 0;

// If we have `Create<T>(T)`, `Create<T>(T, T)` etc., then this is convertible.
if (originalCreateMethod.Parameters.All(static p => p.Type is ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method }))
return arguments.Count == originalCreateMethod.Parameters.Length;

// If we have `Create<T>(params T[])` this is legal if there are multiple arguments. Or a single argument that
// is an array literal.
if (originalCreateMethod.Parameters is [{ IsParams: true, Type: IArrayTypeSymbol { ElementType: ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method } } }])
{
if (arguments.Count >= 2)
return true;

if (arguments is [{ Expression: ArrayCreationExpressionSyntax { Initializer: not null } or ImplicitArrayCreationExpressionSyntax }])
{
unwrapArgument = true;
return true;
}

return false;
}

// If we have `Create<T>(ReadOnlySpan<T> values)` this is legal if a stack-alloc expression is passed along.
//
// Runtime needs to support inline arrays in order for this to be ok. Otherwise compiler will change the
// stack alloc to a heap alloc, which could be very bad for user perf.

if (arguments.Count == 1 &&
compilation.SupportsRuntimeCapability(RuntimeCapability.InlineArrayTypes) &&
originalCreateMethod.Parameters is [
{
Type: INamedTypeSymbol
{
Name: nameof(Span<int>) or nameof(ReadOnlySpan<int>),
TypeArguments: [ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method }]
} spanType
}])
{
if (spanType.OriginalDefinition.Equals(compilation.SpanOfTType()) ||
spanType.OriginalDefinition.Equals(compilation.ReadOnlySpanOfTType()))
{
if (arguments[0].Expression
is StackAllocArrayCreationExpressionSyntax { Initializer: not null }
or ImplicitStackAllocArrayCreationExpressionSyntax)
{
unwrapArgument = true;
return true;
}
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private static void AnalyzeExplicitStackAllocExpression(SyntaxNodeAnalysisContex
additionalUnnecessaryLocations: additionalUnnecessaryLocations));
}

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches(
public static ImmutableArray<CollectionExpressionMatch<StatementSyntax>> TryGetMatches(
SemanticModel semanticModel,
StackAllocArrayCreationExpressionSyntax expression,
CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.UseCollectionInitializer;

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

/// <inheritdoc cref="Match{TStatementSyntax}"/>
internal readonly record struct CollectionExpressionMatch(
StatementSyntax Statement,
bool UseSpread);
internal readonly record struct CollectionExpressionMatch<TMatchNode>(
TMatchNode Node,
bool UseSpread) where TMatchNode : SyntaxNode;
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ private static bool ShouldReplaceExistingExpressionEntirely(
return false;
}

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches<TArrayCreationExpressionSyntax>(
public static ImmutableArray<CollectionExpressionMatch<StatementSyntax>> TryGetMatches<TArrayCreationExpressionSyntax>(
SemanticModel semanticModel,
TArrayCreationExpressionSyntax expression,
Func<TArrayCreationExpressionSyntax, TypeSyntax> getType,
Expand All @@ -310,7 +310,7 @@ public static ImmutableArray<CollectionExpressionMatch> TryGetMatches<TArrayCrea
if (getType(expression) is not ArrayTypeSyntax { RankSpecifiers: [{ Sizes: [var size] }, ..] })
return default;

using var _ = ArrayBuilder<CollectionExpressionMatch>.GetInstance(out var matches);
using var _ = ArrayBuilder<CollectionExpressionMatch<StatementSyntax>>.GetInstance(out var matches);

var initializer = getInitializer(expression);
if (size is OmittedArraySizeExpressionSyntax)
Expand Down
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<Compile Include="$(MSBuildThisFileDirectory)UpdateProjectToAllowUnsafe\CSharpUpdateProjectToAllowUnsafeCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UpgradeProject\CSharpUpgradeProjectCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpCollectionExpressionRewriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForCreateCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForArrayCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForStackAllocCodeFixProvider.cs" />
Expand Down
Loading