Skip to content

Commit 960a9d7

Browse files
committed
Add PreferReadOnlySpanOverSpan analyzer and fixer (CA1517)
1 parent c401b59 commit 960a9d7

21 files changed

+3063
-1
lines changed

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,18 @@ This rule detects usage of platform-specific intrinsics that can be replaced wit
948948
|CodeFix|True|
949949
---
950950

951+
## [CA1517](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1517): Use 'ReadOnlySpan\<T>' or 'ReadOnlyMemory\<T>' instead of 'Span\<T>' or 'Memory\<T>'
952+
953+
Using 'ReadOnlySpan\<T>' or 'ReadOnlyMemory\<T>' instead of 'Span\<T>' or 'Memory\<T>' for parameters that are not written to can prevent errors, convey intent more clearly, and may improve performance.
954+
955+
|Item|Value|
956+
|-|-|
957+
|Category|Maintainability|
958+
|Enabled|True|
959+
|Severity|Info|
960+
|CodeFix|True|
961+
---
962+
951963
## [CA1700](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700): Do not name enum values 'Reserved'
952964

953965
This rule assumes that an enumeration member that has a name that contains "reserved" is not currently used but is a placeholder to be renamed or removed in a future version. Renaming or removing a member is a breaking change.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,26 @@
21822182
]
21832183
}
21842184
},
2185+
"CA1517": {
2186+
"id": "CA1517",
2187+
"shortDescription": "Use 'ReadOnlySpan<T>' or 'ReadOnlyMemory<T>' instead of 'Span<T>' or 'Memory<T>'",
2188+
"fullDescription": "Using 'ReadOnlySpan<T>' or 'ReadOnlyMemory<T>' instead of 'Span<T>' or 'Memory<T>' for parameters that are not written to can prevent errors, convey intent more clearly, and may improve performance.",
2189+
"defaultLevel": "note",
2190+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1517",
2191+
"properties": {
2192+
"category": "Maintainability",
2193+
"isEnabledByDefault": true,
2194+
"typeName": "PreferReadOnlySpanOverSpanAnalyzer",
2195+
"languages": [
2196+
"C#",
2197+
"Visual Basic"
2198+
],
2199+
"tags": [
2200+
"Telemetry",
2201+
"EnabledRuleInAggressiveMode"
2202+
]
2203+
}
2204+
},
21852205
"CA1700": {
21862206
"id": "CA1700",
21872207
"shortDescription": "Do not name enum values 'Reserved'",

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
Rule ID | Category | Severity | Notes
66
--------|----------|----------|-------
7+
CA1517 | Maintainability | Info | PreferReadOnlySpanOverSpanAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/CA1516)
78
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
89
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
910
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,4 +2261,16 @@ Widening and user defined conversions are not supported with generic types.</val
22612261
<data name="CollapseMultiplePathOperationsCodeFixTitle" xml:space="preserve">
22622262
<value>Collapse into single Path.{0} operation</value>
22632263
</data>
2264+
<data name="PreferReadOnlySpanOverSpanTitle" xml:space="preserve">
2265+
<value>Use 'ReadOnlySpan&lt;T&gt;' or 'ReadOnlyMemory&lt;T&gt;' instead of 'Span&lt;T&gt;' or 'Memory&lt;T&gt;'</value>
2266+
</data>
2267+
<data name="PreferReadOnlySpanOverSpanMessage" xml:space="preserve">
2268+
<value>Parameter '{0}' can be declared as '{1}' instead of as '{2}'</value>
2269+
</data>
2270+
<data name="PreferReadOnlySpanOverSpanDescription" xml:space="preserve">
2271+
<value>Using 'ReadOnlySpan&lt;T&gt;' or 'ReadOnlyMemory&lt;T&gt;' instead of 'Span&lt;T&gt;' or 'Memory&lt;T&gt;' for parameters that are not written to can prevent errors, convey intent more clearly, and may improve performance.</value>
2272+
</data>
2273+
<data name="PreferReadOnlySpanOverSpanCodeFixTitle" xml:space="preserve">
2274+
<value>Change to '{0}'</value>
2275+
</data>
22642276
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System.Collections.Immutable;
4+
using System.Composition;
5+
using Analyzer.Utilities;
6+
using Analyzer.Utilities.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.Editing;
11+
12+
namespace Microsoft.NetCore.Analyzers.Performance
13+
{
14+
/// <summary>
15+
/// CA1517: Use ReadOnlySpan&lt;T&gt; or ReadOnlyMemory&lt;T&gt; instead of Span&lt;T&gt; or Memory&lt;T&gt;
16+
/// </summary>
17+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferReadOnlySpanOverSpanFixer))]
18+
[Shared]
19+
public sealed class PreferReadOnlySpanOverSpanFixer : CodeFixProvider
20+
{
21+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
22+
ImmutableArray.Create(PreferReadOnlySpanOverSpanAnalyzer.RuleId);
23+
24+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
25+
26+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
27+
{
28+
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
29+
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
30+
var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
31+
32+
if (semanticModel.GetDeclaredSymbol(node, context.CancellationToken) is IParameterSymbol parameterSymbol &&
33+
GetReadOnlyTypeName(parameterSymbol.Type) is { } targetTypeName)
34+
{
35+
var title = string.Format(MicrosoftNetCoreAnalyzersResources.PreferReadOnlySpanOverSpanCodeFixTitle, targetTypeName);
36+
37+
context.RegisterCodeFix(
38+
CodeAction.Create(
39+
title: title,
40+
createChangedDocument: c => ChangeParameterTypeAsync(context.Document, node, c),
41+
equivalenceKey: title),
42+
context.Diagnostics[0]);
43+
}
44+
}
45+
46+
private static string? GetReadOnlyTypeName(ITypeSymbol typeSymbol) =>
47+
typeSymbol is INamedTypeSymbol namedType && namedType.OriginalDefinition.Name is "Span" or "Memory" ?
48+
$"ReadOnly{typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}" :
49+
null;
50+
51+
private static async Task<Document> ChangeParameterTypeAsync(
52+
Document document,
53+
SyntaxNode node,
54+
CancellationToken cancellationToken)
55+
{
56+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
57+
var generator = editor.Generator;
58+
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
59+
60+
// Get the parameter symbol to construct the correct type
61+
var parameterSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IParameterSymbol;
62+
if (parameterSymbol?.Type is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 1)
63+
{
64+
// Get the compilation to find the readonly types
65+
var compilation = semanticModel.Compilation;
66+
var typeName = namedType.OriginalDefinition.Name;
67+
68+
INamedTypeSymbol? readOnlyType =
69+
typeName is "Span" ? compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1) :
70+
typeName is "Memory" ? compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlyMemory1) :
71+
null;
72+
73+
if (readOnlyType is not null)
74+
{
75+
// Construct the generic type with the same type argument
76+
var newTypeNode = generator.TypeExpression(
77+
readOnlyType.Construct(namedType.TypeArguments[0]));
78+
79+
// Replace the parameter's type
80+
editor.ReplaceNode(node, (currentNode, gen) => gen.WithType(currentNode, newTypeNode));
81+
82+
return editor.GetChangedDocument();
83+
}
84+
}
85+
86+
return document;
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)