-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Add analyzer/fixer to recommend updating stackalloc expressions to use collection expressions. #69414
Changes from 35 commits
6941fb2
8d5b777
98e7e5a
322c753
2f91311
5dfedd1
32d1b9b
c67b59d
479ed16
eb9e5a7
58c3478
27c3baa
6ab18d9
ba688eb
4bf1d35
b3f2a57
23fb418
69a9f96
4691cf6
c6b39f2
23f415e
0baa1c1
194e831
8e79de1
3087615
c2028c2
3a151f3
2fc84e2
0c1d16e
a275908
67869eb
60156bd
4b98dc7
19f78ce
920e2c2
dd8079a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is just a method inline. |
||
} | ||
|
||
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) | ||
{ | ||
|
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
// 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 | ||
|
@@ -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(); | ||
|
||
|
There was a problem hiding this comment.
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.