Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
63ddd73
Initial plan
Copilot Oct 12, 2025
9895466
Add PreferReadOnlySpanOverSpan analyzer and fixer (CA1876)
Copilot Oct 12, 2025
f6d6050
Add unit tests for CA1876 analyzer and fixer
Copilot Oct 12, 2025
d1eb3fa
Address code review feedback - fix implicit interface detection and i…
Copilot Oct 12, 2025
2048486
Address review feedback: Use ConcurrentDictionary, consolidate condit…
Copilot Oct 13, 2025
7a671a6
Simplify fixer code with pattern matching, add comprehensive test cov…
Copilot Oct 13, 2025
a5c435b
Add ImplicitIndexerReference support for Index/Range operations and c…
Copilot Oct 15, 2025
24bb214
Remove unused ArrayElementReference handler to avoid dead code
Copilot Oct 15, 2025
ed46589
Add comprehensive handling and tests for additional scenarios
Copilot Oct 16, 2025
971feca
Address code review feedback: move comments, combine nested ifs, remo…
Copilot Oct 16, 2025
9ea8a05
Convert first 3 tests to raw string literals as example pattern
Copilot Oct 16, 2025
e707884
Update src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnal…
stephentoub Oct 16, 2025
edab27b
Add blank line before PreferReadOnlySpanOverSpan entries in resx file
Copilot Oct 16, 2025
99e22c7
Update src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnal…
stephentoub Oct 16, 2025
fcfd745
Fix false positive when span indexer element is passed as ref/out arg…
Copilot Oct 17, 2025
b891eb1
Fix false positives for ref variable declarations and ref returns
Copilot Oct 17, 2025
2732125
Fix false positive for Memory/Span parameters passed via Slice() to w…
Copilot Oct 17, 2025
04f9807
Fix false positive for Memory/Span parameters passed via Slice() to w…
Copilot Oct 17, 2025
4fabc06
Enhance analyzer to handle fixed statements, Slice assignments, and r…
Copilot Oct 18, 2025
2fa7a21
Add test for chained Slice operations with indexer increment
Copilot Oct 18, 2025
11c1683
Fix build errors: use AddressOf operation instead of Fixed operation …
Copilot Oct 18, 2025
23b02f5
Fix syntax error: move test method inside class definition
Copilot Oct 18, 2025
53a22fd
Fix AllowUnsafeBlocks: use SolutionTransforms with WithAllowUnsafe
Copilot Oct 18, 2025
7ff966b
Add missing using directive for CSharpCompilationOptions
Copilot Oct 19, 2025
8f762c1
Fix PropertyReference handler to detect chained Slice operations
Copilot Oct 19, 2025
3266540
Fix increment/decrement detection by adding dedicated handler
Copilot Oct 19, 2025
d673d72
Fix invocation handler to detect Slice() assigned through ternary exp…
Copilot Oct 19, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,18 @@ In many situations, logging is disabled or set to a log level that results in an
|CodeFix|True|
---

## [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>'

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.

|Item|Value|
|-|-|
|Category|Performance|
|Enabled|True|
|Severity|Info|
|CodeFix|True|
---

## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3507,6 +3507,26 @@
]
}
},
"CA1876": {
"id": "CA1876",
"shortDescription": "Use 'ReadOnlySpan<T>' or 'ReadOnlyMemory<T>' instead of 'Span<T>' or 'Memory<T>'",
"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.",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876",
"properties": {
"category": "Performance",
"isEnabledByDefault": true,
"typeName": "PreferReadOnlySpanOverSpanAnalyzer",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2000": {
"id": "CA2000",
"shortDescription": "Dispose objects before losing scope",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Rule ID | Category | Severity | Notes
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
CA1876 | Performance | Info | PreferReadOnlySpanOverSpanAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)
Original file line number Diff line number Diff line change
Expand Up @@ -2216,4 +2216,16 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="DoNotUseThreadVolatileReadWriteCodeFixTitle" xml:space="preserve">
<value>Replace obsolete call</value>
</data>
<data name="PreferReadOnlySpanOverSpanTitle" xml:space="preserve">
<value>Use 'ReadOnlySpan&lt;T&gt;' or 'ReadOnlyMemory&lt;T&gt;' instead of 'Span&lt;T&gt;' or 'Memory&lt;T&gt;'</value>
</data>
<data name="PreferReadOnlySpanOverSpanMessage" xml:space="preserve">
<value>Parameter '{0}' can be declared as '{1}' instead of as '{2}'</value>
</data>
<data name="PreferReadOnlySpanOverSpanDescription" xml:space="preserve">
<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>
</data>
<data name="PreferReadOnlySpanOverSpanCodeFixTitle" xml:space="preserve">
<value>Change to '{0}'</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;

namespace Microsoft.NetCore.Analyzers.Performance
{
/// <summary>
/// CA1876: Use ReadOnlySpan&lt;T&gt; or ReadOnlyMemory&lt;T&gt; instead of Span&lt;T&gt; or Memory&lt;T&gt;
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferReadOnlySpanOverSpanFixer))]
[Shared]
public sealed class PreferReadOnlySpanOverSpanFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(PreferReadOnlySpanOverSpanAnalyzer.RuleId);

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);

var diagnostic = context.Diagnostics[0];

if (semanticModel.GetDeclaredSymbol(node, context.CancellationToken) is not IParameterSymbol parameterSymbol ||
GetReadOnlyTypeName(parameterSymbol.Type) is not { } targetTypeName)
{
return;
}

var title = string.Format(MicrosoftNetCoreAnalyzersResources.PreferReadOnlySpanOverSpanCodeFixTitle, targetTypeName);

context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => ChangeParameterTypeAsync(context.Document, node, c),
equivalenceKey: title),
diagnostic);
}

private static string? GetReadOnlyTypeName(ITypeSymbol typeSymbol)
{
return typeSymbol is INamedTypeSymbol namedType && namedType.OriginalDefinition.Name is "Span" or "Memory" ?
$"ReadOnly{typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}" :
null;
}

private static async Task<Document> ChangeParameterTypeAsync(
Document document,
SyntaxNode node,
CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

// Get the parameter symbol to construct the correct type
var parameterSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IParameterSymbol;
if (parameterSymbol?.Type is not INamedTypeSymbol namedType || namedType.TypeArguments.Length != 1)
{
return document;
}

// Get the compilation to find the readonly types
var compilation = semanticModel.Compilation;
var typeName = namedType.OriginalDefinition.Name;
INamedTypeSymbol? readOnlyType = null;

if (typeName == "Span")
{
readOnlyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1);
}
else if (typeName == "Memory")
{
readOnlyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlyMemory1);
}

if (readOnlyType == null)
{
return document;
}

// Construct the generic type with the same type argument
var newType = readOnlyType.Construct(namedType.TypeArguments[0]);
var newTypeNode = generator.TypeExpression(newType);

// Replace the parameter's type
editor.ReplaceNode(node, (currentNode, gen) =>
{
return gen.WithType(currentNode, newTypeNode);
});

return editor.GetChangedDocument();
}
}
}
Loading
Loading