File: ConvertToRawString\ConvertStringToRawStringCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.ConvertToRawString;
 
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToRawString), Shared]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed partial class ConvertStringToRawStringCodeRefactoringProvider() : SyntaxEditorBasedCodeRefactoringProvider
{
    private static readonly string[] s_kindToEquivalenceKeyMap;
 
    static ConvertStringToRawStringCodeRefactoringProvider()
    {
        var count = (int)ConvertToRawKind.ContainsEscapedEndOfLineCharacter * 2;
        s_kindToEquivalenceKeyMap = new string[count];
        for (var i = 0; i < count; i++)
            s_kindToEquivalenceKeyMap[i] = i.ToString(CultureInfo.InvariantCulture);
    }
 
    private static readonly ImmutableArray<IConvertStringProvider> s_convertStringProviders = [ConvertRegularStringToRawStringProvider.Instance, ConvertInterpolatedStringToRawStringProvider.Instance];
 
    protected override ImmutableArray<FixAllScope> SupportedFixAllScopes => AllFixAllScopes;
 
    private static bool CanConvert(
        ParsedDocument parsedDocument,
        ExpressionSyntax expression,
        SyntaxFormattingOptions formattingOptions,
        out CanConvertParams canConvertParams,
        [NotNullWhen(true)] out IConvertStringProvider? provider,
        CancellationToken cancellationToken)
    {
        foreach (var convertProvider in s_convertStringProviders)
        {
            if (convertProvider.CanConvert(parsedDocument, expression, formattingOptions, out canConvertParams, cancellationToken))
            {
                provider = convertProvider;
                return true;
            }
        }
 
        canConvertParams = default;
        provider = null;
        return false;
    }
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, span, cancellationToken) = context;
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var token = root.FindToken(span.Start);
        if (!context.Span.IntersectsWith(token.Span))
            return;
 
        if (token.Parent is not ExpressionSyntax parentExpression)
            return;
 
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
 
        if (!CanConvert(parsedDocument, parentExpression, formattingOptions, out var convertParams, out var provider, cancellationToken))
            return;
 
        var priority = convertParams.Priority;
 
        var kind = convertParams.ContainsEscapedEndOfLineCharacter
            ? ConvertToRawKind.ContainsEscapedEndOfLineCharacter
            : 0;
 
        if (convertParams.CanBeSingleLine)
        {
            context.RegisterRefactoring(
                CodeAction.Create(
                    CSharpFeaturesResources.Convert_to_raw_string,
                    cancellationToken => UpdateDocumentAsync(document, parentExpression, kind | ConvertToRawKind.SingleLine, provider, cancellationToken),
                    s_kindToEquivalenceKeyMap[(int)(kind | ConvertToRawKind.SingleLine)],
                    priority),
                token.Span);
        }
        else
        {
            context.RegisterRefactoring(
                CodeAction.Create(
                    CSharpFeaturesResources.Convert_to_raw_string,
                    cancellationToken => UpdateDocumentAsync(document, parentExpression, kind | ConvertToRawKind.MultiLineIndented, provider, cancellationToken),
                    s_kindToEquivalenceKeyMap[(int)(kind | ConvertToRawKind.MultiLineIndented)],
                    priority),
                token.Span);
 
            if (convertParams.CanBeMultiLineWithoutLeadingWhiteSpaces)
            {
                context.RegisterRefactoring(
                    CodeAction.Create(
                        CSharpFeaturesResources.without_leading_whitespace_may_change_semantics,
                        cancellationToken => UpdateDocumentAsync(document, parentExpression, kind | ConvertToRawKind.MultiLineWithoutLeadingWhitespace, provider, cancellationToken),
                        s_kindToEquivalenceKeyMap[(int)(kind | ConvertToRawKind.MultiLineWithoutLeadingWhitespace)],
                        priority),
                    token.Span);
            }
        }
    }
 
    private static async Task<Document> UpdateDocumentAsync(
        Document document,
        ExpressionSyntax expression,
        ConvertToRawKind kind,
        IConvertStringProvider provider,
        CancellationToken cancellationToken)
    {
        var options = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
        var replacement = provider.Convert(parsedDocument, expression, kind, options, cancellationToken);
        return document.WithSyntaxRoot(root.ReplaceNode(expression, replacement));
    }
 
    protected override async Task FixAllAsync(
        Document document,
        ImmutableArray<TextSpan> fixAllSpans,
        SyntaxEditor editor,
        string? equivalenceKey,
        CancellationToken cancellationToken)
    {
        // Get the kind to be fixed from the equivalenceKey for the FixAll operation
        Debug.Assert(equivalenceKey != null);
        var kind = (ConvertToRawKind)int.Parse(equivalenceKey, CultureInfo.InvariantCulture);
 
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
 
        foreach (var fixSpan in fixAllSpans)
        {
            var node = editor.OriginalRoot.FindNode(fixSpan);
            foreach (var expression in node.DepthFirstTraversalNodes().OfType<ExpressionSyntax>())
            {
                // Ensure we can convert the string literal
                if (!CanConvert(parsedDocument, expression, formattingOptions, out var canConvertParams, out var provider, cancellationToken))
                    continue;
 
                // If the user started on a string that didn't have an explicit \n in it, then don't update other string
                // literals that do.  Note: if they did start on a string with an explicit \n, then we should update all
                // other literals, regardless of whether they had an explicit \n or not.
                if ((kind & ConvertToRawKind.ContainsEscapedEndOfLineCharacter) == 0 && canConvertParams.ContainsEscapedEndOfLineCharacter)
                    continue;
 
                // Ensure we have a matching kind to fix for this literal.
                // Remove the end of line flag so we can trivially switch on the rest below.
                var hasMatchingKind = (kind & ~ConvertToRawKind.ContainsEscapedEndOfLineCharacter) switch
                {
                    ConvertToRawKind.SingleLine => canConvertParams.CanBeSingleLine,
                    ConvertToRawKind.MultiLineIndented => !canConvertParams.CanBeSingleLine,
                    // If we started with a multi-line string that we're changing semantics for.  Then any
                    // multi-line matches are something we can proceed with.  After all, we're updating all other
                    // ones that might change semantics, so we can def update the ones that won't change semantics.
                    ConvertToRawKind.MultiLineWithoutLeadingWhitespace =>
                        !canConvertParams.CanBeSingleLine || canConvertParams.CanBeMultiLineWithoutLeadingWhiteSpaces,
                    _ => throw ExceptionUtilities.UnexpectedValue(kind),
                };
 
                if (!hasMatchingKind)
                    continue;
 
                editor.ReplaceNode(
                    expression,
                    (current, _) =>
                    {
                        if (current is not ExpressionSyntax currentExpression)
                            return current;
 
                        var currentParsedDocument = parsedDocument.WithChangedRoot(
                            currentExpression.SyntaxTree.GetRoot(cancellationToken), cancellationToken);
                        var replacement = provider.Convert(currentParsedDocument, currentExpression, kind, formattingOptions, cancellationToken);
                        return replacement;
                    });
            }
        }
    }
}