File: ConvertNumericLiteral\AbstractConvertNumericLiteralCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
 
namespace Microsoft.CodeAnalysis.ConvertNumericLiteral;
 
internal abstract class AbstractConvertNumericLiteralCodeRefactoringProvider<TNumericLiteralExpression>(string hexPrefix, string binaryPrefix)
    : CodeRefactoringProvider
    where TNumericLiteralExpression : SyntaxNode
{
    private enum NumericKind { Unknown, Decimal, Binary, Hexadecimal }
 
    private readonly Regex _regex = new($"({hexPrefix}|{binaryPrefix})?([_0-9a-f]+)(.*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
    /// <summary>
    /// Converting numbers is a fairly uncommon task.  Put these at the end of the list after more relevant
    /// refactorings.
    /// </summary>
    protected override CodeActionRequestPriority ComputeRequestPriority()
        => CodeActionRequestPriority.Low;
 
    public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, _, cancellationToken) = context;
        var numericLiteralNode = await context.TryGetRelevantNodeAsync<TNumericLiteralExpression>().ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        if (!syntaxFacts.IsNumericLiteralExpression(numericLiteralNode))
            return;
 
        var numericToken = numericLiteralNode.GetFirstToken();
        if (numericToken.ContainsDiagnostics)
            return;
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var symbol = semanticModel.GetTypeInfo(numericLiteralNode, cancellationToken).Type;
        if (symbol == null)
            return;
 
        if (!symbol.SpecialType.IsIntegralType())
            return;
 
        var valueOpt = semanticModel.GetConstantValue(numericLiteralNode);
        if (!valueOpt.HasValue)
            return;
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var value = IntegerUtilities.ToInt64(valueOpt.Value);
        var numericText = numericToken.ToString();
        var (prefix, number, suffix) = GetNumericLiteralParts(numericText);
        var kind = string.IsNullOrEmpty(prefix) ? NumericKind.Decimal
            : prefix.Equals(hexPrefix, StringComparison.OrdinalIgnoreCase) ? NumericKind.Hexadecimal
            : prefix.Equals(binaryPrefix, StringComparison.OrdinalIgnoreCase) ? NumericKind.Binary
            : NumericKind.Unknown;
 
        if (kind == NumericKind.Unknown)
            return;
 
        using var result = TemporaryArray<CodeAction>.Empty;
 
        if (kind != NumericKind.Decimal)
            result.Add(CreateCodeAction(value.ToString(), FeaturesResources.Convert_to_decimal));
 
        if (kind != NumericKind.Binary)
            result.Add(CreateCodeAction(binaryPrefix + Convert.ToString(value, toBase: 2), FeaturesResources.Convert_to_binary));
 
        if (kind != NumericKind.Hexadecimal)
            result.Add(CreateCodeAction(hexPrefix + value.ToString("X"), FeaturesResources.Convert_to_hex));
 
        const string DigitSeparator = "_";
        if (numericText.Contains(DigitSeparator))
        {
            result.Add(CreateCodeAction(prefix + number.Replace(DigitSeparator, string.Empty), FeaturesResources.Remove_separators));
        }
        else
        {
            result.AsRef().AddIfNotNull(kind switch
            {
                NumericKind.Decimal when number.Length > 3 => CreateCodeAction(AddSeparators(number, interval: 3), FeaturesResources.Separate_thousands),
                NumericKind.Hexadecimal when number.Length > 4 => CreateCodeAction(hexPrefix + AddSeparators(number, interval: 4), FeaturesResources.Separate_words),
                NumericKind.Binary when number.Length > 4 => CreateCodeAction(binaryPrefix + AddSeparators(number, interval: 4), FeaturesResources.Separate_nibbles),
                _ => null,
            });
        }
 
        if (result.Count == 1)
        {
            context.RegisterRefactoring(result[0]);
        }
        else if (result.Count > 1)
        {
            context.RegisterRefactoring(CodeAction.Create(
                FeaturesResources.Convert_number,
                result.ToImmutableAndClear(),
                isInlinable: true));
        }
 
        CodeAction CreateCodeAction(string text, string title)
            => CodeAction.Create(title, c => ReplaceTokenAsync(document, root, numericToken, value, text, suffix), title);
 
        static string AddSeparators(string numericText, int interval)
        {
            // Insert digit separators in the given interval.
            var result = Regex.Replace(numericText, $"(.{{{interval}}})", "_$1", RegexOptions.RightToLeft);
            // Fix for the case "0x_1111" that is not supported yet.
            return result[0] == '_' ? result[1..] : result;
        }
 
        static Task<Document> ReplaceTokenAsync(Document document, SyntaxNode root, SyntaxToken numericToken, long value, string text, string suffix)
        {
            var generator = SyntaxGenerator.GetGenerator(document);
            var updatedToken = generator.NumericLiteralToken(text + suffix, (ulong)value)
                .WithTriviaFrom(numericToken);
            var updatedRoot = root.ReplaceToken(numericToken, updatedToken);
            return Task.FromResult(document.WithSyntaxRoot(updatedRoot));
        }
 
        (string prefix, string number, string suffix) GetNumericLiteralParts(string numericText)
        {
            // Match literal text and extract out base prefix, type suffix and the number itself.
            var groups = _regex.Match(numericText).Groups;
            return (prefix: groups[1].Value, number: groups[2].Value, suffix: groups[3].Value);
        }
    }
}