-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #48502 from louis-z/issue-48219
Refactoring provider to convert regular string to interpolated string
- Loading branch information
Showing
5 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
181 changes: 181 additions & 0 deletions
181
...s/CSharpTest/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {""; | ||
} | ||
}"); | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
...ualBasicTest/ConvertToInterpolatedString/ConvertRegularStringToInterpolatedStringTests.vb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
...onvertToInterpolatedString/ConvertRegularStringToInterpolatedStringRefactoringProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
} | ||
} | ||
} | ||
} |