Skip to content

Commit ee0cc2f

Browse files
Copilotstephentoub
authored andcommitted
Add PreferReadOnlySpanOverSpan analyzer and fixer (CA1876)
1 parent 65384c6 commit ee0cc2f

21 files changed

+2326
-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
@@ -1908,6 +1908,18 @@ In many situations, logging is disabled or set to a log level that results in an
19081908
|CodeFix|True|
19091909
---
19101910

1911+
## [CA1876](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876): Use 'ReadOnlySpan\<T>' or 'ReadOnlyMemory\<T>' instead of 'Span\<T>' or 'Memory\<T>'
1912+
1913+
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.
1914+
1915+
|Item|Value|
1916+
|-|-|
1917+
|Category|Performance|
1918+
|Enabled|True|
1919+
|Severity|Info|
1920+
|CodeFix|True|
1921+
---
1922+
19111923
## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope
19121924

19131925
If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,6 +3507,26 @@
35073507
]
35083508
}
35093509
},
3510+
"CA1876": {
3511+
"id": "CA1876",
3512+
"shortDescription": "Use 'ReadOnlySpan<T>' or 'ReadOnlyMemory<T>' instead of 'Span<T>' or 'Memory<T>'",
3513+
"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.",
3514+
"defaultLevel": "note",
3515+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876",
3516+
"properties": {
3517+
"category": "Performance",
3518+
"isEnabledByDefault": true,
3519+
"typeName": "PreferReadOnlySpanOverSpanAnalyzer",
3520+
"languages": [
3521+
"C#",
3522+
"Visual Basic"
3523+
],
3524+
"tags": [
3525+
"Telemetry",
3526+
"EnabledRuleInAggressiveMode"
3527+
]
3528+
}
3529+
},
35103530
"CA2000": {
35113531
"id": "CA2000",
35123532
"shortDescription": "Dispose objects before losing scope",

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
@@ -7,6 +7,7 @@ Rule ID | Category | Severity | Notes
77
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
88
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
99
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
10+
CA1876 | Performance | Info | PreferReadOnlySpanOverSpanAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
1011
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
1112
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
1213
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)

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
@@ -2228,4 +2228,16 @@ Widening and user defined conversions are not supported with generic types.</val
22282228
<data name="DoNotUseThreadVolatileReadWriteCodeFixTitle" xml:space="preserve">
22292229
<value>Replace obsolete call</value>
22302230
</data>
2231+
<data name="PreferReadOnlySpanOverSpanTitle" xml:space="preserve">
2232+
<value>Use 'ReadOnlySpan&lt;T&gt;' or 'ReadOnlyMemory&lt;T&gt;' instead of 'Span&lt;T&gt;' or 'Memory&lt;T&gt;'</value>
2233+
</data>
2234+
<data name="PreferReadOnlySpanOverSpanMessage" xml:space="preserve">
2235+
<value>Parameter '{0}' can be declared as '{1}' instead of as '{2}'</value>
2236+
</data>
2237+
<data name="PreferReadOnlySpanOverSpanDescription" xml:space="preserve">
2238+
<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>
2239+
</data>
2240+
<data name="PreferReadOnlySpanOverSpanCodeFixTitle" xml:space="preserve">
2241+
<value>Change to '{0}'</value>
2242+
</data>
22312243
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 System.Threading;
6+
using System.Threading.Tasks;
7+
using Analyzer.Utilities;
8+
using Analyzer.Utilities.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.Editing;
13+
14+
namespace Microsoft.NetCore.Analyzers.Performance
15+
{
16+
/// <summary>
17+
/// CA1876: Use ReadOnlySpan&lt;T&gt; or ReadOnlyMemory&lt;T&gt; instead of Span&lt;T&gt; or Memory&lt;T&gt;
18+
/// </summary>
19+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferReadOnlySpanOverSpanFixer))]
20+
[Shared]
21+
public sealed class PreferReadOnlySpanOverSpanFixer : CodeFixProvider
22+
{
23+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
24+
ImmutableArray.Create(PreferReadOnlySpanOverSpanAnalyzer.RuleId);
25+
26+
public sealed override FixAllProvider GetFixAllProvider()
27+
{
28+
return WellKnownFixAllProviders.BatchFixer;
29+
}
30+
31+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34+
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
35+
var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
36+
37+
var diagnostic = context.Diagnostics[0];
38+
39+
if (semanticModel.GetDeclaredSymbol(node, context.CancellationToken) is not IParameterSymbol parameterSymbol ||
40+
GetReadOnlyTypeName(parameterSymbol.Type) is not { } targetTypeName)
41+
{
42+
return;
43+
}
44+
45+
var title = string.Format(MicrosoftNetCoreAnalyzersResources.PreferReadOnlySpanOverSpanCodeFixTitle, targetTypeName);
46+
47+
context.RegisterCodeFix(
48+
CodeAction.Create(
49+
title: title,
50+
createChangedDocument: c => ChangeParameterTypeAsync(context.Document, node, c),
51+
equivalenceKey: title),
52+
diagnostic);
53+
}
54+
55+
private static string? GetReadOnlyTypeName(ITypeSymbol typeSymbol)
56+
{
57+
return typeSymbol is INamedTypeSymbol namedType && namedType.OriginalDefinition.Name is "Span" or "Memory" ?
58+
$"ReadOnly{typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}" :
59+
null;
60+
}
61+
62+
private static async Task<Document> ChangeParameterTypeAsync(
63+
Document document,
64+
SyntaxNode node,
65+
CancellationToken cancellationToken)
66+
{
67+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
68+
var generator = editor.Generator;
69+
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
70+
71+
// Get the parameter symbol to construct the correct type
72+
var parameterSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IParameterSymbol;
73+
if (parameterSymbol?.Type is not INamedTypeSymbol namedType || namedType.TypeArguments.Length != 1)
74+
{
75+
return document;
76+
}
77+
78+
// Get the compilation to find the readonly types
79+
var compilation = semanticModel.Compilation;
80+
var typeName = namedType.OriginalDefinition.Name;
81+
INamedTypeSymbol? readOnlyType = null;
82+
83+
if (typeName == "Span")
84+
{
85+
readOnlyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1);
86+
}
87+
else if (typeName == "Memory")
88+
{
89+
readOnlyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlyMemory1);
90+
}
91+
92+
if (readOnlyType == null)
93+
{
94+
return document;
95+
}
96+
97+
// Construct the generic type with the same type argument
98+
var newType = readOnlyType.Construct(namedType.TypeArguments[0]);
99+
var newTypeNode = generator.TypeExpression(newType);
100+
101+
// Replace the parameter's type
102+
editor.ReplaceNode(node, (currentNode, gen) =>
103+
{
104+
return gen.WithType(currentNode, newTypeNode);
105+
});
106+
107+
return editor.GetChangedDocument();
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)