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 recommend updating stackalloc expressions to use collection expressions. #69414

Merged
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6941fb2
Add analyzer
CyrusNajmabadi Aug 4, 2023
8d5b777
in progress
CyrusNajmabadi Aug 4, 2023
98e7e5a
in progress
CyrusNajmabadi Aug 4, 2023
322c753
Merge branch 'rewriteCode' into useCollectionExpressionStackAlloc
CyrusNajmabadi Aug 5, 2023
2f91311
in progress
CyrusNajmabadi Aug 5, 2023
5dfedd1
Merge branch 'rewriteCode' into useCollectionExpressionStackAlloc
CyrusNajmabadi Aug 5, 2023
32d1b9b
fix project
CyrusNajmabadi Aug 5, 2023
c67b59d
Extract helper
CyrusNajmabadi Aug 5, 2023
479ed16
Simplify
CyrusNajmabadi Aug 5, 2023
eb9e5a7
in progress
CyrusNajmabadi Aug 5, 2023
58c3478
in progress
CyrusNajmabadi Aug 5, 2023
27c3baa
in progress
CyrusNajmabadi Aug 5, 2023
6ab18d9
in progress
CyrusNajmabadi Aug 5, 2023
ba688eb
Should be working
CyrusNajmabadi Aug 5, 2023
4bf1d35
Fixes
CyrusNajmabadi Aug 5, 2023
b3f2a57
Working tests
CyrusNajmabadi Aug 5, 2023
23fb418
Tests
CyrusNajmabadi Aug 5, 2023
69a9f96
Fix
CyrusNajmabadi Aug 5, 2023
4691cf6
Add tests
CyrusNajmabadi Aug 5, 2023
c6b39f2
tests
CyrusNajmabadi Aug 5, 2023
23f415e
Merge remote-tracking branch 'upstream/main' into useCollectionExpres…
CyrusNajmabadi Aug 5, 2023
0baa1c1
fixes
CyrusNajmabadi Aug 5, 2023
194e831
in progress
CyrusNajmabadi Aug 5, 2023
8e79de1
in progress
CyrusNajmabadi Aug 5, 2023
3087615
Fixes
CyrusNajmabadi Aug 5, 2023
c2028c2
Tests
CyrusNajmabadi Aug 5, 2023
3a151f3
Add comment
CyrusNajmabadi Aug 5, 2023
2fc84e2
Simplify
CyrusNajmabadi Aug 5, 2023
0c1d16e
Update test
CyrusNajmabadi Aug 6, 2023
a275908
Update test
CyrusNajmabadi Aug 6, 2023
67869eb
Merge branch 'main' into useCollectionExpressionStackAlloc
CyrusNajmabadi Aug 7, 2023
60156bd
DeletE
CyrusNajmabadi Aug 7, 2023
4b98dc7
Delete
CyrusNajmabadi Aug 7, 2023
19f78ce
Fix
CyrusNajmabadi Aug 7, 2023
920e2c2
Fix
CyrusNajmabadi Aug 7, 2023
dd8079a
Update missing docs
CyrusNajmabadi Aug 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/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\CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionExpression\UseCollectionExpressionHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionInitializer\CSharpUseCollectionInitializerAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCompoundAssignment\CSharpUseCompoundAssignmentDiagnosticAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,23 @@ public CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer()
}

protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterCompilationStartAction(OnCompilationStart);
Copy link
Member Author

Choose a reason for hiding this comment

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

review with whitespace off.

this is just a method inline.


private void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (!context.Compilation.LanguageVersion().SupportsCollectionExpressions())
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.RegisterCompilationStartAction(context =>
{
context.RegisterSyntaxNodeAction(
context => AnalyzeArrayInitializer(context),
SyntaxKind.ArrayInitializerExpression);
if (!context.Compilation.LanguageVersion().SupportsCollectionExpressions())
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 => AnalyzeArrayInitializer(context),
SyntaxKind.ArrayInitializerExpression);
});
});
}

private static void AnalyzeArrayInitializer(SyntaxNodeAnalysisContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,23 @@ public CSharpUseCollectionExpressionForEmptyDiagnosticAnalyzer()
}

protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterCompilationStartAction(OnCompilationStart);

private void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (!context.Compilation.LanguageVersion().SupportsCollectionExpressions())
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.RegisterCompilationStartAction(context =>
{
context.RegisterSyntaxNodeAction(
context => AnalyzeMemberAccess(context),
SyntaxKind.SimpleMemberAccessExpression);
if (!context.Compilation.LanguageVersion().SupportsCollectionExpressions())
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 => AnalyzeMemberAccess(context),
SyntaxKind.SimpleMemberAccessExpression);
});
});
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 is just a method inline.

}

private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// 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.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;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed partial class CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer
: AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptorWithId(
IDEDiagnosticIds.UseCollectionExpressionForStackAllocDiagnosticId,
EnforceOnBuildValues.UseCollectionExpressionForStackAlloc,
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.UseCollectionExpressionForStackAllocDiagnosticId,
EnforceOnBuildValues.UseCollectionExpressionForStackAlloc,
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 CSharpUseCollectionExpressionForStackAllocDiagnosticAnalyzer()
: 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;

// Runtime needs to support inline arrays in order for this to be ok. Otherwise compiler has no good way to
// emit these collection expressions.
//
// TODO: add this check once the SDK test system supports referencing .Net 8.
//
// if (!compilation.SupportsRuntimeCapability(RuntimeCapability.InlineArrayTypes))
// 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 => AnalyzeExplicitStackAllocExpression(context),
SyntaxKind.StackAllocArrayCreationExpression);
context.RegisterSyntaxNodeAction(
context => AnalyzeImplicitStackAllocExpression(context),
SyntaxKind.ImplicitStackAllocArrayCreationExpression);
});
});

private static void AnalyzeImplicitStackAllocExpression(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var syntaxTree = semanticModel.SyntaxTree;
var expression = (ImplicitStackAllocArrayCreationExpressionSyntax)context.Node;
var cancellationToken = context.CancellationToken;

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

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

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.CloseBracketToken.Span.End)));

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

private static void AnalyzeExplicitStackAllocExpression(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var syntaxTree = semanticModel.SyntaxTree;
var expression = (StackAllocArrayCreationExpressionSyntax)context.Node;
var cancellationToken = context.CancellationToken;

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

var matches = TryGetMatches(semanticModel, expression, cancellationToken);
if (matches.IsDefault)
return;

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.Type.Span.End)));

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

public static ImmutableArray<CollectionExpressionMatch> TryGetMatches(
SemanticModel semanticModel,
StackAllocArrayCreationExpressionSyntax expression,
CancellationToken cancellationToken)
{
// has to either be `stackalloc X[]` or `stackalloc X[const]`.
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public static bool CanReplaceWithCollectionExpression(
bool skipVerificationForReplacedNode,
CancellationToken cancellationToken)
{
var compilation = semanticModel.Compilation;

var topMostExpression = expression.WalkUpParentheses();
if (topMostExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
return false;
Expand All @@ -47,8 +49,21 @@ public static bool CanReplaceWithCollectionExpression(
if (originalTypeInfo.ConvertedType is null or IErrorTypeSymbol)
return false;

// Conservatively, avoid making this change if the original expression was itself converted. Consider, for
// example, `IEnumerable<string> x = new List<string>()`. If we change that to `[]` we will still compile,
// but it's possible we'll end up with different types at runtime that may cause problems.
//
// Note: we can relax this on a case by case basis if we feel like it's acceptable.
if (originalTypeInfo.Type != null && !originalTypeInfo.Type.Equals(originalTypeInfo.ConvertedType))
return false;
{
var isOk =
originalTypeInfo.Type.Name == nameof(Span<int>) &&
originalTypeInfo.ConvertedType.Name == nameof(ReadOnlySpan<int>) &&
originalTypeInfo.Type.OriginalDefinition.Equals(compilation.SpanOfTType()) &&
originalTypeInfo.ConvertedType.OriginalDefinition.Equals(compilation.ReadOnlySpanOfTType());
if (!isOk)
return false;
}
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 code is saying: it's ok if using a collection expression would produce Span<T> if the original code was ReadONlySpan<T>. That's because the former is trivially implicitly convertible to the latter, despite being different types.


// Looks good as something to replace. Now check the semantics of making the replacement to see if there would
// any issues. To keep things simple, all we do is replace the existing expression with the `[]` literal. This
Expand Down Expand Up @@ -207,7 +222,12 @@ public static CollectionExpressionSyntax ReplaceWithCollectionExpression(
CollectionExpressionSyntax newCollectionExpression,
bool newCollectionIsSingleLine)
{
Contract.ThrowIfFalse(originalInitializer.Parent is ArrayCreationExpressionSyntax or ImplicitArrayCreationExpressionSyntax or BaseObjectCreationExpressionSyntax);
Contract.ThrowIfFalse(originalInitializer.Parent
is ArrayCreationExpressionSyntax
or ImplicitArrayCreationExpressionSyntax
or StackAllocArrayCreationExpressionSyntax
or ImplicitStackAllocArrayCreationExpressionSyntax
or BaseObjectCreationExpressionSyntax);

var initializerParent = originalInitializer.GetRequiredParent();

Expand Down
Loading