Skip to content

Commit

Permalink
Merge pull request #48502 from louis-z/issue-48219
Browse files Browse the repository at this point in the history
Refactoring provider to convert regular string to interpolated string
  • Loading branch information
CyrusNajmabadi authored Oct 17, 2020
2 parents ebdaa28 + 05f7c19 commit 81b6a51
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.ConvertToInterpolatedString;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ConvertToInterpolatedString
{
public class ConvertRegularStringToInterpolatedStringTests : AbstractCSharpCodeActionTest
{
protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspace workspace, TestParameters parameters)
=> new ConvertRegularStringToInterpolatedStringRefactoringProvider();

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnRegularStringWithNoBraces()
{
await TestMissingInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = [||]""string"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestOnRegularStringWithBraces()
{
await TestInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = [||]""string {"";
}
}",
@"public class C
{
void M()
{
var v = $""string {{"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestOnRegularStringWithBracesAndEscapedCharacters()
{
await TestInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = [||]""string { \r\n \t"";
}
}",
@"public class C
{
void M()
{
var v = $""string {{ \r\n \t"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnInterpolatedString()
{
await TestMissingInRegularAndScriptAsync(
@"public class C
{
void M()
{
var i = 0;
var v = $[||]""string {i}"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestOnVerbatimStringWithBraces()
{
await TestInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = @[||]""string
}"";
}
}",
@"public class C
{
void M()
{
var v = $@""string
}}"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestOnVerbatimStringWithBracesAndEscapedQuotes()
{
await TestInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = @[||]""string """"foo""""
}"";
}
}",
@"public class C
{
void M()
{
var v = $@""string """"foo""""
}}"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnRegularStringWithBracesAssignedToConst()
{
await TestMissingInRegularAndScriptAsync(
@"public class C
{
void M()
{
const string v = [||]""string {"";
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnUnterminatedStringWithBraces()
{
await TestMissingInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v = [||]""string {
}
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnAttributeStringParameterWithBraces()
{
await TestMissingInRegularAndScriptAsync(
@"[System.Diagnostics.DebuggerDisplay([||]""FirstName={FirstName}, LastName={LastName}"")]
public class C
{
public string FirstName { get; set; }
public string LastName { get; set; }
}");
}

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)]
public async Task TestMissingOnRegularStringWithBracesAndCursorOutOfBounds()
{
await TestMissingInRegularAndScriptAsync(
@"public class C
{
void M()
{
var v [||]= ""string {"";
}
}");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
' 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.

Imports Microsoft.CodeAnalysis.CodeRefactorings
Imports Microsoft.CodeAnalysis.ConvertToInterpolatedString
Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings

Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.ConvertToInterpolatedString
Public Class ConvertRegularStringToInterpolatedStringTests
Inherits AbstractVisualBasicCodeActionTest

Protected Overrides Function CreateCodeRefactoringProvider(workspace As Workspace, parameters As TestParameters) As CodeRefactoringProvider
Return New ConvertRegularStringToInterpolatedStringRefactoringProvider()
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)>
Public Async Function TestMissingOnRegularStringWithNoBraces() As Task
Await TestMissingInRegularAndScriptAsync(
"
Public Class C
Sub M()
Dim v = [||]""string""
End Sub
End Class")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)>
Public Async Function TestOnRegularStringWithBraces() As Task
Await TestInRegularAndScriptAsync(
"
Public Class C
Sub M()
Dim v = [||]""string {""
End Sub
End Class",
"
Public Class C
Sub M()
Dim v = $""string {{""
End Sub
End Class")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)>
Public Async Function TestMissingOnInterpolatedString() As Task
Await TestMissingInRegularAndScriptAsync(
"
Public Class C
Sub M()
Dim i = 0;
Dim v = $[||]""string {i}""
End Sub
End Class")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)>
Public Async Function TestMissingOnRegularStringWithBracesAssignedToConst() As Task
Await TestMissingInRegularAndScriptAsync(
"
Public Class C
Sub M()
Const v = [||]""string {""
End Sub
End Class")
End Function
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp), Shared]
[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)]
internal class ConvertBetweenRegularAndVerbatimInterpolatedStringCodeRefactoringProvider
: AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider<InterpolatedStringExpressionSyntax>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp), Shared]
[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString)]
internal class ConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider
: AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider<LiteralExpressionSyntax>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageServices;
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 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(
new MyCodeAction(_ => UpdateDocumentAsync(document, root, token)),
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 newNode = generator.InterpolatedStringText(
generator.InterpolatedStringTextToken(
GetTextWithoutQuotes(text.Replace("{", "{{").Replace("}", "}}"),
isVerbatim)));

return generator.InterpolatedStringExpression(
generator.CreateInterpolatedStringStartToken(isVerbatim),
new[] { 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)))));
}

private class MyCodeAction : CodeAction.DocumentChangeAction
{
internal override CodeActionPriority Priority => CodeActionPriority.Low;

public MyCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument)
: base(FeaturesResources.Convert_to_interpolated_string, createChangedDocument)
{
}
}
}
}

0 comments on commit 81b6a51

Please sign in to comment.