File: ConvertToInterpolatedString\ConvertRegularStringToInterpolatedStringRefactoringProvider.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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString;
 
/// <summary>
/// Code refactoring that converts a regular string containing braces to an interpolated string
/// </summary>
[ExportCodeRefactoringProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared]
internal sealed class ConvertRegularStringToInterpolatedStringRefactoringProvider : CodeRefactoringProvider
{
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public ConvertRegularStringToInterpolatedStringRefactoringProvider()
    {
    }
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, _, cancellationToken) = context;
 
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        if (root is null)
            return;
 
        var token = root.FindToken(context.Span.Start);
        if (!context.Span.IntersectsWith(token.Span))
            return;
 
        var syntaxKinds = document.GetRequiredLanguageService<ISyntaxKindsService>();
        if (token.RawKind != syntaxKinds.StringLiteralToken)
            return;
 
        var literalExpression = token.GetRequiredParent();
 
        // Check the string literal for errors.  This will ensure that we do not try to fixup an incomplete string.
        if (literalExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
            return;
 
        if (!token.Text.Contains('{') && !token.Text.Contains('}'))
            return;
 
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        if (!syntaxFacts.SupportsConstantInterpolatedStrings(document.Project.ParseOptions!))
        {
            // If there is a const keyword, do not offer the refactoring (an interpolated string is not const)
            var declarator = literalExpression.FirstAncestorOrSelf<SyntaxNode>(syntaxFacts.IsVariableDeclarator);
            if (declarator != null)
            {
                var generator = SyntaxGenerator.GetGenerator(document);
                if (generator.GetModifiers(declarator).IsConst)
                    return;
            }
 
            // Attributes also only allow constant values.
            var attribute = literalExpression.FirstAncestorOrSelf<SyntaxNode>(syntaxFacts.IsAttribute);
            if (attribute != null)
                return;
        }
 
        context.RegisterRefactoring(
            CodeAction.Create(
                FeaturesResources.Convert_to_interpolated_string,
                _ => UpdateDocumentAsync(document, root, token),
                nameof(FeaturesResources.Convert_to_interpolated_string),
                CodeActionPriority.Low),
            literalExpression.Span);
    }
 
    private static string GetTextWithoutQuotes(string text, bool isVerbatim)
    {
        // Trim off an extra character (@ symbol) for verbatim strings
        var startIndex = isVerbatim ? 2 : 1;
        return text[startIndex..^1];
    }
 
    private static SyntaxNode CreateInterpolatedString(Document document, SyntaxNode literalExpression, bool isVerbatim)
    {
        var generator = SyntaxGenerator.GetGenerator(document);
        var text = literalExpression.GetFirstToken().Text;
        var valueText = literalExpression.GetFirstToken().ValueText;
        var newNode = generator.InterpolatedStringText(
            generator.InterpolatedStringTextToken(
                GetTextWithoutQuotes(text.Replace("{", "{{").Replace("}", "}}"), isVerbatim),
                valueText));
 
        return generator.InterpolatedStringExpression(
            generator.CreateInterpolatedStringStartToken(isVerbatim),
            [newNode],
            generator.CreateInterpolatedStringEndToken()).WithTriviaFrom(literalExpression);
    }
 
    private static Task<Document> UpdateDocumentAsync(Document document, SyntaxNode root, SyntaxToken token)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var literalExpression = token.GetRequiredParent();
        return Task.FromResult(document.WithSyntaxRoot(
            root.ReplaceNode(
                literalExpression,
                CreateInterpolatedString(document, literalExpression, syntaxFacts.IsVerbatimStringLiteral(token)))));
    }
}