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 fluent collection building to a collection expression #69580

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d2f5718
in progress
CyrusNajmabadi Aug 17, 2023
ca406b0
progress
CyrusNajmabadi Aug 17, 2023
fd53717
Add helpers
CyrusNajmabadi Aug 17, 2023
e88364c
Move helper
CyrusNajmabadi Aug 17, 2023
2925c68
Move helper
CyrusNajmabadi Aug 17, 2023
299fa8a
Move helper
CyrusNajmabadi Aug 17, 2023
412237f
Move helper
CyrusNajmabadi Aug 17, 2023
2db9289
Flesh out
CyrusNajmabadi Aug 17, 2023
c239c5c
Fix check
CyrusNajmabadi Aug 17, 2023
650ec70
initial fixer
CyrusNajmabadi Aug 17, 2023
ef593d5
Share more
CyrusNajmabadi Aug 17, 2023
787cf94
in progress
CyrusNajmabadi Aug 17, 2023
8de08c4
Merge
CyrusNajmabadi Aug 17, 2023
10817e0
Merge branch 'main' into useCollectionExpressionForFluent
CyrusNajmabadi Aug 18, 2023
9c7efe3
In progress
CyrusNajmabadi Aug 18, 2023
1caeb43
Use helpers
CyrusNajmabadi Aug 18, 2023
4663a33
Store more info
CyrusNajmabadi Aug 18, 2023
1f326c3
in progress
CyrusNajmabadi Aug 18, 2023
f8fad50
Move to arguments
CyrusNajmabadi Aug 18, 2023
f8ccc00
Add fixer
CyrusNajmabadi Aug 18, 2023
dd93254
Move + tests
CyrusNajmabadi Aug 18, 2023
d81e539
Add comma
CyrusNajmabadi Aug 18, 2023
995ffbf
Add tests
CyrusNajmabadi Aug 18, 2023
fab9144
Add tests
CyrusNajmabadi Aug 18, 2023
31461d2
Add tests
CyrusNajmabadi Aug 18, 2023
a16ebf9
Tests
CyrusNajmabadi Aug 18, 2023
1b30473
Almost
CyrusNajmabadi Aug 18, 2023
2ce1641
Improve placement
CyrusNajmabadi Aug 18, 2023
6a5676e
Better formatting
CyrusNajmabadi Aug 18, 2023
b26a3b6
Add tests
CyrusNajmabadi Aug 18, 2023
1b9c9af
More tests
CyrusNajmabadi Aug 18, 2023
5f6cc10
More tests
CyrusNajmabadi Aug 18, 2023
2c2feed
Merge branch 'useCollectionExpressionExtracts' into useCollectionExpr…
CyrusNajmabadi Aug 18, 2023
44a3b9f
Add tests
CyrusNajmabadi Aug 18, 2023
f6af9f9
Fix formatting
CyrusNajmabadi Aug 18, 2023
cb211c2
Add Append support
CyrusNajmabadi Aug 18, 2023
1ed0304
Fix
CyrusNajmabadi Aug 18, 2023
a52bbb3
Merge remote-tracking branch 'upstream/main' into useCollectionExpres…
CyrusNajmabadi Aug 18, 2023
f6669fe
Apply suggestions from code review
CyrusNajmabadi Aug 18, 2023
d1078a3
Add docs
CyrusNajmabadi Aug 18, 2023
089e09f
Add docs
CyrusNajmabadi Aug 18, 2023
83fdc23
Merge branch 'useCollectionExpressionForFluent' of https://github.com…
CyrusNajmabadi Aug 18, 2023
6d72b64
Add tests
CyrusNajmabadi Aug 18, 2023
5fdd2ac
tests
CyrusNajmabadi Aug 19, 2023
555bbcc
Merge remote-tracking branch 'upstream/main' into useCollectionExpres…
CyrusNajmabadi Aug 19, 2023
c985341
Update src/Features/RulesMissingDocumentation.md
CyrusNajmabadi Aug 19, 2023
4c3198a
Filter out builders
CyrusNajmabadi Aug 19, 2023
3463acb
Limit cases
CyrusNajmabadi Aug 19, 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\AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CollectionExpressionMatch.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForFluentDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.cs" />
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ public static bool CanReplaceWithCollectionExpression(
// Ensure that we have a collection conversion with the replacement. If not, this wasn't a legal replacement
// (for example, we're trying to replace an expression that is converted to something that isn't even a
// collection type).
//
// Note: an identity conversion is always legal without needing any more checks.
var conversion = speculationAnalyzer.SpeculativeSemanticModel.GetConversion(speculationAnalyzer.ReplacedExpression, cancellationToken);
if (conversion.IsIdentity)
Copy link
Member Author

Choose a reason for hiding this comment

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

@cston this appears to be an error. Once this merged, i'll open a bug on this.

return true;

if (!conversion.IsCollectionExpression)
return false;

Expand Down Expand Up @@ -177,6 +182,7 @@ private static bool IsInTargetTypedLocation(SemanticModel semanticModel, Express
// Similar rules for switches.
SwitchExpressionArmSyntax switchExpressionArm => IsInTargetTypedSwitchExpressionArm(switchExpressionArm),
InitializerExpressionSyntax initializerExpression => IsInTargetTypedInitializerExpression(initializerExpression, topExpression),
CollectionElementSyntax collectionElement => IsInTargetTypedCollectionElement(collectionElement),
AssignmentExpressionSyntax assignmentExpression => IsInTargetTypedAssignmentExpression(assignmentExpression, topExpression),
BinaryExpressionSyntax binaryExpression => IsInTargetTypedBinaryExpression(binaryExpression, topExpression),
ArgumentSyntax or AttributeArgumentSyntax => true,
Expand Down Expand Up @@ -220,6 +226,17 @@ bool IsInTargetTypedSwitchExpressionArm(SwitchExpressionArmSyntax switchExpressi
return IsInTargetTypedLocation(semanticModel, switchExpression, cancellationToken);
}

bool IsInTargetTypedCollectionElement(CollectionElementSyntax collectionElement)
{
// We do not currently target type spread expressions in a collection expression.
if (collectionElement is not ExpressionElementSyntax)
return false;

// The element it target typed if the parent collection is itself target typed.
var collectionExpression = (CollectionExpressionSyntax)collectionElement.GetRequiredParent();
return IsInTargetTypedLocation(semanticModel, collectionExpression, cancellationToken);
}

bool IsInTargetTypedInitializerExpression(InitializerExpressionSyntax initializerExpression, ExpressionSyntax expression)
{
// new X[] { [1, 2, 3] }. Elements are target typed by array type.
Expand Down Expand Up @@ -471,8 +488,10 @@ public static bool IsCollectionFactoryCreate(
{
const string CreateName = nameof(ImmutableArray.Create);
const string CreateRangeName = nameof(ImmutableArray.CreateRange);

unwrapArgument = false;
memberAccess = null;

// Looking for `XXX.Create(...)`
if (invocationExpression.Expression is not MemberAccessExpressionSyntax
{
Expand All @@ -482,14 +501,18 @@ public static bool IsCollectionFactoryCreate(
{
return false;
}

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

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

var compilation = semanticModel.Compilation;

// 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.
Expand Down Expand Up @@ -599,13 +622,13 @@ bool IsCompatibleSignatureAndArguments(
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
}])
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()))
Expand Down Expand Up @@ -734,12 +757,15 @@ static bool IsPossiblyDottedName(ExpressionSyntax name)
public static SeparatedSyntaxList<ArgumentSyntax> GetArguments(InvocationExpressionSyntax invocationExpression, bool unwrapArgument)
{
var arguments = invocationExpression.ArgumentList.Arguments;

// If we're not unwrapping a singular argument expression, then just pass back all the explicit argument
// expressions the user wrote out.
if (!unwrapArgument)
return arguments;

Contract.ThrowIfTrue(arguments.Count != 1);
var expression = arguments.Single().Expression;

var initializer = expression switch
{
ImplicitArrayCreationExpressionSyntax implicitArray => implicitArray.Initializer,
Expand All @@ -750,6 +776,7 @@ public static SeparatedSyntaxList<ArgumentSyntax> GetArguments(InvocationExpress
ObjectCreationExpressionSyntax objectCreation => objectCreation.Initializer,
_ => throw ExceptionUtilities.Unreachable(),
};

return initializer is null
? default
: SeparatedList<ArgumentSyntax>(initializer.Expressions.GetWithSeparators().Select(
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 @@ -78,6 +78,7 @@
<Compile Include="$(MSBuildThisFileDirectory)UpgradeProject\CSharpUpgradeProjectCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpCollectionExpressionRewriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForBuilderCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForFluentCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForCreateCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForEmptyCodeFixProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\CSharpUseCollectionExpressionForArrayCodeFixProvider.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ForEachCast;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.PooledObjects;
Expand Down Expand Up @@ -433,22 +432,7 @@ ExpressionSyntax IndentExpression(
if (preferredIndentation is null)
return expression.WithoutLeadingTrivia();

// we're starting with something either like:
//
// collection.Add(some_expr +
// cont);
//
// or
//
// collection.Add(
// some_expr +
// cont);
//
// In the first, we want to consider the `some_expr + cont` to actually start where `collection` starts so
// that we can accurately determine where the preferred indentation should move all of it.

var startLine = document.Text.Lines.GetLineFromPosition(expression.SpanStart);

var startLine = document.Text.Lines.GetLineFromPosition(GetAnchorNode(expression).SpanStart);
var firstTokenOnLineIndentationString = GetIndentationStringForToken(document.Root.FindToken(startLine.Start));

var expressionFirstToken = expression.GetFirstToken();
Expand All @@ -465,6 +449,38 @@ ExpressionSyntax IndentExpression(

// Now, once we've indented the expression, attempt to move comments on its containing statement to it.
return TransferParentStatementComments(parentStatement, updatedExpression, preferredIndentation);

SyntaxNode GetAnchorNode(SyntaxNode node)
{
// we're starting with something either like:
//
// collection.Add(some_expr +
// cont);
//
// or
//
// collection.Add(
// some_expr +
// cont);
//
// In the first, we want to consider the `some_expr + cont` to actually start where `collection` starts so
// that we can accurately determine where the preferred indentation should move all of it.

// If the expression is parented by a statement or member-decl (like a field/prop), use that container
// to determine the indentation point. Otherwise, default to the indentation of the line the expression
// is on.
var firstToken = node.GetFirstToken();
if (document.Text.AreOnSameLine(firstToken.GetPreviousToken(), firstToken))
{
for (var current = node; current != null; current = current.Parent)
{
if (current is StatementSyntax or MemberDeclarationSyntax)
return current;
}
}

return node;
}
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 improved indentation tracking in a few more situations.

}

SyntaxToken IndentToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

using static CSharpUseCollectionExpressionForBuilderDiagnosticAnalyzer;
using static CSharpCollectionExpressionRewriter;
using static SyntaxFactory;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionExpressionForBuilder), Shared]
Expand Down Expand Up @@ -65,7 +66,7 @@ protected override async Task FixAsync(
analysisResult = TrackAnalysisResult(root, analysisResult);

// Get the new collection expression.
var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync(
var collectionExpression = await CreateCollectionExpressionAsync(
newDocument,
fallbackOptions,
dummyObjectCreation,
Expand Down Expand Up @@ -108,10 +109,11 @@ static async Task<Document> CreateTrackedDocumentAsync(
using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var nodesToTrack);

var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

nodesToTrack.Add(analysisResult.LocalDeclarationStatement);
nodesToTrack.Add(analysisResult.CreationExpression);
foreach (var (statement, _) in analysisResult.Matches)
nodesToTrack.Add(statement);
nodesToTrack.Add(analysisResult.CreationExpression);

var newRoot = root.TrackNodes(nodesToTrack);
var creationExpression = newRoot.GetCurrentNode(analysisResult.CreationExpression)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;

using static CSharpCollectionExpressionRewriter;
using static SyntaxFactory;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionExpressionForCreate), Shared]
Expand Down Expand Up @@ -45,7 +46,7 @@ protected override async Task FixAsync(
var unwrapArgument = properties.ContainsKey(CSharpUseCollectionExpressionForCreateDiagnosticAnalyzer.UnwrapArgument);

// We want to replace `XXX.Create(...)` with the new collection expression. To do this, we go through the
// following steps. First, we replace `XXX.Create(a, b, c)` with `new(a, b, c)` (an dummy object creation
// following steps. First, we replace `XXX.Create(a, b, c)` with `new(a, b, c)` (a dummy object creation
// expression). We then call into our helper which replaces expressions with collection expressions. The reason
// for the dummy object creation expression is that it serves as an actual node the rewriting code can attach an
// initializer to, by which it can figure out appropriate wrapping and indentation for the collection expression
Expand All @@ -67,7 +68,7 @@ protected override async Task FixAsync(
var expressions = dummyObjectCreation.ArgumentList.Arguments.Select(a => a.Expression);
var matches = expressions.SelectAsArray(static e => new CollectionExpressionMatch<ExpressionSyntax>(e, UseSpread: false));

var collectionExpression = await CSharpCollectionExpressionRewriter.CreateCollectionExpressionAsync(
var collectionExpression = await CreateCollectionExpressionAsync(
newSemanticDocument.Document,
fallbackOptions,
dummyObjectCreation,
Expand Down
Loading