Skip to content

Commit

Permalink
Merge pull request #4806 from NewellClark/prefer-AsSpan-over-Substrin…
Browse files Browse the repository at this point in the history
…g-analyzer

(4/4) Prefer 'AsSpan' over 'Substring' analyzer
  • Loading branch information
mavasani committed May 21, 2021
2 parents 1d505a7 + 34dc2d6 commit 5ee4a10
Show file tree
Hide file tree
Showing 26 changed files with 2,689 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,18 @@
<value>and all other platforms</value>
<comment>This call site is reachable on: 'windows' 10.0.2000 and later, and all other platforms</comment>
</data>
<data name="PreferAsSpanOverSubstringDescription" xml:space="preserve">
<value>'AsSpan' is more efficient then 'Substring'. 'Substring' performs an O(n) string copy, while 'AsSpan' does not and has a constant cost.</value>
</data>
<data name="PreferAsSpanOverSubstringMessage" xml:space="preserve">
<value>Prefer 'AsSpan' over 'Substring' when span-based overloads are available</value>
</data>
<data name="PreferAsSpanOverSubstringTitle" xml:space="preserve">
<value>Prefer 'AsSpan' over 'Substring'</value>
</data>
<data name="PreferAsSpanOverSubstringCodefixTitle" xml:space="preserve">
<value>Replace 'Substring' with 'AsSpan'</value>
</data>
<data name="UseCancellationTokenThrowIfCancellationRequestedDescription" xml:space="preserve">
<value>'ThrowIfCancellationRequested' automatically checks whether the token has been canceled, and throws an 'OperationCanceledException' if it has.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> 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<Document> 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);
}
}
}
Loading

0 comments on commit 5ee4a10

Please sign in to comment.