| File: Microsoft.NetCore.Analyzers\Runtime\UseSpanBasedStringConcat.Fixer.cs | Web Access |
| Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Analyzer.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Operations; using Resx = Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; using RequiredSymbols = Microsoft.NetCore.Analyzers.Runtime.UseSpanBasedStringConcat.RequiredSymbols; namespace Microsoft.NetCore.Analyzers.Runtime { public abstract class UseSpanBasedStringConcatFixer : CodeFixProvider { private protected const string AsSpanName = nameof(MemoryExtensions.AsSpan); private protected const string AsSpanStartParameterName = "start"; private protected const string ToStringName = nameof(ToString); private static readonly SyntaxAnnotation s_asSpanSymbolAnnotation = new("SymbolId", "System.MemoryExtensions"); private protected abstract SyntaxNode ReplaceInvocationMethodName(SyntaxGenerator generator, SyntaxNode invocationSyntax, string newName); private protected abstract IOperation WalkDownBuiltInImplicitConversionOnConcatOperand(IOperation operand); private protected abstract bool IsNamedArgument(IArgumentOperation argumentOperation); public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseSpanBasedStringConcat.RuleId); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var document = context.Document; var diagnostic = context.Diagnostics.First(); var cancellationToken = context.CancellationToken; var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var compilation = model.Compilation; SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (!RequiredSymbols.TryGetSymbols(compilation, out var symbols)) return; if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode concatExpressionSyntax) return; // OperatorKind will be BinaryOperatorKind.Concatenate, even when '+' is used instead of '&' in Visual Basic. if (model.GetOperation(concatExpressionSyntax, cancellationToken) is not IBinaryOperation concatOperation || concatOperation.OperatorKind is not (BinaryOperatorKind.Add or BinaryOperatorKind.Concatenate)) { return; } var operands = UseSpanBasedStringConcat.FlattenBinaryOperation(concatOperation); // Bail out if we don't have a long enough span-based string.Concat overload. if (!symbols.TryGetRoscharConcatMethodWithArity(operands.Length, out IMethodSymbol? roscharConcatMethod)) return; // Bail if none of the operands are a non-conditional substring invocation. This could be the case if the // only substring invocations in the expression were conditional invocations. if (!operands.Any(IsAnyNonConditionalSubstringInvocation)) return; var codeAction = CodeAction.Create( Resx.UseSpanBasedStringConcatCodeFixTitle, FixConcatOperationChain, Resx.UseSpanBasedStringConcatCodeFixTitle); context.RegisterCodeFix(codeAction, diagnostic); return; // Local functions bool IsAnyNonConditionalSubstringInvocation(IOperation operation) { var value = WalkDownBuiltInImplicitConversionOnConcatOperand(operation); return value is IInvocationOperation invocation && symbols.IsAnySubstringMethod(invocation.TargetMethod); } async Task<Document> FixConcatOperationChain(CancellationToken cancellationToken) { RoslynDebug.Assert(roscharConcatMethod is not null); var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); var generator = editor.Generator; SyntaxNode stringTypeNameSyntax = generator.TypeExpressionForStaticMemberAccess(symbols.StringType); SyntaxNode concatMemberAccessSyntax = generator.MemberAccessExpression(stringTypeNameSyntax, roscharConcatMethod.Name); // Save leading and trailing trivia so it can be attached to the outside of the string.Concat invocation node. var leadingTrivia = operands.First().Syntax.GetLeadingTrivia(); var trailingTrivia = operands.Last().Syntax.GetTrailingTrivia(); var arguments = ImmutableArray.CreateBuilder<SyntaxNode>(operands.Length); foreach (var operand in operands) arguments.Add(ConvertOperandToArgument(symbols, generator, operand)); // Strip off leading and trailing trivia from first and last operand nodes, respectively, and // reattach it to the outside of the newly-created string.Concat invocation node. arguments[0] = arguments[0].WithoutLeadingTrivia(); arguments[^1] = arguments[^1].WithoutTrailingTrivia(); SyntaxNode concatMethodInvocationSyntax = generator.InvocationExpression(concatMemberAccessSyntax, arguments.MoveToImmutable()) .WithLeadingTrivia(leadingTrivia) .WithTrailingTrivia(trailingTrivia); SyntaxNode newRoot = generator.ReplaceNode(root, concatExpressionSyntax, concatMethodInvocationSyntax); editor.ReplaceNode(root, newRoot); return editor.GetChangedDocument(); } } public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; private SyntaxNode ConvertOperandToArgument(in RequiredSymbols symbols, SyntaxGenerator generator, IOperation operand) { var value = WalkDownBuiltInImplicitConversionOnConcatOperand(operand); // Convert substring invocations to equivalent AsSpan invocation. if (value is IInvocationOperation invocation && symbols.IsAnySubstringMethod(invocation.TargetMethod)) { SyntaxNode invocationSyntax = invocation.Syntax; // Swap out parameter names if named-arguments are used. if (TryGetNamedStartIndexArgument(symbols, invocation, out var namedStartIndexArgument)) { var renamedArgumentSyntax = generator.Argument(AsSpanStartParameterName, RefKind.None, namedStartIndexArgument.Value.Syntax); invocationSyntax = generator.ReplaceNode(invocationSyntax, namedStartIndexArgument.Syntax, renamedArgumentSyntax); } var asSpanInvocationSyntax = ReplaceInvocationMethodName(generator, invocationSyntax, AsSpanName).WithAddImportsAnnotation().WithAdditionalAnnotations(s_asSpanSymbolAnnotation); return generator.Argument(asSpanInvocationSyntax); } // Character literals become string literals. else if (value.Type?.SpecialType == SpecialType.System_Char && value is ILiteralOperation literalOperation && literalOperation.ConstantValue.HasValue && literalOperation.ConstantValue.Value is { } literalValue) { var stringLiteral = generator.LiteralExpression(literalValue.ToString()).WithTriviaFrom(literalOperation.Syntax); return generator.Argument(stringLiteral); } else { return generator.Argument(value.Syntax); } bool TryGetNamedStartIndexArgument(in RequiredSymbols symbols, IInvocationOperation substringInvocation, [NotNullWhen(true)] out IArgumentOperation? namedStartIndexArgument) { RoslynDebug.Assert(symbols.IsAnySubstringMethod(substringInvocation.TargetMethod)); foreach (var argument in substringInvocation.Arguments) { if (IsNamedArgument(argument) && symbols.IsAnySubstringStartIndexParameter(argument.Parameter)) { namedStartIndexArgument = argument; return true; } } namedStartIndexArgument = default; return false; } } } }