diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs
new file mode 100644
index 0000000000..c461c904d9
--- /dev/null
+++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferAsSpanOverSubstring.Fixer.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.NetCore.Analyzers.Runtime;
+
+namespace Microsoft.NetCore.CSharp.Analyzers.Runtime
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp)]
+ public sealed class CSharpPreferAsSpanOverSubstringFixer : PreferAsSpanOverSubstringFixer
+ {
+ private protected override void ReplaceNonConditionalInvocationMethodName(SyntaxEditor editor, SyntaxNode memberInvocation, string newName)
+ {
+ var cast = (InvocationExpressionSyntax)memberInvocation;
+ var memberAccessSyntax = (MemberAccessExpressionSyntax)cast.Expression;
+ var newNameSyntax = SyntaxFactory.IdentifierName(newName);
+ editor.ReplaceNode(memberAccessSyntax.Name, newNameSyntax);
+ }
+
+ private protected override void ReplaceNamedArgumentName(SyntaxEditor editor, SyntaxNode invocation, string oldArgumentName, string newArgumentName)
+ {
+ var cast = (InvocationExpressionSyntax)invocation;
+ var oldNameSyntax = cast.ArgumentList.Arguments
+ .FirstOrDefault(x => x.NameColon is not null && x.NameColon.Name.Identifier.ValueText == oldArgumentName)?.NameColon.Name;
+ if (oldNameSyntax is null)
+ return;
+ var newNameSyntax = SyntaxFactory.IdentifierName(newArgumentName);
+ editor.ReplaceNode(oldNameSyntax, newNameSyntax);
+ }
+ }
+}
diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
index a82b9f618e..52661da90e 100644
--- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
+++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
@@ -12,6 +12,7 @@ CA1842 | Performance | Info | DoNotUseWhenAllOrWaitAllWithSingleArgument, [Docum
CA1843 | Performance | Info | DoNotUseWhenAllOrWaitAllWithSingleArgument, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843)
CA1844 | Performance | Info | ProvideStreamMemoryBasedAsyncOverrides, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844)
CA1845 | Performance | Info | UseSpanBasedStringConcat, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845)
+CA1846 | Performance | Info | PreferAsSpanOverSubstring, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846)
CA2250 | Usage | Info | UseCancellationTokenThrowIfCancellationRequested, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250)
### Removed Rules
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
index 723dba5b18..01e3b55deb 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
@@ -1534,6 +1534,18 @@
and all other platformsThis call site is reachable on: 'windows' 10.0.2000 and later, and all other platforms
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+
+ Replace 'Substring' with 'AsSpan'
+
'ThrowIfCancellationRequested' automatically checks whether the token has been canceled, and throws an 'OperationCanceledException' if it has.
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs
new file mode 100644
index 0000000000..8d41a9ee4e
--- /dev/null
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.Fixer.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Analyzer.Utilities.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.Operations;
+using RequiredSymbols = Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring.RequiredSymbols;
+
+namespace Microsoft.NetCore.Analyzers.Runtime
+{
+ public abstract class PreferAsSpanOverSubstringFixer : CodeFixProvider
+ {
+ private const string SubstringStartIndexArgumentName = "startIndex";
+ private const string AsSpanStartArgumentName = "start";
+
+ private protected abstract void ReplaceNonConditionalInvocationMethodName(SyntaxEditor editor, SyntaxNode memberInvocation, string newName);
+
+ private protected abstract void ReplaceNamedArgumentName(SyntaxEditor editor, SyntaxNode invocation, string oldArgumentName, string newArgumentName);
+
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PreferAsSpanOverSubstring.RuleId);
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var document = context.Document;
+ var token = context.CancellationToken;
+ SyntaxNode root = await document.GetSyntaxRootAsync(token).ConfigureAwait(false);
+ SemanticModel model = await document.GetSemanticModelAsync(token).ConfigureAwait(false);
+ var compilation = model.Compilation;
+
+ if (!RequiredSymbols.TryGetSymbols(compilation, out RequiredSymbols symbols) ||
+ root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode reportedNode ||
+ model.GetOperation(reportedNode, token) is not IInvocationOperation reportedInvocation)
+ {
+ return;
+ }
+
+ var bestCandidates = PreferAsSpanOverSubstring.GetBestSpanBasedOverloads(symbols, reportedInvocation, context.CancellationToken);
+
+ // We only apply the fix if there is an unambiguous best overload.
+ if (bestCandidates.Length != 1)
+ return;
+ IMethodSymbol spanBasedOverload = bestCandidates[0];
+
+ string title = MicrosoftNetCoreAnalyzersResources.PreferAsSpanOverSubstringCodefixTitle;
+ var codeAction = CodeAction.Create(title, CreateChangedDocument, title);
+ context.RegisterCodeFix(codeAction, context.Diagnostics);
+ return;
+
+ async Task CreateChangedDocument(CancellationToken token)
+ {
+ var editor = await DocumentEditor.CreateAsync(document, token).ConfigureAwait(false);
+
+ foreach (var argument in reportedInvocation.Arguments)
+ {
+ IOperation value = argument.Value.WalkDownConversion(c => c.IsImplicit);
+ IParameterSymbol newParameter = spanBasedOverload.Parameters[argument.Parameter.Ordinal];
+
+ // Convert Substring invocations to equivalent AsSpan invocations.
+ if (symbols.IsAnySubstringInvocation(value) && SymbolEqualityComparer.Default.Equals(newParameter.Type, symbols.ReadOnlySpanOfCharType))
+ {
+ ReplaceNonConditionalInvocationMethodName(editor, value.Syntax, nameof(MemoryExtensions.AsSpan));
+ // Ensure named Substring arguments get renamed to their equivalent AsSpan counterparts.
+ ReplaceNamedArgumentName(editor, value.Syntax, SubstringStartIndexArgumentName, AsSpanStartArgumentName);
+ }
+
+ // Ensure named arguments on the original overload are renamed to their
+ // ordinal counterparts on the new overload.
+ string oldArgumentName = argument.Parameter.Name;
+ string newArgumentName = newParameter.Name;
+ ReplaceNamedArgumentName(editor, reportedInvocation.Syntax, oldArgumentName, newArgumentName);
+ }
+
+ // Import System namespace if necessary.
+ if (!IsMemoryExtensionsInScope(symbols, reportedInvocation))
+ {
+ SyntaxNode withoutSystemImport = editor.GetChangedRoot();
+ SyntaxNode systemNamespaceImportStatement = editor.Generator.NamespaceImportDeclaration(nameof(System));
+ SyntaxNode withSystemImport = editor.Generator.AddNamespaceImports(withoutSystemImport, systemNamespaceImportStatement);
+ editor.ReplaceNode(editor.OriginalRoot, withSystemImport);
+ }
+
+ return editor.GetChangedDocument();
+ }
+ }
+
+ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ private static bool IsMemoryExtensionsInScope(in RequiredSymbols symbols, IInvocationOperation invocation)
+ {
+ var model = invocation.SemanticModel;
+ int position = invocation.Syntax.SpanStart;
+ const string name = nameof(MemoryExtensions);
+
+ return model.LookupNamespacesAndTypes(position, name: name)
+ .Contains(symbols.MemoryExtensionsType, SymbolEqualityComparer.Default);
+ }
+ }
+}
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs
new file mode 100644
index 0000000000..f17f83968a
--- /dev/null
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstring.cs
@@ -0,0 +1,298 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Analyzer.Utilities;
+using Analyzer.Utilities.Extensions;
+using Analyzer.Utilities.PooledObjects;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+using Resx = Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources;
+
+namespace Microsoft.NetCore.Analyzers.Runtime
+{
+ ///
+ /// CA1842: Prefer 'AsSpan' over 'Substring'.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+ public sealed class PreferAsSpanOverSubstring : DiagnosticAnalyzer
+ {
+ internal const string RuleId = "CA1846";
+
+ private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringTitle), Resx.ResourceManager, typeof(Resx));
+ private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringMessage), Resx.ResourceManager, typeof(Resx));
+ private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(Resx.PreferAsSpanOverSubstringDescription), Resx.ResourceManager, typeof(Resx));
+
+ internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
+ RuleId,
+ s_localizableTitle,
+ s_localizableMessage,
+ DiagnosticCategory.Performance,
+ RuleLevel.IdeSuggestion,
+ s_localizableDescription,
+ isPortedFxCopRule: false,
+ isDataflowRule: false);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterCompilationStartAction(OnCompilationStart);
+ }
+
+ private static void OnCompilationStart(CompilationStartAnalysisContext context)
+ {
+ if (!RequiredSymbols.TryGetSymbols(context.Compilation, out RequiredSymbols symbols))
+ return;
+
+ context.RegisterOperationBlockStartAction(OnOperationBlockStart);
+ return;
+
+ // Local functions
+
+ void OnOperationBlockStart(OperationBlockStartAnalysisContext context)
+ {
+ var invocations = PooledConcurrentSet.GetInstance();
+
+ context.RegisterOperationAction(context =>
+ {
+ var argument = (IArgumentOperation)context.Operation;
+ if (symbols.IsAnySubstringInvocation(argument.Value.WalkDownConversion(c => c.IsImplicit)) && argument.Parent is IInvocationOperation invocation)
+ {
+ invocations.Add(invocation);
+ }
+ }, OperationKind.Argument);
+
+ context.RegisterOperationBlockEndAction(context =>
+ {
+ foreach (var invocation in invocations)
+ {
+ // We search for an overload of the invoked member whose signature matches the signature of
+ // the invoked member, except with ReadOnlySpan substituted in for some of the
+ // arguments that are Substring invocations.
+ if (!GetBestSpanBasedOverloads(symbols, invocation, context.CancellationToken).IsEmpty)
+ {
+ Diagnostic diagnostic = invocation.CreateDiagnostic(Rule);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ invocations.Free(context.CancellationToken);
+ });
+ }
+ }
+
+ ///
+ /// Gets all the overloads that are tied for being the "best" span-based overload for the specified .
+ /// An overload is considered "better" if it allows more Substring invocations to be replaced with AsSpan invocations.
+ ///
+ /// If there are no overloads that replace any Substring calls, or none of the arguments in the invocation are
+ /// Substring calls, an empty array is returned.
+ ///
+ internal static ImmutableArray GetBestSpanBasedOverloads(in RequiredSymbols symbols, IInvocationOperation invocation, CancellationToken cancellationToken)
+ {
+ var method = invocation.TargetMethod;
+
+ // Whether an argument at a particular parameter ordinal is a Substring call.
+ Span isSubstringLookup = stackalloc bool[method.Parameters.Length];
+ int substringCalls = 0;
+ foreach (var argument in invocation.Arguments)
+ {
+ if (symbols.IsAnySubstringInvocation(argument.Value.WalkDownConversion(c => c.IsImplicit)))
+ {
+ isSubstringLookup[argument.Parameter.Ordinal] = true;
+ ++substringCalls;
+ }
+ }
+
+ if (substringCalls == 0)
+ return ImmutableArray.Empty;
+
+ // Find all overloads that are tied for being the "best" overload. An overload is considered
+ // "better" if it allows more Substring calls to be replaced with AsSpan calls.
+ var bestCandidates = ImmutableArray.CreateBuilder();
+ int resultQuality = 0;
+ var candidates = GetAllAccessibleOverloadsAtInvocationCallSite(invocation, cancellationToken);
+ foreach (var candidate in candidates)
+ {
+ int quality = EvaluateCandidateQuality(symbols, isSubstringLookup, invocation, candidate);
+
+ // Reject candidates that do not replace at least one Substring call.
+ if (quality < 1)
+ {
+ continue;
+ }
+ else if (quality == resultQuality)
+ {
+ bestCandidates.Add(candidate);
+ }
+ else if (quality > resultQuality)
+ {
+ resultQuality = quality;
+ bestCandidates.Clear();
+ bestCandidates.Add(candidate);
+ }
+ }
+
+ return bestCandidates.ToImmutable();
+
+ // Returns a number indicating how good the candidate method is.
+ // If the candidate is valid, the number of Substring calls that can be replaced with AsSpan calls is returned.
+ // If the candidate is invalid, -1 is returned.
+ static int EvaluateCandidateQuality(in RequiredSymbols symbols, ReadOnlySpan isSubstringLookup, IInvocationOperation invocation, IMethodSymbol candidate)
+ {
+ var method = invocation.TargetMethod;
+
+ if (candidate.Parameters.Length != method.Parameters.Length)
+ return -1;
+
+ int replacementCount = 0;
+ foreach (var parameter in candidate.Parameters)
+ {
+ if (isSubstringLookup[parameter.Ordinal] && SymbolEqualityComparer.Default.Equals(parameter.Type, symbols.ReadOnlySpanOfCharType))
+ {
+ ++replacementCount;
+ continue;
+ }
+
+ var oldParameter = method.Parameters[parameter.Ordinal];
+ if (!SymbolEqualityComparer.Default.Equals(parameter.Type, oldParameter.Type))
+ {
+ return -1;
+ }
+ }
+
+ return replacementCount;
+ }
+ }
+
+ private static IEnumerable GetAllAccessibleOverloadsAtInvocationCallSite(IInvocationOperation invocation, CancellationToken cancellationToken)
+ {
+ var method = invocation.TargetMethod;
+ var model = invocation.SemanticModel;
+ int location = invocation.Syntax.SpanStart;
+ var instance = invocation.Instance;
+
+ IEnumerable allOverloads;
+ if (method.IsStatic)
+ {
+ allOverloads = model.LookupStaticMembers(location, method.ContainingType, method.Name).OfType();
+ }
+ else if (instance is not null)
+ {
+ // Ensure protected members can only be invoked on instances that are known to be instances of the accessing class.
+ var enclosingType = GetEnclosingType(model, location, cancellationToken);
+ allOverloads = model.LookupSymbols(location, instance.Type, method.Name).OfType();
+ if (instance.Type.DerivesFrom(enclosingType, baseTypesOnly: true) || instance is IInstanceReferenceOperation)
+ {
+ allOverloads = allOverloads.Union(model.LookupBaseMembers(location, method.Name).OfType());
+ }
+ }
+ else
+ {
+ // This can happen when compiling invalid code.
+ return Enumerable.Empty();
+ }
+
+ return allOverloads.Where(x => x.IsStatic == method.IsStatic && SymbolEqualityComparer.Default.Equals(x.ReturnType, method.ReturnType));
+
+ static INamedTypeSymbol GetEnclosingType(SemanticModel model, int location, CancellationToken cancellationToken)
+ {
+ ISymbol symbol = model.GetEnclosingSymbol(location, cancellationToken);
+ if (symbol is not INamedTypeSymbol type)
+ type = symbol.ContainingType;
+
+ return type;
+ }
+ }
+
+ // Use struct to avoid allocations.
+#pragma warning disable CA1815 // Override equals and operator equals on value types
+ internal readonly struct RequiredSymbols
+#pragma warning restore CA1815 // Override equals and operator equals on value types
+ {
+ private RequiredSymbols(
+ INamedTypeSymbol stringType, INamedTypeSymbol roscharType,
+ INamedTypeSymbol memoryExtensionsType,
+ IMethodSymbol substring1, IMethodSymbol substring2,
+ IMethodSymbol asSpan1, IMethodSymbol asSpan2)
+ {
+ StringType = stringType;
+ ReadOnlySpanOfCharType = roscharType;
+ MemoryExtensionsType = memoryExtensionsType;
+ SubstringStart = substring1;
+ SubstringStartLength = substring2;
+ AsSpanStart = asSpan1;
+ AsSpanStartLength = asSpan2;
+ }
+
+ public static bool TryGetSymbols(Compilation compilation, out RequiredSymbols symbols)
+ {
+ var stringType = compilation.GetSpecialType(SpecialType.System_String);
+ var charType = compilation.GetSpecialType(SpecialType.System_Char);
+ var int32Type = compilation.GetSpecialType(SpecialType.System_Int32);
+
+ if (stringType is null || charType is null || int32Type is null)
+ {
+ symbols = default;
+ return false;
+ }
+
+ var readOnlySpanOfCharType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1)?.Construct(charType);
+ var memoryExtensionsType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemoryExtensions);
+
+ if (readOnlySpanOfCharType is null || memoryExtensionsType is null)
+ {
+ symbols = default;
+ return false;
+ }
+
+ var int32ParamInfo = ParameterInfo.GetParameterInfo(int32Type);
+ var stringParamInfo = ParameterInfo.GetParameterInfo(stringType);
+
+ var substringMembers = stringType.GetMembers(nameof(string.Substring)).OfType();
+ var substringStart = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo);
+ var substringStartLength = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo);
+
+ var asSpanMembers = memoryExtensionsType.GetMembers(nameof(MemoryExtensions.AsSpan)).OfType();
+ var asSpanStart = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, int32ParamInfo);
+ var asSpanStartLength = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, int32ParamInfo, int32ParamInfo);
+
+ if (substringStart is null || substringStartLength is null || asSpanStart is null || asSpanStartLength is null)
+ {
+ symbols = default;
+ return false;
+ }
+
+ symbols = new RequiredSymbols(
+ stringType, readOnlySpanOfCharType,
+ memoryExtensionsType,
+ substringStart, substringStartLength,
+ asSpanStart, asSpanStartLength);
+ return true;
+ }
+
+ public INamedTypeSymbol StringType { get; }
+ public INamedTypeSymbol ReadOnlySpanOfCharType { get; }
+ public INamedTypeSymbol MemoryExtensionsType { get; }
+ public IMethodSymbol SubstringStart { get; }
+ public IMethodSymbol SubstringStartLength { get; }
+ public IMethodSymbol AsSpanStart { get; }
+ public IMethodSymbol AsSpanStartLength { get; }
+
+ public bool IsAnySubstringInvocation(IOperation operation)
+ {
+ if (operation is not IInvocationOperation invocation)
+ return false;
+ return SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, SubstringStart) ||
+ SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, SubstringStartLength);
+ }
+ }
+ }
+}
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf
index 6af04a2095..68812c7030 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf
@@ -1617,6 +1617,26 @@
Potenciální cyklus odkazů v deserializovaném grafu objektů
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf
index cfaa562463..0785cce804 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf
@@ -1617,6 +1617,26 @@
Potenzieller Verweiszyklus in deserialisiertem Objektgraph
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf
index 0018af8f92..0dddd9fbe3 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf
@@ -1617,6 +1617,26 @@
Posible ciclo de referencia en el gráfico de objetos deserializados
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf
index f915bf31cd..bab5e55d08 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf
@@ -1617,6 +1617,26 @@
Cycle de référence potentiel dans un graphe d'objet désérialisé
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf
index 8eb665ee47..08c50ace13 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf
@@ -1617,6 +1617,26 @@
Potenziale ciclo di riferimento nel grafico di oggetti deserializzati
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf
index 8e53387376..e13f8d5717 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf
@@ -1617,6 +1617,26 @@
逆シリアル化されたオブジェクト グラフ内の参照サイクルの可能性
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf
index 40a9ba2dee..e28b4caccc 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf
@@ -1617,6 +1617,26 @@
역직렬화된 개체 그래프의 잠재적 참조 주기
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf
index 592daa7204..9e552a899d 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf
@@ -1617,6 +1617,26 @@
Potencjalny cykl odwołań w deserializowanym grafie obiektów
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf
index e285035d7e..8d9726c1af 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf
@@ -1617,6 +1617,26 @@
Ciclo de referência potencial em grafo de objeto desserializado
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf
index 85434b1f50..3ff7eabf40 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf
@@ -1617,6 +1617,26 @@
Потенциальное зацикливание ссылок в графе десериализованного объекта
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf
index dc94be6024..694c65a137 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf
@@ -1617,6 +1617,26 @@
Seri durumdan çıkarılan nesne grafındaki olası başvuru döngüsü
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf
index dc86c9a46a..e2b26bf1fb 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf
@@ -1617,6 +1617,26 @@
反序列化对象图中的潜在引用循环
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf
index 16bfe4a6d0..c69cab2ec6 100644
--- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf
+++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf
@@ -1617,6 +1617,26 @@
還原序列化物件圖中的潛在參考迴圈
+
+
+ Replace 'Substring' with 'AsSpan'
+
+
+
+
+ 'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+
+
+
+ Prefer 'AsSpan' over 'Substring' when span-based overloads are available
+
+
+
+
+ Prefer 'AsSpan' over 'Substring'
+
+ Use 'ContainsKey'
diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
index a3222d52e0..cb6537e39b 100644
--- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
+++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
@@ -1392,6 +1392,18 @@ It is more efficient to use 'AsSpan' and 'string.Concat', instead of 'Substring'
|CodeFix|True|
---
+## [CA1846](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846): Prefer 'AsSpan' over 'Substring'
+
+'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.
+
+|Item|Value|
+|-|-|
+|Category|Performance|
+|Enabled|True|
+|Severity|Info|
+|CodeFix|True|
+---
+
## [CA2000](https://docs.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.
diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
index b7a3af4bb9..f67315150f 100644
--- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
+++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
@@ -2582,6 +2582,26 @@
]
}
},
+ "CA1846": {
+ "id": "CA1846",
+ "shortDescription": "Prefer 'AsSpan' over 'Substring'",
+ "fullDescription": "'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.",
+ "defaultLevel": "note",
+ "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846",
+ "properties": {
+ "category": "Performance",
+ "isEnabledByDefault": true,
+ "typeName": "PreferAsSpanOverSubstring",
+ "languages": [
+ "C#",
+ "Visual Basic"
+ ],
+ "tags": [
+ "Telemetry",
+ "EnabledRuleInAggressiveMode"
+ ]
+ }
+ },
"CA2000": {
"id": "CA2000",
"shortDescription": "Dispose objects before losing scope",
diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md
index abf017a075..2201c424f8 100644
--- a/src/NetAnalyzers/RulesMissingDocumentation.md
+++ b/src/NetAnalyzers/RulesMissingDocumentation.md
@@ -6,8 +6,8 @@ CA2250 | | Use valid platform string |
CA1839 | | Use 'Environment.ProcessPath' |
CA1840 | | Use 'Environment.CurrentManagedThreadId' |
-CA1841 | | Prefer Dictionary.Contains methods |
CA1842 | | Do not use 'WhenAll' with a single task |
CA1843 | | Do not use 'WaitAll' with a single task |
CA1844 | | Provide memory-based overrides of async methods when subclassing 'Stream' |
CA1845 | | Use span-based 'string.Concat' |
+CA1846 | | Prefer 'AsSpan' over 'Substring' |
diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs
new file mode 100644
index 0000000000..dc747f9e90
--- /dev/null
+++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferAsSpanOverSubstringTests.cs
@@ -0,0 +1,1850 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.VisualBasic;
+using Xunit;
+
+using VerifyCS = Test.Utilities.CSharpCodeFixVerifier<
+ Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring,
+ Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpPreferAsSpanOverSubstringFixer>;
+using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier<
+ Microsoft.NetCore.Analyzers.Runtime.PreferAsSpanOverSubstring,
+ Microsoft.NetCore.VisualBasic.Analyzers.Runtime.BasicPreferAsSpanOverSubstringFixer>;
+
+namespace Microsoft.NetCore.Analyzers.Runtime.UnitTests
+{
+ public class PreferAsSpanOverSubstringTests
+ {
+ public static IEnumerable