File: Microsoft.NetCore.Analyzers\Performance\CSharpUseSearchValues.Fixer.cs
Web Access
Project: ..\..\..\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.CSharp.NetAnalyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj (Microsoft.CodeAnalysis.CSharp.NetAnalyzers)
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.NetCore.Analyzers.Performance;
 
namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{
    /// <inheritdoc/>
    [ExportCodeFixProvider(LanguageNames.CSharp)]
    public sealed class CSharpUseSearchValuesFixer : UseSearchValuesFixer
    {
        protected override async ValueTask<(SyntaxNode TypeDeclaration, INamedTypeSymbol? TypeSymbol, bool IsRealType)> GetTypeSymbolAsync(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken)
        {
            SyntaxNode? typeDeclarationOrCompilationUnit = node.FirstAncestorOrSelf<TypeDeclarationSyntax>();
 
            typeDeclarationOrCompilationUnit ??= await node.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
            return typeDeclarationOrCompilationUnit is TypeDeclarationSyntax typeDeclaration
                ? (typeDeclaration, semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken), IsRealType: true)
                : (typeDeclarationOrCompilationUnit, semanticModel.GetDeclaredSymbol((CompilationUnitSyntax)typeDeclarationOrCompilationUnit, cancellationToken)?.ContainingType, IsRealType: false);
        }
 
        protected override SyntaxNode ReplaceSearchValuesFieldName(SyntaxNode node)
        {
            if (node is FieldDeclarationSyntax fieldDeclaration &&
                fieldDeclaration.Declaration is { } declaration &&
                declaration.Variables is [var declarator])
            {
                var newDeclarator = declarator.ReplaceToken(declarator.Identifier, declarator.Identifier.WithAdditionalAnnotations(RenameAnnotation.Create()));
                return fieldDeclaration.WithDeclaration(declaration.WithVariables(new SeparatedSyntaxList<VariableDeclaratorSyntax>().Add(newDeclarator)));
            }
 
            return node;
        }
 
        protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax)
        {
            if (syntax is VariableDeclaratorSyntax variableDeclarator)
            {
                return variableDeclarator.Initializer!.Value;
            }
 
            if (syntax is PropertyDeclarationSyntax propertyDeclaration)
            {
                return CSharpUseSearchValuesAnalyzer.TryGetPropertyGetterExpression(propertyDeclaration)!;
            }
 
            throw new InvalidOperationException($"Expected 'VariableDeclaratorSyntax' or 'PropertyDeclarationSyntax', got {syntax.GetType().Name}");
        }
 
        // new[] { 'a', 'b', 'c' } => "abc"
        // new[] { (byte)'a', (byte)'b', (byte)'c' } => "abc"u8
        // "abc".ToCharArray() => "abc"
        // ['a', 'b', 'c'] => "abc"
        // [(byte)'a', (byte)'b', (byte)'c'] => "abc"u8
        protected override SyntaxNode? TryReplaceArrayCreationWithInlineLiteralExpression(IOperation operation)
        {
            if (operation is IConversionOperation conversion)
            {
                operation = conversion.Operand;
            }
 
            if (operation is IInvocationOperation invocation)
            {
                if (UseSearchValuesAnalyzer.IsConstantStringToCharArrayInvocation(invocation, out _))
                {
                    Debug.Assert(invocation.Instance is not null);
                    return invocation.Instance!.Syntax;
                }
 
                return null;
            }
 
            ITypeSymbol? elementType = null;
 
            if (operation.Type is IArrayTypeSymbol arrayType)
            {
                elementType = arrayType.ElementType;
            }
            else if (operation.Type is INamedTypeSymbol namedType)
            {
                if (namedType.TypeArguments is [var typeArgument])
                {
                    Debug.Assert(namedType.Name.Contains("Span", StringComparison.Ordinal), namedType.Name);
 
                    elementType = typeArgument;
                }
            }
 
            if (elementType is not null)
            {
                bool isByte = elementType.SpecialType == SpecialType.System_Byte;
 
                if (isByte &&
                    (operation.SemanticModel?.Compilation is not CSharpCompilation compilation ||
                    compilation.LanguageVersion < (LanguageVersion)1100)) // LanguageVersion.CSharp11
                {
                    // Can't use Utf8StringLiterals
                    return null;
                }
 
                List<char> values = new();
 
                if (operation.Syntax is ExpressionSyntax creationSyntax &&
                    CSharpUseSearchValuesAnalyzer.IsConstantByteOrCharArrayCreationExpression(operation.SemanticModel!, creationSyntax, values, out _) &&
                    values.Count <= 128 &&                  // Arbitrary limit to avoid emitting huge literals
                    !ContainsAnyComments(creationSyntax))   // Avoid removing potentially valuable comments
                {
                    if (isByte)
                    {
                        foreach (char c in values)
                        {
                            if (c > 127)
                            {
                                // We shouldn't turn non-ASCII byte values into Utf8StringLiterals as that may change behavior.
                                // e.g. (byte)'ÿ' means 0xFF, but "ÿ"u8 is two bytes (0xC3, 0xBF) that encode that character in UTF8.
                                return null;
                            }
                        }
                    }
 
                    string valuesString = string.Concat(values);
                    string stringLiteral = SymbolDisplay.FormatLiteral(valuesString, quote: true);
 
                    const SyntaxKind Utf8StringLiteralExpression = (SyntaxKind)8756;
                    const SyntaxKind Utf8StringLiteralToken = (SyntaxKind)8520;
 
                    return SyntaxFactory.LiteralExpression(
                        isByte ? Utf8StringLiteralExpression : SyntaxKind.StringLiteralExpression,
                        SyntaxFactory.Token(
                            leading: default,
                            kind: isByte ? Utf8StringLiteralToken : SyntaxKind.StringLiteralToken,
                            text: isByte ? $"{stringLiteral}u8" : stringLiteral,
                            valueText: valuesString,
                            trailing: default));
                }
            }
 
            return null;
        }
 
        private static bool ContainsAnyComments(SyntaxNode node)
        {
            foreach (SyntaxTrivia trivia in node.DescendantTrivia(node.Span))
            {
                if (trivia.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.MultiLineCommentTrivia)
                {
                    return true;
                }
            }
 
            return false;
        }
    }
}