From d05c6c8c3b0d5161735848836aaa1e8b68fe6c08 Mon Sep 17 00:00:00 2001 From: Jonathon Marolf Date: Fri, 3 Jun 2016 16:21:43 -0700 Subject: [PATCH] Port from CSharpEssentials of Convert-To-Interpolated-String (#11415) * initial port from CSharpEssentials * responding to feedback. removing syntax rewriter and incorporating formatting rules into formatter * responding to cryus' feedback - general logic cleanup - moving methods to abstract base class --- .../Core/Portable/PublicAPI.Unshipped.txt | 2 + .../Portable/Syntax/SeparatedSyntaxList.cs | 10 + .../CSharpEditorServicesTest.csproj | 1 + .../ConvertToInterpolatedStringTests.cs | 527 ++++++++++++++++++ .../CrefCompletionProviderTests.cs | 37 +- src/EditorFeatures/TestUtilities/Traits.cs | 1 + .../BasicEditorServicesTest.vbproj | 1 + .../ConvertToInterpolatedStringTests.vb | 420 ++++++++++++++ .../CrefCompletionProviderTests.vb | 30 +- .../CSharp/Portable/CSharpFeatures.csproj | 1 + ...ToInterpolatedStringRefactoringProvider.cs | 18 + ...ToInterpolatedStringRefactoringProvider.cs | 250 +++++++++ .../PredefinedCodeRefactoringProviderNames.cs | 1 + src/Features/Core/Portable/Features.csproj | 1 + .../Portable/FeaturesResources.Designer.cs | 9 + .../Core/Portable/FeaturesResources.resx | 3 + .../VisualBasic/Portable/BasicFeatures.vbproj | 1 + ...ToInterpolatedStringRefactoringProvider.vb | 15 + ...SyntaxNodeExtensions.SingleLineRewriter.cs | 24 +- .../Extensions/SyntaxNodeExtensions.cs | 4 +- .../Rules/SuppressFormattingRule.cs | 7 + .../CSharpSyntaxFactsService.cs | 31 +- .../SyntaxFactsService/ISyntaxFactsService.cs | 8 +- .../RenameLocation.ReferenceProcessing.cs | 2 +- .../Portable/Extensions/SingleLineRewriter.vb | 24 +- .../Extensions/SyntaxNodeExtensions.vb | 4 +- .../VisualBasicSyntaxFactsService.vb | 30 +- 27 files changed, 1425 insertions(+), 37 deletions(-) create mode 100644 src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs create mode 100644 src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb create mode 100644 src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs create mode 100644 src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs create mode 100644 src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 58b6f0b9c3b..da45a7f9029 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -780,6 +780,8 @@ static Microsoft.CodeAnalysis.Semantics.UnaryAndBinaryOperationExtensions.GetSim static Microsoft.CodeAnalysis.Semantics.UnaryAndBinaryOperationExtensions.GetUnaryOperandKind(Microsoft.CodeAnalysis.Semantics.UnaryOperationKind kind) -> Microsoft.CodeAnalysis.Semantics.UnaryOperandKind static Microsoft.CodeAnalysis.Semantics.UnaryAndBinaryOperationExtensions.GetUnaryOperandKind(this Microsoft.CodeAnalysis.Semantics.IIncrementExpression increment) -> Microsoft.CodeAnalysis.Semantics.UnaryOperandKind static Microsoft.CodeAnalysis.Semantics.UnaryAndBinaryOperationExtensions.GetUnaryOperandKind(this Microsoft.CodeAnalysis.Semantics.IUnaryOperatorExpression unary) -> Microsoft.CodeAnalysis.Semantics.UnaryOperandKind +static Microsoft.CodeAnalysis.SeparatedSyntaxList.implicit operator Microsoft.CodeAnalysis.SeparatedSyntaxList(Microsoft.CodeAnalysis.SeparatedSyntaxList nodes) -> Microsoft.CodeAnalysis.SeparatedSyntaxList +static Microsoft.CodeAnalysis.SeparatedSyntaxList.implicit operator Microsoft.CodeAnalysis.SeparatedSyntaxList(Microsoft.CodeAnalysis.SeparatedSyntaxList nodes) -> Microsoft.CodeAnalysis.SeparatedSyntaxList static Microsoft.CodeAnalysis.SourceGeneratorExtensions.GenerateSource(this Microsoft.CodeAnalysis.Compilation compilation, System.Collections.Immutable.ImmutableArray generators, string path, bool writeToDisk, System.Threading.CancellationToken cancellationToken) -> System.Collections.Immutable.ImmutableArray virtual Microsoft.CodeAnalysis.Diagnostics.AnalysisContext.RegisterOperationAction(System.Action action, System.Collections.Immutable.ImmutableArray operationKinds) -> void virtual Microsoft.CodeAnalysis.Diagnostics.AnalysisContext.RegisterOperationBlockAction(System.Action action) -> void diff --git a/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs b/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs index 41a958ce95c..f02042e5bff 100644 --- a/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs +++ b/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs @@ -573,5 +573,15 @@ IEnumerator IEnumerable.GetEnumerator() return SpecializedCollections.EmptyEnumerator(); } + + public static implicit operator SeparatedSyntaxList(SeparatedSyntaxList nodes) + { + return new SeparatedSyntaxList(nodes._list); + } + + public static implicit operator SeparatedSyntaxList(SeparatedSyntaxList nodes) + { + return new SeparatedSyntaxList(nodes._list); + } } } diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj index 8ecca19b3b2..fa186b39778 100644 --- a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj +++ b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj @@ -145,6 +145,7 @@ + diff --git a/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs new file mode 100644 index 00000000000..77ff4895698 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs @@ -0,0 +1,527 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.ConvertToInterpolatedString +{ + public class ConvertToInterpolatedStringTests : AbstractCSharpCodeActionTest + { + protected override object CreateCodeRefactoringProvider(Workspace workspace) => + new ConvertToInterpolatedStringRefactoringProvider(); + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestSingleItemSubstitution() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", 1)|]; + } + }", +@"using System; +class T +{ + void M() + { + var a = $""{1}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{1}{2}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{2}{3}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering2() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{2}{1}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{3}{2}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering3() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{0}{0}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{1}{1}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOutsideRange() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{4}{5}{6}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{4}{5}{6}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemDoNotHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{1}{2}"", 0.5, ""Hello"", 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{0.5}{""Hello""}{3}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemWithSyntaxErrorDoesHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", new object)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{ (object)new object}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemWithoutSyntaxErrorDoesNotHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", new object())|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{new object()}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestParenthesisAddedForTernaryExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", true ? ""Yes"" : ""No"")|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : ""No"")}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestDoesNotAddDoubleParenthesisForTernaryExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", (true ? ""Yes"" : ""No""))|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : ""No"")}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestMultiLineExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format( + ""{0}"", + true + ? ""Yes"" + : false as object)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : false as object)}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + Decimal pricePerOunce = 17.36m; + String s = [|String.Format(""The current price is { 0:C2} per ounce."", + pricePerOunce)|]; + } +}", +@"using System; +class T +{ + void M() + { + Decimal pricePerOunce = 17.36m; + String s = $""The current price is { pricePerOunce:C2} per ounce.""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers2() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + string s = [|String.Format(""It is now {0:d} at {0:t}"", DateTime.Now)|]; + } +}", +@"using System; +class T +{ + void M() + { + string s = $""It is now {DateTime.Now:d} at {DateTime.Now:t}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers3() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + int[] years = { 2013, 2014, 2015 }; + int[] population = { 1025632, 1105967, 1148203 }; + String s = String.Format(""{0,6} {1,15}\n\n"", ""Year"", ""Population""); + for (int index = 0; index < years.Length; index++) + s += [|String.Format(""{0,6} {1,15:N0}\n"", + years[index], population[index])|]; + } +}", +@"using System; +class T +{ + void M() + { + int[] years = { 2013, 2014, 2015 }; + int[] population = { 1025632, 1105967, 1148203 }; + String s = String.Format(""{0,6} {1,15}\n\n"", ""Year"", ""Population""); + for (int index = 0; index < years.Length; index++) + s += $""{years[index],6} {population[index],15:N0}\n""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers4() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|String.Format(""{ 0,-10:C}"", 126347.89m)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{ 126347.89m,-10:C}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers5() + { + await TestAsync( +@"using System; +public class T +{ + public static void M() + { + Tuple[] cities = + { Tuple.Create(""Los Angeles"", new DateTime(1940, 1, 1), 1504277, + new DateTime(1950, 1, 1), 1970358), + Tuple.Create(""New York"", new DateTime(1940, 1, 1), 7454995, + new DateTime(1950, 1, 1), 7891957), + Tuple.Create(""Chicago"", new DateTime(1940, 1, 1), 3396808, + new DateTime(1950, 1, 1), 3620962), + Tuple.Create(""Detroit"", new DateTime(1940, 1, 1), 1623452, + new DateTime(1950, 1, 1), 1849568) }; + string output; + foreach (var city in cities) + { + output = [|String.Format(""{0,-12}{1,8:yyyy}{2,12:N0}{3,8:yyyy}{4,12:N0}{5,14:P1}"", + city.Item1, city.Item2, city.Item3, city.Item4, city.Item5, + (city.Item5 - city.Item3) / (double)city.Item3)|]; + } + } +}", +@"using System; +public class T +{ + public static void M() + { + Tuple[] cities = + { Tuple.Create(""Los Angeles"", new DateTime(1940, 1, 1), 1504277, + new DateTime(1950, 1, 1), 1970358), + Tuple.Create(""New York"", new DateTime(1940, 1, 1), 7454995, + new DateTime(1950, 1, 1), 7891957), + Tuple.Create(""Chicago"", new DateTime(1940, 1, 1), 3396808, + new DateTime(1950, 1, 1), 3620962), + Tuple.Create(""Detroit"", new DateTime(1940, 1, 1), 1623452, + new DateTime(1950, 1, 1), 1849568) }; + string output; + foreach (var city in cities) + { + output = $""{city.Item1,-12}{city.Item2,8:yyyy}{city.Item3,12:N0}{city.Item4,8:yyyy}{city.Item5,12:N0}{(city.Item5 - city.Item3) / (double)city.Item3,14:P1}""; + } + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers6() + { + await TestAsync( +@"using System; +public class T +{ + public static void M() + { + short[] values = { Int16.MinValue, -27, 0, 1042, Int16.MaxValue }; + foreach (short value in values) + { + string formatString = [|String.Format(""{0,10:G}: {0,10:X}"", value)|]; + } + } +}", +@"using System; +public class T +{ + public static void M() + { + short[] values = { Int16.MinValue, -27, 0, 1042, Int16.MaxValue }; + foreach (short value in values) + { + string formatString = $""{value,10:G}: {value,10:X}""; + } + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestVerbatimStringLiteral() + { + await TestAsync( +@"using System; + +public class T +{ + public static void M() + { + int value1 = 16932; + int value2 = 15421; + string result = [|string.Format(@"" + {0,10} ({0,8:X8}) +And {1,10} ({1,8:X8}) + = {2,10} ({2,8:X8})"", + value1, value2, value1 & value2)|]; + } +}", +@"using System; + +public class T +{ + public static void M() + { + int value1 = 16932; + int value2 = 15421; + string result = $@"" + {value1,10} ({value1,8:X8}) +And {value2,10} ({value2,8:X8}) + = {value1 & value2,10} ({value1 & value2,8:X8})""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatWithParams() + { + await TestMissingAsync( +@"using System; + +public class T +{ + public static void M() + { + DateTime date1 = new DateTime(2009, 7, 1); + TimeSpan hiTime = new TimeSpan(14, 17, 32); + decimal hiTemp = 62.1m; + TimeSpan loTime = new TimeSpan(3, 16, 10); + decimal loTemp = 54.8m; + + string result = [|String.Format(@""Temperature on {0:d}: + {1,11}: {2} degrees (hi) + {3,11}: {4} degrees (lo)"", + new object[] { date1, hiTime, hiTemp, loTime, loTemp })|]; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestInvalidInteger() + { + await TestAsync( +@"using System; + +public class T +{ + public static void M() + { + string result = [|String.Format(""{0L}"", 5)|]; + } +}", +@"using System; + +public class T +{ + public static void M() + { + string result = $""{5}""; + } +}"); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs index 384bb2ea19b..d0b30112005 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs @@ -455,11 +455,6 @@ public bool ContainsInMemberBody(SyntaxNode node, TextSpan span) throw new NotImplementedException(); } - public SyntaxNode ConvertToSingleLine(SyntaxNode node) - { - throw new NotImplementedException(); - } - public SyntaxToken FindTokenOnLeftOfPosition(SyntaxNode node, int position, bool includeSkipped = true, bool includeDirectives = false, bool includeDocumentationComments = false) { throw new NotImplementedException(); @@ -807,7 +802,7 @@ public bool IsStartOfUnicodeEscapeSequence(char c) throw new NotImplementedException(); } - public bool IsStringLiteral(SyntaxToken token) + public bool IsStringLiteralOrInterpolatedStringLiteral(SyntaxToken token) { throw new NotImplementedException(); } @@ -961,6 +956,36 @@ public bool IsOperandOfIncrementExpression(SyntaxNode node) { throw new NotImplementedException(); } + + public bool IsNumericLiteralExpression(SyntaxNode node) + { + throw new NotImplementedException(); + } + + public SyntaxNode GetExpressionOfInterpolation(SyntaxNode node) + { + throw new NotImplementedException(); + } + + public SyntaxList GetContentsOfInterpolatedString(SyntaxNode interpolatedString) + { + throw new NotImplementedException(); + } + + public bool IsStringLiteral(SyntaxToken token) + { + throw new NotImplementedException(); + } + + public SeparatedSyntaxList GetArgumentsForInvocationExpression(SyntaxNode invocationExpression) + { + throw new NotImplementedException(); + } + + public SyntaxNode ConvertToSingleLine(SyntaxNode node, bool useElasticTrivia = false) + { + throw new NotImplementedException(); + } } } } diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index 1fc5c58682c..f26f5a91007 100644 --- a/src/EditorFeatures/TestUtilities/Traits.cs +++ b/src/EditorFeatures/TestUtilities/Traits.cs @@ -37,6 +37,7 @@ public static class Features public const string CodeActionsChangeToAsync = "CodeActions.ChangeToAsync"; public const string CodeActionsChangeToIEnumerable = "CodeActions.ChangeToIEnumerable"; public const string CodeActionsChangeToYield = "CodeActions.ChangeToYield"; + public const string CodeActionsConvertToInterpolatedString = "CodeActions.ConvertToInterpolatedString"; public const string CodeActionsConvertToIterator = "CodeActions.CodeActionsConvertToIterator"; public const string CodeActionsCorrectExitContinue = "CodeActions.CorrectExitContinue"; public const string CodeActionsCorrectFunctionReturnType = "CodeActions.CorrectFunctionReturnType"; diff --git a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj index b67be1915ea..5abbec5950e 100644 --- a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj +++ b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj @@ -126,6 +126,7 @@ + diff --git a/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb new file mode 100644 index 00000000000..2715ef2fd44 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb @@ -0,0 +1,420 @@ +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings + +Public Class ConvertToInterpolatedStringTests + Inherits AbstractVisualBasicCodeActionTest + + Protected Overrides Function CreateCodeRefactoringProvider(workspace As Workspace) As Object + Return New ConvertToInterpolatedStringRefactoringProvider() + End Function + + + Public Async Function TestSingleItemSubstitution() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", 1)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{2}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{2}{1}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{3}{2}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering3() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{0}{0}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{1}{1}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOutsideRange() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{4}{5}{6}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{4}{5}{6}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemDoNotHaveCast() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 0.5, "Hello", 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{0.5}{"Hello"}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemWithoutSyntaxErrorDoesNotHaveCast() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 0.5, "Hello", 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{0.5}{"Hello"}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestPreserveParenthesis() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", (New Object))|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{(New Object)}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestMultiLineExpression() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", If(True, + "Yes", + TryCast(False, Object)))|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{If(True, + "Yes", + TryCast(False, Object))}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers() As Task + Dim text = +Imports System +Module T + Sub M() + Dim pricePerOunce As Decimal = 17.36 + Dim s = [|String.Format("The current price Is {0:C2} per ounce.", + pricePerOunce)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim pricePerOunce As Decimal = 17.36 + Dim s = $"The current price Is {pricePerOunce:C2} per ounce." + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim s = [|String.Format("It Is now {0:d} at {0:T}", DateTime.Now)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim s = $"It Is now {DateTime.Now:d} at {DateTime.Now:T}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers3() As Task + Dim text = +Imports System +Module T + Sub M() + Dim years As Integer() = {2013, 2014, 2015} + Dim population As Integer() = {1025632, 1105967, 1148203} + Dim s = String.Format("{0,6} {1,15}\n\n", "Year", "Population") + For index = 0 To years.Length - 1 + s += [|String.Format("{0, 6} {1, 15: N0}\n", + years(index), population(index))|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim years As Integer() = {2013, 2014, 2015} + Dim population As Integer() = {1025632, 1105967, 1148203} + Dim s = String.Format("{0,6} {1,15}\n\n", "Year", "Population") + For index = 0 To years.Length - 1 + s += $"{years(index),6} {population(index),15: N0}\n" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers4() As Task + Dim text = +Imports System +Module T + Sub M() + Dim s = [|String.Format("{0,-10:C}", 126347.89)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim s = $"{126347.89,-10:C}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers5() As Task + Dim text = +Imports System +Module T + Sub M() + Dim cities As Tuple(Of String, DateTime, Integer, DateTime, Integer)() = + {Tuple.Create("Los Angeles", New DateTime(1940, 1, 1), 1504277, + New DateTime(1950, 1, 1), 1970358), + Tuple.Create("New York", New DateTime(1940, 1, 1), 7454995, + New DateTime(1950, 1, 1), 7891957), + Tuple.Create("Chicago", New DateTime(1940, 1, 1), 3396808, + New DateTime(1950, 1, 1), 3620962), + Tuple.Create("Detroit", New DateTime(1940, 1, 1), 1623452, + New DateTime(1950, 1, 1), 1849568)} + Dim output As String + For Each city In cities + output = [|String.Format("{0,-12}{1,8:yyyy}{2,12:N0}{3,8:yyyy}{4,12:N0}{5,14:P1}", + city.Item1, city.Item2, city.Item3, city.Item4, city.Item5, + (city.Item5 - city.Item3) / CType(city.Item3, Double))|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim cities As Tuple(Of String, DateTime, Integer, DateTime, Integer)() = + {Tuple.Create("Los Angeles", New DateTime(1940, 1, 1), 1504277, + New DateTime(1950, 1, 1), 1970358), + Tuple.Create("New York", New DateTime(1940, 1, 1), 7454995, + New DateTime(1950, 1, 1), 7891957), + Tuple.Create("Chicago", New DateTime(1940, 1, 1), 3396808, + New DateTime(1950, 1, 1), 3620962), + Tuple.Create("Detroit", New DateTime(1940, 1, 1), 1623452, + New DateTime(1950, 1, 1), 1849568)} + Dim output As String + For Each city In cities + output = $"{city.Item1,-12}{city.Item2,8:yyyy}{city.Item3,12:N0}{city.Item4,8:yyyy}{city.Item5,12:N0}{(city.Item5 - city.Item3) / CType(city.Item3, Double),14:P1}" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers6() As Task + Dim text = +Imports System +Module T + Sub M() + Dim values As Short() = {Int16.MaxValue, -27, 0, 1042, Int16.MaxValue} + For Each value In values + Dim s = [|String.Format("{0,10:G}: {0,10:X}", value)|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim values As Short() = {Int16.MaxValue, -27, 0, 1042, Int16.MaxValue} + For Each value In values + Dim s = $"{value,10:G}: {value,10:X}" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestMultilineStringLiteral2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim value1 = 16932 + Dim value2 = 15421 + Dim result = [|String.Format(" + {0,10} ({0,8:X8}) +And {1,10} ({1,8:X8}) + = {2,10} ({2,8:X8})", + value1, value2, value1 And value2)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim value1 = 16932 + Dim value2 = 15421 + Dim result = $" + {value1,10} ({value1,8:X8}) +And {value2,10} ({value2,8:X8}) + = {value1 And value2,10} ({value1 And value2,8:X8})" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestParamsArray() As Task + Dim text = +Imports System +Module T + Sub M(args As String()) + Dim s = [|String.Format("{0}", args)|] + End Sub +End Module.ConvertTestSourceTag() + Await TestMissingAsync(text) + End Function +End Class diff --git a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb index d40810ea07c..9cbc3ebfb3b 100644 --- a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb @@ -454,10 +454,6 @@ End Class]]>.Value.NormalizeLineEndings() Throw New NotImplementedException() End Function - Public Function ConvertToSingleLine(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine - Throw New NotImplementedException() - End Function - Public Function FindTokenOnLeftOfPosition(node As SyntaxNode, position As Integer, Optional includeSkipped As Boolean = True, Optional includeDirectives As Boolean = False, Optional includeDocumentationComments As Boolean = False) As SyntaxToken Implements ISyntaxFactsService.FindTokenOnLeftOfPosition Throw New NotImplementedException() End Function @@ -733,7 +729,7 @@ End Class]]>.Value.NormalizeLineEndings() Throw New NotImplementedException() End Function - Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Public Function IsStringLiteralOrInterpolatedStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteralOrInterpolatedStringLiteral Throw New NotImplementedException() End Function @@ -852,6 +848,30 @@ End Class]]>.Value.NormalizeLineEndings() Public Function IsOperandOfIncrementOrDecrementExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsOperandOfIncrementOrDecrementExpression Throw New NotImplementedException() End Function + + Public Function IsNumericLiteralExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsNumericLiteralExpression + Throw New NotImplementedException() + End Function + + Public Function GetExpressionOfInterpolation(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.GetExpressionOfInterpolation + Throw New NotImplementedException() + End Function + + Public Function GetContentsOfInterpolatedString(interpolatedString As SyntaxNode) As SyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetContentsOfInterpolatedString + Throw New NotImplementedException() + End Function + + Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Throw New NotImplementedException() + End Function + + Public Function GetArgumentsForInvocationExpression(invocationExpression As SyntaxNode) As SeparatedSyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetArgumentsForInvocationExpression + Throw New NotImplementedException() + End Function + + Public Function ConvertToSingleLine(node As SyntaxNode, Optional useElasticTrivia As Boolean = False) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine + Throw New NotImplementedException() + End Function End Class End Class End Namespace diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index ddc1d128a3d..cc33c05ca21 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -92,6 +92,7 @@ + diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs new file mode 100644 index 00000000000..2cf9375a0d4 --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] + internal partial class ConvertToInterpolatedStringRefactoringProvider : + AbstractConvertToInterpolatedStringRefactoringProvider + { + protected override SyntaxNode GetInterpolatedString(string text) => + ParseExpression("$" + text) as InterpolatedStringExpressionSyntax; + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs new file mode 100644 index 00000000000..f5fa5236f34 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Formatting; + +namespace Microsoft.CodeAnalysis.CodeRefactorings +{ + internal abstract class AbstractConvertToInterpolatedStringRefactoringProvider : CodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TInvocationExpressionSyntax : TExpressionSyntax + where TArgumentSyntax : SyntaxNode + where TLiteralExpressionSyntax : SyntaxNode + { + protected abstract SyntaxNode GetInterpolatedString(string text); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + var stringType = semanticModel.Compilation.GetSpecialType(SpecialType.System_String); + if (stringType == null) + { + return; + } + + var formatMethods = stringType + .GetMembers("Format") + .OfType() + .Where(ShouldIncludeFormatMethod) + .ToImmutableArray(); + + if (formatMethods.Length == 0) + { + return; + } + + var syntaxFactsService = context.Document.GetLanguageService(); + if (syntaxFactsService == null) + { + return; + } + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + TInvocationExpressionSyntax invocation; + ISymbol invocationSymbol; + if (TryFindInvocation(context.Span, root, semanticModel, formatMethods, syntaxFactsService, context.CancellationToken, out invocation, out invocationSymbol) && + IsArgumentListCorrect(syntaxFactsService.GetArgumentsForInvocationExpression(invocation), invocationSymbol, formatMethods, semanticModel, syntaxFactsService, context.CancellationToken)) + { + context.RegisterRefactoring( + new ConvertToInterpolatedStringCodeAction( + FeaturesResources.ConvertToInterpolatedString, + c => CreateInterpolatedString(invocation, context.Document, syntaxFactsService, c))); + } + } + + private bool TryFindInvocation( + TextSpan span, + SyntaxNode root, + SemanticModel semanticModel, + ImmutableArray formatMethods, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken, + out TInvocationExpressionSyntax invocation, + out ISymbol invocationSymbol) + { + invocationSymbol = null; + invocation = root.FindNode(span, getInnermostNodeForTie: true)?.FirstAncestorOrSelf(); + while (invocation != null) + { + var arguments = syntaxFactsService.GetArgumentsForInvocationExpression(invocation); + if (arguments.Count >= 2) + { + var firstArgumentExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + if (firstArgumentExpression != null && syntaxFactsService.IsStringLiteral(firstArgumentExpression.GetFirstToken())) + { + invocationSymbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (formatMethods.Contains(invocationSymbol)) + { + break; + } + } + } + + invocation = invocation.Parent?.FirstAncestorOrSelf(); + } + + return invocation != null; + } + + private bool IsArgumentListCorrect( + SeparatedSyntaxList? nullableArguments, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken) + { + var arguments = nullableArguments.Value; + var firstExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + if (arguments.Count >= 2 && + firstExpression != null && + syntaxFactsService.IsStringLiteral(firstExpression.GetFirstToken())) + { + // We do not want to substitute the expression if it is being passed to params array argument + // Example: + // string[] args; + // String.Format("{0}{1}{2}", args); + return IsArgumentListNotPassingArrayToParams( + syntaxFactsService.GetExpressionOfArgument(arguments[1]), + invocationSymbol, + formatMethods, + semanticModel, + cancellationToken); + } + + return false; + } + + + private async Task CreateInterpolatedString( + TInvocationExpressionSyntax invocation, + Document document, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var arguments = syntaxFactsService.GetArgumentsForInvocationExpression(invocation); + var literalExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + var text = literalExpression.GetFirstToken().ToString(); + var syntaxGenerator = document.Project.LanguageServices.GetService(); + var expandedArguments = GetExpandedArguments(semanticModel, arguments, syntaxGenerator, syntaxFactsService); + var interpolatedString = GetInterpolatedString(text); + var newInterpolatedString = VisitArguments(expandedArguments, interpolatedString, syntaxFactsService); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(invocation, newInterpolatedString.WithTriviaFrom(invocation)); + return document.WithSyntaxRoot(newRoot); + } + + private ImmutableArray GetExpandedArguments( + SemanticModel semanticModel, + SeparatedSyntaxList arguments, + SyntaxGenerator syntaxGenerator, + ISyntaxFactsService syntaxFactsService) + { + var builder = ImmutableArray.CreateBuilder(); + for (int i = 1; i < arguments.Count; i++) + { + var argumentExpression = syntaxFactsService.GetExpressionOfArgument(arguments[i]); + var convertedType = semanticModel.GetTypeInfo(argumentExpression).ConvertedType; + if (convertedType == null) + { + builder.Add(syntaxFactsService.Parenthesize(argumentExpression) as TExpressionSyntax); + } + else + { + var castExpression = syntaxGenerator.CastExpression(convertedType, syntaxFactsService.Parenthesize(argumentExpression)).WithAdditionalAnnotations(Simplifier.Annotation); + builder.Add(castExpression as TExpressionSyntax); + } + } + + var expandedArguments = builder.ToImmutable(); + return expandedArguments; + } + + private SyntaxNode VisitArguments( + ImmutableArray expandedArguments, + SyntaxNode interpolatedString, + ISyntaxFactsService syntaxFactsService) + { + return interpolatedString.ReplaceNodes(syntaxFactsService.GetContentsOfInterpolatedString(interpolatedString), (oldNode, newNode) => + { + var interpolationSyntaxNode = newNode; + if (interpolationSyntaxNode != null) + { + var literalExpression = syntaxFactsService.GetExpressionOfInterpolation(interpolationSyntaxNode) as TLiteralExpressionSyntax; + if (literalExpression != null && syntaxFactsService.IsNumericLiteralExpression(literalExpression)) + { + int index; + + if (int.TryParse(literalExpression.GetFirstToken().ValueText, out index)) + { + if (index >= 0 && index < expandedArguments.Length) + { + return interpolationSyntaxNode.ReplaceNode( + syntaxFactsService.GetExpressionOfInterpolation(interpolationSyntaxNode), + syntaxFactsService.ConvertToSingleLine(expandedArguments[index], useElasticTrivia: true).WithAdditionalAnnotations(Formatter.Annotation)); + } + } + } + } + + return newNode; + }); + } + + private static bool ShouldIncludeFormatMethod(IMethodSymbol methodSymbol) + { + if (!methodSymbol.IsStatic) + { + return false; + } + + if (methodSymbol.Parameters.Length == 0) + { + return false; + } + + var firstParameter = methodSymbol.Parameters[0]; + if (firstParameter?.Name != "format") + { + return false; + } + + return true; + } + + private static bool IsArgumentListNotPassingArrayToParams( + SyntaxNode expression, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var formatMethodsAcceptingParamsArray = formatMethods + .Where(x => x.Parameters.Length > 1 && x.Parameters[1].Type.Kind == SymbolKind.ArrayType); + if (formatMethodsAcceptingParamsArray.Contains(invocationSymbol)) + { + return semanticModel.GetTypeInfo(expression, cancellationToken).Type?.Kind != SymbolKind.ArrayType; + } + + return true; + } + + private class ConvertToInterpolatedStringCodeAction : CodeAction.DocumentChangeAction + { + public ConvertToInterpolatedStringCodeAction(string title, Func> createChangedDocument) : + base(title, createChangedDocument) + { + } + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index f36cae8e6ed..a4da83de80b 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -17,5 +17,6 @@ internal static class PredefinedCodeRefactoringProviderNames public const string InvertIf = "Invert If Code Action Provider"; public const string MoveDeclarationNearReference = "Move Declaration Near Reference Code Action Provider"; public const string SimplifyLambda = "Simplify Lambda Code Action Provider"; + public const string ConvertToInterpolatedString = "Convert To Interpolated String Code Action Provider"; } } diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 78f02508c43..ee309cb1f1b 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -146,6 +146,7 @@ + diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 5744a5a490b..4c7efa8bc56 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -619,6 +619,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Convert to interpolated string. + /// + internal static string ConvertToInterpolatedString { + get { + return ResourceManager.GetString("ConvertToInterpolatedString", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not extract interface: The selection is not inside a class/interface/struct.. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index e0f9a62c051..7697d82e53a 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1031,4 +1031,7 @@ This version used in: {2} Property cannot safely be replaced with a method call + + Convert to interpolated string + \ No newline at end of file diff --git a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj index fde63f0a791..5bd6d9fa290 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -126,6 +126,7 @@ + diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb new file mode 100644 index 00000000000..5095a1ece4f --- /dev/null +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb @@ -0,0 +1,15 @@ +Imports System.Composition +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory + + +Partial Friend Class ConvertToInterpolatedStringRefactoringProvider + Inherits AbstractConvertToInterpolatedStringRefactoringProvider(Of InvocationExpressionSyntax, ExpressionSyntax, ArgumentSyntax, LiteralExpressionSyntax) + + Protected Overrides Function GetInterpolatedString(text As String) As SyntaxNode + Return TryCast(ParseExpression("$" + text), InterpolatedStringExpressionSyntax) + End Function +End Class diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs index 778903afcc9..4d247970c25 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs @@ -12,8 +12,14 @@ internal partial class SyntaxNodeExtensions { internal class SingleLineRewriter : CSharpSyntaxRewriter { + private bool useElasticTrivia; private bool _lastTokenEndedInWhitespace; + public SingleLineRewriter(bool useElasticTrivia) + { + this.useElasticTrivia = useElasticTrivia; + } + public override SyntaxToken VisitToken(SyntaxToken token) { if (_lastTokenEndedInWhitespace) @@ -22,12 +28,26 @@ public override SyntaxToken VisitToken(SyntaxToken token) } else if (token.LeadingTrivia.Count > 0) { - token = token.WithLeadingTrivia(SyntaxFactory.Space); + if (useElasticTrivia) + { + token = token.WithLeadingTrivia(SyntaxFactory.ElasticSpace); + } + else + { + token = token.WithLeadingTrivia(SyntaxFactory.Space); + } } if (token.TrailingTrivia.Count > 0) { - token = token.WithTrailingTrivia(SyntaxFactory.Space); + if (useElasticTrivia) + { + token = token.WithTrailingTrivia(SyntaxFactory.ElasticSpace); + } + else + { + token = token.WithTrailingTrivia(SyntaxFactory.Space); + } _lastTokenEndedInWhitespace = true; } else diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs index f389e67875a..92a4beab7de 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs @@ -372,7 +372,7 @@ public static bool IsReturnableConstruct(this SyntaxNode node) return node.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia); } - public static TNode ConvertToSingleLine(this TNode node) + public static TNode ConvertToSingleLine(this TNode node, bool useElasticTrivia = false) where TNode : SyntaxNode { if (node == null) @@ -380,7 +380,7 @@ public static TNode ConvertToSingleLine(this TNode node) return node; } - var rewriter = new SingleLineRewriter(); + var rewriter = new SingleLineRewriter(useElasticTrivia); return (TNode)rewriter.Visit(node); } diff --git a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs index e79d5a430ce..8fbc3f0a692 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Options; +using System.Linq; namespace Microsoft.CodeAnalysis.CSharp.Formatting { @@ -174,6 +175,12 @@ private void AddSpecificNodesSuppressOperations(List list, Sy AddSuppressWrappingIfOnSingleLineOperation(list, finallyClause.FinallyKeyword, finallyClause.Block.CloseBraceToken); } } + + var interpolatedStringExpression = node as InterpolatedStringExpressionSyntax; + if (interpolatedStringExpression != null) + { + AddSuppressWrappingIfOnSingleLineOperation(list, interpolatedStringExpression.StringStartToken, interpolatedStringExpression.StringEndToken); + } } private void AddStatementExceptBlockSuppressOperations(List list, SyntaxNode node) diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 26b623afc95..dcf8f2db9fe 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -462,11 +462,16 @@ public bool IsLiteral(SyntaxToken token) return false; } - public bool IsStringLiteral(SyntaxToken token) + public bool IsStringLiteralOrInterpolatedStringLiteral(SyntaxToken token) { return token.IsKind(SyntaxKind.StringLiteralToken, SyntaxKind.InterpolatedStringTextToken); } + public bool IsNumericLiteralExpression(SyntaxNode node) + { + return node?.IsKind(SyntaxKind.NumericLiteralExpression) == true; + } + public bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token, SyntaxNode parent) { var typedToken = token; @@ -566,6 +571,11 @@ public SyntaxNode GetExpressionOfConditionalMemberAccessExpression(SyntaxNode no return (node as ConditionalAccessExpressionSyntax)?.Expression; } + public SyntaxNode GetExpressionOfInterpolation(SyntaxNode node) + { + return (node as InterpolationSyntax)?.Expression; + } + public bool IsInStaticContext(SyntaxNode node) { return node.IsInStaticContext(); @@ -671,9 +681,9 @@ public bool IsElementAccessExpression(SyntaxNode node) return node.Kind() == SyntaxKind.ElementAccessExpression; } - public SyntaxNode ConvertToSingleLine(SyntaxNode node) + public SyntaxNode ConvertToSingleLine(SyntaxNode node, bool useElasticTrivia = false) { - return node.ConvertToSingleLine(); + return node.ConvertToSingleLine(useElasticTrivia); } public SyntaxToken ToIdentifierToken(string name) @@ -1644,5 +1654,20 @@ public bool IsOperandOfIncrementOrDecrementExpression(SyntaxNode node) { return IsOperandOfIncrementExpression(node) || IsOperandOfDecrementExpression(node); } + + public SyntaxList GetContentsOfInterpolatedString(SyntaxNode interpolatedString) + { + return ((interpolatedString as InterpolatedStringExpressionSyntax)?.Contents).Value; + } + + public bool IsStringLiteral(SyntaxToken token) + { + return token.IsKind(SyntaxKind.StringLiteralToken); + } + + public SeparatedSyntaxList GetArgumentsForInvocationExpression(SyntaxNode invocationExpression) + { + return ((invocationExpression as InvocationExpressionSyntax)?.ArgumentList.Arguments).Value; + } } } diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index 201ea939af4..aa30d07fea9 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -27,7 +27,9 @@ internal interface ISyntaxFactsService : ILanguageService bool IsPreprocessorKeyword(SyntaxToken token); bool IsHashToken(SyntaxToken token); bool IsLiteral(SyntaxToken token); + bool IsStringLiteralOrInterpolatedStringLiteral(SyntaxToken token); bool IsStringLiteral(SyntaxToken token); + bool IsNumericLiteralExpression(SyntaxNode node); bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token, SyntaxNode parent); bool IsTypeNamedDynamic(SyntaxToken token, SyntaxNode parent); @@ -71,12 +73,14 @@ internal interface ISyntaxFactsService : ILanguageService SyntaxNode GetExpressionOfMemberAccessExpression(SyntaxNode node); SyntaxNode GetExpressionOfConditionalMemberAccessExpression(SyntaxNode node); SyntaxNode GetExpressionOfArgument(SyntaxNode node); + SyntaxNode GetExpressionOfInterpolation(SyntaxNode node); bool IsConditionalMemberAccessExpression(SyntaxNode node); SyntaxNode GetNameOfAttribute(SyntaxNode node); SyntaxToken GetIdentifierOfGenericName(SyntaxNode node); RefKind GetRefKindOfArgument(SyntaxNode node); void GetNameAndArityOfSimpleName(SyntaxNode node, out string name, out int arity); - + SyntaxList GetContentsOfInterpolatedString(SyntaxNode interpolatedString); + SeparatedSyntaxList GetArgumentsForInvocationExpression(SyntaxNode invocationExpression); bool IsUsingDirectiveName(SyntaxNode node); bool IsGenericName(SyntaxNode node); @@ -139,7 +143,7 @@ internal interface ISyntaxFactsService : ILanguageService SyntaxNode Parenthesize(SyntaxNode expression, bool includeElasticTrivia = true); - SyntaxNode ConvertToSingleLine(SyntaxNode node); + SyntaxNode ConvertToSingleLine(SyntaxNode node, bool useElasticTrivia = false); SyntaxToken ToIdentifierToken(string name); diff --git a/src/Workspaces/Core/Portable/Rename/RenameLocation.ReferenceProcessing.cs b/src/Workspaces/Core/Portable/Rename/RenameLocation.ReferenceProcessing.cs index 4d89abef8ae..eee0fe90c13 100644 --- a/src/Workspaces/Core/Portable/Rename/RenameLocation.ReferenceProcessing.cs +++ b/src/Workspaces/Core/Portable/Rename/RenameLocation.ReferenceProcessing.cs @@ -416,7 +416,7 @@ private static async Task AddLocationsToRenameInStringsAsync(Document document, var renameStringsAndPositions = root .DescendantTokens() - .Where(t => syntaxFactsService.IsStringLiteral(t) && t.Span.Length >= renameTextLength) + .Where(t => syntaxFactsService.IsStringLiteralOrInterpolatedStringLiteral(t) && t.Span.Length >= renameTextLength) .Select(t => Tuple.Create(t.ToString(), t.Span.Start, t.Span)); if (renameStringsAndPositions.Any()) diff --git a/src/Workspaces/VisualBasic/Portable/Extensions/SingleLineRewriter.vb b/src/Workspaces/VisualBasic/Portable/Extensions/SingleLineRewriter.vb index 3e57ffd474c..70dfe41ab19 100644 --- a/src/Workspaces/VisualBasic/Portable/Extensions/SingleLineRewriter.vb +++ b/src/Workspaces/VisualBasic/Portable/Extensions/SingleLineRewriter.vb @@ -12,23 +12,29 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Friend Class SingleLineRewriter Inherits VisualBasicSyntaxRewriter + Private useElasticTrivia As Boolean Private _lastTokenEndedInWhitespace As Boolean - Private Shared ReadOnly s_space As SyntaxTriviaList = SyntaxTriviaList.Create(SyntaxFactory.WhitespaceTrivia(" ")) + + Public Sub New(Optional useElasticTrivia As Boolean = False) + Me.useElasticTrivia = useElasticTrivia + End Sub Public Overrides Function VisitToken(token As SyntaxToken) As SyntaxToken If _lastTokenEndedInWhitespace Then token = token.WithLeadingTrivia(Enumerable.Empty(Of SyntaxTrivia)()) ElseIf token.LeadingTrivia.Count > 0 Then - token = token.WithLeadingTrivia(s_space) - End If - -#If False Then - If token.Kind = SyntaxKind.StatementTerminatorToken Then - token = Syntax.Token(token.LeadingTrivia, SyntaxKind.StatementTerminatorToken, token.TrailingTrivia, ":") + If useElasticTrivia Then + token = token.WithLeadingTrivia(SyntaxFactory.ElasticSpace) + Else + token = token.WithLeadingTrivia(SyntaxFactory.Space) + End If End If -#End If If token.TrailingTrivia.Count > 0 Then - token = token.WithTrailingTrivia(s_space) + If useElasticTrivia Then + token = token.WithTrailingTrivia(SyntaxFactory.ElasticSpace) + Else + token = token.WithTrailingTrivia(SyntaxFactory.Space) + End If _lastTokenEndedInWhitespace = True Else _lastTokenEndedInWhitespace = False diff --git a/src/Workspaces/VisualBasic/Portable/Extensions/SyntaxNodeExtensions.vb b/src/Workspaces/VisualBasic/Portable/Extensions/SyntaxNodeExtensions.vb index c38701f93a5..e5c3a9fad6f 100644 --- a/src/Workspaces/VisualBasic/Portable/Extensions/SyntaxNodeExtensions.vb +++ b/src/Workspaces/VisualBasic/Portable/Extensions/SyntaxNodeExtensions.vb @@ -284,12 +284,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions End Function - Public Function ConvertToSingleLine(Of TNode As SyntaxNode)(node As TNode) As TNode + Public Function ConvertToSingleLine(Of TNode As SyntaxNode)(node As TNode, Optional useElasticTrivia As Boolean = False) As TNode If node Is Nothing Then Return node End If - Dim rewriter = New SingleLineRewriter() + Dim rewriter = New SingleLineRewriter(useElasticTrivia) Return DirectCast(rewriter.Visit(node), TNode) End Function diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index e1803c555e4..4836c270fad 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -409,10 +409,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return False End Function - Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Public Function IsStringLiteralOrInterpolatedStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteralOrInterpolatedStringLiteral Return token.IsKind(SyntaxKind.StringLiteralToken, SyntaxKind.InterpolatedStringTextToken) End Function + Public Function IsNumericLiteralExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsNumericLiteralExpression + Return If(node Is Nothing, False, node.IsKind(SyntaxKind.NumericLiteralExpression)) + End Function + Public Function IsBindableToken(token As Microsoft.CodeAnalysis.SyntaxToken) As Boolean Implements ISyntaxFactsService.IsBindableToken Return Me.IsWord(token) OrElse Me.IsLiteral(token) OrElse @@ -444,6 +448,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return TryCast(node, ConditionalAccessExpressionSyntax)?.Expression End Function + Public Function GetExpressionOfInterpolation(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.GetExpressionOfInterpolation + Return TryCast(node, InterpolationSyntax)?.Expression + End Function + Public Function IsInNamespaceOrTypeContext(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsInNamespaceOrTypeContext Return SyntaxFacts.IsInNamespaceOrTypeContext(node) End Function @@ -553,10 +561,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return node.Kind = SyntaxKind.InvocationExpression OrElse node.Kind = SyntaxKind.DictionaryAccessExpression End Function - Public Function ConvertToSingleLine(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine - Return node.ConvertToSingleLine() - End Function - Public Function ToIdentifierToken(name As String) As SyntaxToken Implements ISyntaxFactsService.ToIdentifierToken Return name.ToIdentifierToken() End Function @@ -1319,5 +1323,21 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Public Function IsOperandOfIncrementOrDecrementExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsOperandOfIncrementOrDecrementExpression Return False End Function + + Public Function GetContentsOfInterpolatedString(interpolatedString As SyntaxNode) As SyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetContentsOfInterpolatedString + Return (TryCast(interpolatedString, InterpolatedStringExpressionSyntax)?.Contents).Value + End Function + + Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Return token.IsKind(SyntaxKind.StringLiteralToken) + End Function + + Public Function GetArgumentsForInvocationExpression(invocationExpression As SyntaxNode) As SeparatedSyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetArgumentsForInvocationExpression + Return (TryCast(invocationExpression, InvocationExpressionSyntax)?.ArgumentList.Arguments).Value + End Function + + Public Function ConvertToSingleLine(node As SyntaxNode, Optional useElasticTrivia As Boolean = False) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine + Return node.ConvertToSingleLine(useElasticTrivia) + End Function End Class End Namespace -- GitLab