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

Detect more fluent cases for converting collection expressions #75267

Merged
merged 26 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ void FadeOutCode(SyntaxNodeAnalysisContext context, AnalysisResult analysisResul
identifier,
initializedSymbol: semanticModel.GetDeclaredSymbol(declarator, cancellationToken));

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

// Now walk all the statement after the local declaration.
using var enumerator = state.GetSubsequentStatements().GetEnumerator();
Expand Down Expand Up @@ -249,6 +249,6 @@ public readonly record struct AnalysisResult(
Location DiagnosticLocation,
LocalDeclarationStatementSyntax LocalDeclarationStatement,
InvocationExpressionSyntax CreationExpression,
ImmutableArray<Match<StatementSyntax>> Matches,
ImmutableArray<Match> Matches,
bool ChangesSemantics);
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,46 +133,57 @@ private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context, INamedTypeSy
{
// Because we're recursing from top to bottom in the expression tree, we build up the matches in reverse. Right
// before returning them, we'll reverse them again to get the proper order.
using var _ = ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>.GetInstance(out var matchesInReverse);
if (!AnalyzeInvocation(text, state, invocation, addMatches ? matchesInReverse : null, out var existingInitializer, cancellationToken))
using var _1 = ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>.GetInstance(out var preMatchesInReverse);
using var _2 = ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>.GetInstance(out var postMatchesInReverse);
Copy link
Member Author

Choose a reason for hiding this comment

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

the general idea here is that when converting to a collection expr, there is often:

  1. values that should be inserted into the collection expr prior to the values in any existing initializer.
  2. the values from an existing initializer. (note: the initializer is also used to help determine how to format/shape the collection, so we want to pass it along, not convert it into the matches-array)
  3. values taht should be inserted into the collection expr after any values in the xisting initializer.

This PR updates everything to follow this model.

if (!AnalyzeInvocation(
text, state, invocation,
addMatches ? preMatchesInReverse : null,
addMatches ? postMatchesInReverse : null,
out var existingInitializer, cancellationToken))
{
return null;
}

if (!CanReplaceWithCollectionExpression(
state.SemanticModel, invocation, expressionType, isSingletonInstance: false, allowSemanticsChange, skipVerificationForReplacedNode: true, cancellationToken, out var changesSemantics))
{
return null;
}

matchesInReverse.ReverseContents();
return new AnalysisResult(existingInitializer, invocation, matchesInReverse.ToImmutable(), changesSemantics);
preMatchesInReverse.ReverseContents();
postMatchesInReverse.ReverseContents();
return new AnalysisResult(existingInitializer, invocation, preMatchesInReverse.ToImmutable(), postMatchesInReverse.ToImmutable(), changesSemantics);
}

private static bool AnalyzeInvocation(
SourceText text,
FluentState state,
InvocationExpressionSyntax invocation,
ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>? matchesInReverse,
ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>? preMatchesInReverse,
ArrayBuilder<CollectionExpressionMatch<ArgumentSyntax>>? postMatchesInReverse,
out InitializerExpressionSyntax? existingInitializer,
CancellationToken cancellationToken)
{
var semanticModel = state.SemanticModel;
var compilation = semanticModel.Compilation;

existingInitializer = null;
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
return false;

// Topmost invocation must be a syntactic match for one of our collection manipulation forms. At the top level
// we don't want to end with a linq method as that would be lazy, and a collection expression will eagerly
// realize the collection.
if (!IsMatch(state, memberAccess, invocation, allowLinq: false, matchesInReverse, out var isAdditionMatch, cancellationToken))
if (!IsMatch(state, memberAccess, invocation, allowLinq: false, postMatchesInReverse, out var isAdditionMatch, cancellationToken))
return false;

// We don't want to offer this feature on top of some builder-type. They will commonly end with something like
// `builder.ToImmutable()`. We want that case to be handled by the 'ForBuilder' analyzer instead.
var expressionType = state.SemanticModel.GetTypeInfo(memberAccess.Expression, cancellationToken).Type;
var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression, cancellationToken).Type;
if (expressionType is null || expressionType.Name.EndsWith("Builder", StringComparison.Ordinal))
return false;

var semanticModel = state.SemanticModel;
var compilation = semanticModel.Compilation;
var ienumerableOfTType = compilation.IEnumerableOfTType();

using var _1 = ArrayBuilder<ExpressionSyntax>.GetInstance(out var stack);
stack.Push(memberAccess.Expression);
Expand All @@ -187,7 +198,7 @@ private static bool AnalyzeInvocation(
// left hand side of the expression. In the inner expressions we can have things like `.Concat/.Append`
// calls as the outer expressions will realize the collection.
if (current is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax currentMemberAccess } currentInvocation &&
IsMatch(state, currentMemberAccess, currentInvocation, allowLinq: true, matchesInReverse, out _, cancellationToken))
IsMatch(state, currentMemberAccess, currentInvocation, allowLinq: true, postMatchesInReverse, out _, cancellationToken))
Copy link
Contributor

Choose a reason for hiding this comment

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

IsMatch

I think this would be easier for me to read if the while loop ended after this if block

Copy link
Member Author

Choose a reason for hiding this comment

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

will make that chnage.

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 got much nicer. thanks.

{
copiedData = true;
stack.Push(currentMemberAccess.Expression);
Expand Down Expand Up @@ -224,21 +235,44 @@ private static bool AnalyzeInvocation(
{
if (objectCreation is not
{
ArgumentList: null or { Arguments.Count: 0 },
Initializer: null or { RawKind: (int)SyntaxKind.CollectionInitializerExpression }
})
{
return false;
}

existingInitializer = objectCreation.Initializer;

if (!IsLegalInitializer(objectCreation.Initializer))
return false;

if (!IsListLike(current))
return false;

existingInitializer = objectCreation.Initializer;
return true;
if (objectCreation.ArgumentList is { Arguments.Count: 1 })
{
// Can take a single argument if that argument is itself a collection.
var argumentType = semanticModel.GetTypeInfo(objectCreation.ArgumentList.Arguments[0].Expression, cancellationToken).Type;
if (argumentType is null)
return false;

if (!Equals(argumentType.OriginalDefinition, ienumerableOfTType) &&
!argumentType.AllInterfaces.Any(i => Equals(i.OriginalDefinition, ienumerableOfTType)))
{
return false;
}

// Add the arguments to the pre-matches. They will execute before the initializer values are added.
AddArgumentsInReverse(preMatchesInReverse, objectCreation.ArgumentList.Arguments, useSpread: true);
Copy link
Member Author

Choose a reason for hiding this comment

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

new logic. if the user wrote new List<...>(someValuesToCopy) we'll create a collection expr starting with [.. someValuesToCopy. Any existing initializer values will be added after this. And any more fluent additions will go after that.

return true;
}
else if (objectCreation.ArgumentList is null or { Arguments.Count: 0 })
{
// Otherwise, we have to have an empty argument list.
return true;
}

return false;
}

// Forms like `ImmutableArray.Create(...)` or `ImmutableArray.CreateRange(...)` are fine base cases.
Expand All @@ -248,7 +282,7 @@ private static bool AnalyzeInvocation(
if (!IsListLike(current))
return false;

AddArgumentsInReverse(matchesInReverse, GetArguments(currentInvocationExpression, unwrapArgument), useSpread: false);
AddArgumentsInReverse(postMatchesInReverse, GetArguments(currentInvocationExpression, unwrapArgument), useSpread: false);
return true;
}

Expand Down Expand Up @@ -277,13 +311,13 @@ private static bool AnalyzeInvocation(

void AddFinalMatch(ExpressionSyntax expression)
{
if (matchesInReverse is null)
if (postMatchesInReverse is null)
return;

// We're only adding one item to the final collection. So we're ending up with `[.. <expr>]`. If this
// originally was wrapped over multiple lines in a fluent fashion, and we're down to just a single wrapped
// line, then unwrap.
if (matchesInReverse.Count == 0 &&
if (postMatchesInReverse.Count == 0 &&
expression is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess } innerInvocation &&
text.Lines.GetLineFromPosition(expression.SpanStart).LineNumber + 1 == text.Lines.GetLineFromPosition(expression.Span.End).LineNumber &&
memberAccess.Expression.GetTrailingTrivia()
Expand All @@ -292,7 +326,7 @@ void AddFinalMatch(ExpressionSyntax expression)
.All(static t => t.IsWhitespaceOrEndOfLine()))
{
// Remove any whitespace around the `.`, making the singly-wrapped fluent expression into a single line.
matchesInReverse.Add(new CollectionExpressionMatch<ArgumentSyntax>(
postMatchesInReverse.Add(new CollectionExpressionMatch<ArgumentSyntax>(
Argument(innerInvocation.WithExpression(
memberAccess.Update(
memberAccess.Expression.WithoutTrailingTrivia(),
Expand All @@ -302,7 +336,7 @@ void AddFinalMatch(ExpressionSyntax expression)
return;
}

matchesInReverse.Add(new CollectionExpressionMatch<ArgumentSyntax>(Argument(expression), UseSpread: true));
postMatchesInReverse.Add(new CollectionExpressionMatch<ArgumentSyntax>(Argument(expression), UseSpread: true));
}

// We only want to offer this feature when the original collection was list-like (as opposed to being something
Expand Down Expand Up @@ -359,6 +393,7 @@ static bool IsLegalInitializer(InitializerExpressionSyntax? initializer)
return false;
}
}

return true;
}
}
Expand Down Expand Up @@ -467,12 +502,15 @@ static bool HasAnySuffix(string name)
/// help determine the best collection expression final syntax.</param>
/// <param name="CreationExpression">The location of the code like <c>builder.ToImmutable()</c> that will actually be
/// replaced with the collection expression</param>
/// <param name="Matches">The arguments being added to the collection that will be converted into elements in the
/// final collection expression.</param>
/// <param name="PreMatches">The arguments being added to the collection that will be converted into elements in
/// the final collection expression *before* the existing initializer elements.</param>
/// <param name="PostMatches">The arguments being added to the collection that will be converted into elements in
/// the final collection expression *after* the existing initializer elements.</param>
public readonly record struct AnalysisResult(
// Location DiagnosticLocation,
InitializerExpressionSyntax? ExistingInitializer,
InvocationExpressionSyntax CreationExpression,
ImmutableArray<CollectionExpressionMatch<ArgumentSyntax>> Matches,
ImmutableArray<CollectionExpressionMatch<ArgumentSyntax>> PreMatches,
ImmutableArray<CollectionExpressionMatch<ArgumentSyntax>> PostMatches,
bool ChangesSemantics);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

/// <inheritdoc cref="Match{TStatementSyntax}"/>
/// <inheritdoc cref="Match"/>
internal readonly record struct CollectionExpressionMatch<TMatchNode>(
Copy link
Member Author

Choose a reason for hiding this comment

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

note: we have a few match structs that are all about the same. I want to unify them in the future.

TMatchNode Node,
bool UseSpread) where TMatchNode : SyntaxNode;
Loading
Loading