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

Support converting arrays, whose elements are assigned afterwards, to a collection expression. #69415

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -58,12 +58,53 @@ protected override void InitializeWorker(AnalysisContext context)
context.RegisterCodeBlockStartAction<SyntaxKind>(context =>
{
context.RegisterSyntaxNodeAction(
context => AnalyzeArrayInitializer(context),
context => AnalyzeArrayInitializerExpression(context),
SyntaxKind.ArrayInitializerExpression);

context.RegisterSyntaxNodeAction(
context => AnalyzeArrayCreationExpression(context),
SyntaxKind.ArrayCreationExpression);
});
});

private static void AnalyzeArrayInitializer(SyntaxNodeAnalysisContext context)
private static void AnalyzeArrayCreationExpression(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var syntaxTree = semanticModel.SyntaxTree;
var arrayCreationExpression = (ArrayCreationExpressionSyntax)context.Node;
var cancellationToken = context.CancellationToken;

// Don't analyze arrays with initializers here, they're handled in AnalyzeArrayInitializerExpression instead.
if (arrayCreationExpression.Initializer != null)
return;

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

// Analyze the statements that follow to see if they can initialize this array.
var matches = TryGetMatches(semanticModel, arrayCreationExpression, cancellationToken);
if (matches.IsDefault)
return;

ReportArrayCreationDiagnostics(context, syntaxTree, option, arrayCreationExpression);
}

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches(
SemanticModel semanticModel,
ArrayCreationExpressionSyntax expression,
CancellationToken cancellationToken)
{
return UseCollectionExpressionHelpers.TryGetMatches(
semanticModel,
expression,
static e => e.Type,
static e => e.Initializer,
cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

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

calls into new helper that shares logic with stackalloc creation analysis.

}

private static void AnalyzeArrayInitializerExpression(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var syntaxTree = semanticModel.SyntaxTree;
Expand Down Expand Up @@ -93,27 +134,7 @@ private static void AnalyzeArrayInitializer(SyntaxNodeAnalysisContext context)

if (isConcreteOrImplicitArrayCreation)
{
var locations = ImmutableArray.Create(arrayCreationExpression.GetLocation());
context.ReportDiagnostic(DiagnosticHelper.Create(
s_descriptor,
arrayCreationExpression.GetFirstToken().GetLocation(),
option.Notification.Severity,
additionalLocations: locations,
properties: null));

var additionalUnnecessaryLocations = ImmutableArray.Create(
syntaxTree.GetLocation(TextSpan.FromBounds(
arrayCreationExpression.SpanStart,
arrayCreationExpression is ArrayCreationExpressionSyntax arrayCreation
? arrayCreation.Type.Span.End
: ((ImplicitArrayCreationExpressionSyntax)arrayCreationExpression).CloseBracketToken.Span.End)));

context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
s_unnecessaryCodeDescriptor,
additionalUnnecessaryLocations[0],
ReportDiagnostic.Default,
additionalLocations: locations,
additionalUnnecessaryLocations: additionalUnnecessaryLocations));
ReportArrayCreationDiagnostics(context, syntaxTree, option, arrayCreationExpression);
}
else
{
Expand All @@ -129,4 +150,29 @@ arrayCreationExpression is ArrayCreationExpressionSyntax arrayCreation
properties: null));
}
}

private static void ReportArrayCreationDiagnostics(SyntaxNodeAnalysisContext context, SyntaxTree syntaxTree, CodeStyleOption2<bool> option, ExpressionSyntax expression)
{
var locations = ImmutableArray.Create(expression.GetLocation());
context.ReportDiagnostic(DiagnosticHelper.Create(
s_descriptor,
expression.GetFirstToken().GetLocation(),
option.Notification.Severity,
additionalLocations: locations,
properties: null));

var additionalUnnecessaryLocations = ImmutableArray.Create(
syntaxTree.GetLocation(TextSpan.FromBounds(
expression.SpanStart,
expression is ArrayCreationExpressionSyntax arrayCreationExpression
? arrayCreationExpression.Type.Span.End
: ((ImplicitArrayCreationExpressionSyntax)expression).CloseBracketToken.Span.End)));

context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
s_unnecessaryCodeDescriptor,
additionalUnnecessaryLocations[0],
ReportDiagnostic.Default,
additionalLocations: locations,
additionalUnnecessaryLocations: additionalUnnecessaryLocations));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression;
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.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UseCollectionExpression;

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

Expand Down Expand Up @@ -158,94 +154,11 @@ public static ImmutableArray<CollectionExpressionMatch> TryGetMatches(
StackAllocArrayCreationExpressionSyntax expression,
CancellationToken cancellationToken)
{
// has to either be `stackalloc X[]` or `stackalloc X[const]`.
Copy link
Member Author

Choose a reason for hiding this comment

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

moved to common helper code, since we'll use it for stackalloc and new array creations.

if (expression.Type is not ArrayTypeSyntax { RankSpecifiers: [{ Sizes: [var size] } rankSpecifier] } arrayType)
return default;

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

if (size is OmittedArraySizeExpressionSyntax)
{
// `stackalloc int[]` on its own is illegal. Has to either have a size, or an initializer.
if (expression.Initializer is null)
return default;
}
else
{
// if `stackalloc X[val]`, then it `val` has to be a constant value.
if (semanticModel.GetConstantValue(size, cancellationToken).Value is not int sizeValue)
return default;

if (expression.Initializer != null)
{
// if there is an initializer, then it has to have the right number of elements.
if (sizeValue != expression.Initializer.Expressions.Count)
return default;
}
else
{
// if there is no initializer, we have to be followed by direct statements that initialize the right
// number of elements.

// This needs to be local variable like `ReadOnlySpan<T> x = stackalloc ...
if (expression.WalkUpParentheses().Parent is not EqualsValueClauseSyntax
{
Parent: VariableDeclaratorSyntax
{
Identifier.ValueText: var variableName,
Parent.Parent: LocalDeclarationStatementSyntax localDeclarationStatement
},
})
{
return default;
}

var currentStatement = localDeclarationStatement.GetNextStatement();
for (var currentIndex = 0; currentIndex < sizeValue; currentIndex++)
{
// Each following statement needs to of the form:
//
// x[...] =
if (currentStatement is not ExpressionStatementSyntax
{
Expression: AssignmentExpressionSyntax
{
Left: ElementAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.ValueText: var elementName },
ArgumentList.Arguments: [var elementArgument],
} elementAccess,
}
} expressionStatement)
{
return default;
}

// Ensure we're indexing into the variable created.
if (variableName != elementName)
return default;

// The indexing value has to equal the corresponding location in the result.
if (semanticModel.GetConstantValue(elementArgument.Expression, cancellationToken).Value is not int indexValue ||
indexValue != currentIndex)
{
return default;
}

// this looks like a good statement, add to the right size of the assignment to track as that's what
// we'll want to put in the final collection expression.
matches.Add(new(expressionStatement, UseSpread: false));
currentStatement = currentStatement.GetNextStatement();
}
}
}

if (!UseCollectionExpressionHelpers.CanReplaceWithCollectionExpression(
semanticModel, expression, skipVerificationForReplacedNode: true, cancellationToken))
{
return default;
}

return matches.ToImmutable();
return UseCollectionExpressionHelpers.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.

calls into the shared helper now.

semanticModel,
expression,
static e => e.Type,
static e => e.Initializer,
cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.UseCollectionInitializer;

namespace Microsoft.CodeAnalysis.UseCollectionExpression;
Copy link
Member Author

Choose a reason for hiding this comment

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

a few types were in the wrong namespace. fixed up with downstream consequences.

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

/// <inheritdoc cref="Match{TStatementSyntax}"/>
internal readonly record struct CollectionExpressionMatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression;
namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

using static SyntaxFactory;

Expand Down Expand Up @@ -293,4 +295,106 @@ private static bool ShouldReplaceExistingExpressionEntirely(
// For this we want to remove the 'new' portion, but keep the collection on its own line.
return false;
}

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches<TArrayCreationExpressionSyntax>(
SemanticModel semanticModel,
TArrayCreationExpressionSyntax expression,
Func<TArrayCreationExpressionSyntax, TypeSyntax> getType,
Func<TArrayCreationExpressionSyntax, InitializerExpressionSyntax?> getInitializer,
CancellationToken cancellationToken)
where TArrayCreationExpressionSyntax : ExpressionSyntax
{
Contract.ThrowIfFalse(expression is ArrayCreationExpressionSyntax or StackAllocArrayCreationExpressionSyntax);
Copy link
Member Author

Choose a reason for hiding this comment

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

just a move.


// has to either be `stackalloc X[]` or `stackalloc X[const]`.
if (getType(expression) is not ArrayTypeSyntax { RankSpecifiers: [{ Sizes: [var size] }, ..] })
return default;

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

var initializer = getInitializer(expression);
if (size is OmittedArraySizeExpressionSyntax)
{
// `stackalloc int[]` on its own is illegal. Has to either have a size, or an initializer.
if (initializer is null)
return default;
}
else
{
// if `stackalloc X[val]`, then it `val` has to be a constant value.
if (semanticModel.GetConstantValue(size, cancellationToken).Value is not int sizeValue)
return default;

if (initializer != null)
{
// if there is an initializer, then it has to have the right number of elements.
if (sizeValue != initializer.Expressions.Count)
return default;
}
else
{
// if there is no initializer, we have to be followed by direct statements that initialize the right
// number of elements.

// This needs to be local variable like `ReadOnlySpan<T> x = stackalloc ...
if (expression.WalkUpParentheses().Parent is not EqualsValueClauseSyntax
{
Parent: VariableDeclaratorSyntax
{
Identifier.ValueText: var variableName,
Parent.Parent: LocalDeclarationStatementSyntax localDeclarationStatement
},
})
{
return default;
}

var currentStatement = localDeclarationStatement.GetNextStatement();
for (var currentIndex = 0; currentIndex < sizeValue; currentIndex++)
{
// Each following statement needs to of the form:
//
// x[...] =
if (currentStatement is not ExpressionStatementSyntax
{
Expression: AssignmentExpressionSyntax
{
Left: ElementAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.ValueText: var elementName },
ArgumentList.Arguments: [var elementArgument],
} elementAccess,
}
} expressionStatement)
{
return default;
}

// Ensure we're indexing into the variable created.
if (variableName != elementName)
return default;

// The indexing value has to equal the corresponding location in the result.
if (semanticModel.GetConstantValue(elementArgument.Expression, cancellationToken).Value is not int indexValue ||
indexValue != currentIndex)
{
return default;
}

// this looks like a good statement, add to the right size of the assignment to track as that's what
// we'll want to put in the final collection expression.
matches.Add(new(expressionStatement, UseSpread: false));
currentStatement = currentStatement.GetNextStatement();
}
}
}

if (!CanReplaceWithCollectionExpression(
semanticModel, expression, skipVerificationForReplacedNode: true, cancellationToken))
{
return default;
}

return matches.ToImmutable();
}
}
Loading