From 6eeb77d582f96489ecca8ac5a34a3c27bbd16ea0 Mon Sep 17 00:00:00 2001 From: Jonathon Marolf Date: Thu, 12 May 2016 17:56:00 -0700 Subject: [PATCH 1/3] initial port from CSharpEssentials --- .../CSharpEditorServicesTest.csproj | 1 + .../ConvertToInterpolatedStringTests.cs | 528 ++++++++++++++++++ src/EditorFeatures/TestUtilities/Traits.cs | 1 + .../BasicEditorServicesTest.vbproj | 1 + .../ConvertToInterpolatedStringTests.vb | 442 +++++++++++++++ .../CSharp/Portable/CSharpFeatures.csproj | 3 + ...ToInterpolatedStringRefactoringProvider.cs | 122 ++++ .../InterpolatedStringRewriter.cs | 55 ++ ...mmentInInterpolatedStringFormattingRule.cs | 37 ++ ...ToInterpolatedStringRefactoringProvider.cs | 182 ++++++ .../PredefinedCodeRefactoringProviderNames.cs | 1 + src/Features/Core/Portable/Features.csproj | 1 + .../Portable/FeaturesResources.Designer.cs | 9 + .../Core/Portable/FeaturesResources.resx | 3 + .../VisualBasic/Portable/BasicFeatures.vbproj | 2 + ...ToInterpolatedStringRefactoringProvider.vb | 104 ++++ .../InterpolatedStringRewriter.vb | 33 ++ 17 files changed, 1525 insertions(+) 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/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs create mode 100644 src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs create mode 100644 src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs create mode 100644 src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb create mode 100644 src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj index eac5fe80e8b84..34ddc41aff998 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 0000000000000..730029c1e65c0 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs @@ -0,0 +1,528 @@ +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 TestBraceEscape() + { + await TestAsync( +@"using System; + +public class T +{ + public static void M() + { + string result = [|String.Format(@""The text has {0} '{' characters and {1} '}' characters."", + '{', '}')|]; + } +}", +@"using System; + +public class T +{ + public static void M() + { + string result = $@""The text has {'{'} '{{' characters and {'}'} '}}' characters.""; + } +}"); + } + } +} diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index e82c4a5550711..aca13d1d6eb90 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 8d71a4377c08a..9fdae2f012781 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 0000000000000..a77972de3d330 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb @@ -0,0 +1,442 @@ +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 + + + Public Async Function TestMultilineStringLiteral() As Task + Dim text = +Imports System +Module T + Sub M(args As String()) + Dim s = [|String.Format("The text has {0} '{' characters and {1} '}' characters.", + "{"c, "}"c)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M(args As String()) + Dim s = $"The text has {"{"c} '{{' characters and {"}"c} '}}' characters." + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function +End Class diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index 5bfc053c52a78..d463fb6913d8b 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -92,6 +92,9 @@ + + + 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 0000000000000..4e51b2ea98921 --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Formatting.Rules; +using Microsoft.CodeAnalysis.Simplification; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] + internal partial class ConvertToInterpolatedStringRefactoringProvider : AbstractConvertToInterpolatedStringRefactoringProvider + { + protected override SeparatedSyntaxList? GetArguments(InvocationExpressionSyntax invocation) => + invocation?.ArgumentList?.Arguments; + + protected override ImmutableArray GetExpandedArguments( + SemanticModel semanticModel, + SeparatedSyntaxList arguments) + { + var builder = ImmutableArray.CreateBuilder(); + for (int i = 1; i < arguments.Count; i++) + { + builder.Add(CastAndParenthesize(arguments[i].Expression, semanticModel)); + } + + var expandedArguments = builder.ToImmutable(); + return expandedArguments; + } + + protected override LiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments) => + arguments[0]?.Expression as LiteralExpressionSyntax; + + protected override IEnumerable GetFormattingRules(Document document) + { + var rules = new List { new MultiLineCommentInInterpolatedStringFormattingRule() }; + rules.AddRange(Formatter.GetDefaultFormattingRules(document)); + + return rules; + } + + protected override InterpolatedStringExpressionSyntax GetInterpolatedString(string text) => + (InterpolatedStringExpressionSyntax)ParseExpression("$" + text); + + protected override string GetText(SeparatedSyntaxList arguments) + { + var stringToken = ((LiteralExpressionSyntax)arguments[0].Expression).Token; + var text = stringToken.ToString(); + + if (stringToken.IsVerbatimStringLiteral()) + { + // We need to escape braces as this is an ambiguous case in interpolated strings that is + // not ambiguous in verbatim strings + text = text.Replace(@"'{'", @"'{{'").Replace(@"'}'", @"'}}'"); + } + + return text; + } + + protected override bool IsArgumentListCorrect( + InvocationExpressionSyntax invocation, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + if (invocation.ArgumentList != null && + invocation.ArgumentList.Arguments.Count >= 2 && + invocation.ArgumentList.Arguments[0].Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + // 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( + invocation.ArgumentList.Arguments[1].Expression, + invocationSymbol, + formatMethods, + semanticModel, + cancellationToken); + } + + return false; + } + + protected override bool IsStringLiteral(LiteralExpressionSyntax firstArgument) => + firstArgument?.Token.IsKind(SyntaxKind.StringLiteralToken) == true; + + protected override InterpolatedStringExpressionSyntax VisitArguments( + ImmutableArray expandedArguments, + InterpolatedStringExpressionSyntax interpolatedString) => + InterpolatedStringRewriter.Visit(interpolatedString, expandedArguments); + + private static ExpressionSyntax CastAndParenthesize(ExpressionSyntax expression, SemanticModel semanticModel) => + Parenthesize(Cast(expression, semanticModel.GetTypeInfo(expression).ConvertedType)); + + private static ExpressionSyntax Cast(ExpressionSyntax expression, ITypeSymbol targetType) + { + if (targetType == null) + { + return expression; + } + + var type = ParseTypeName(targetType.ToDisplayString()); + + return CastExpression(type, Parenthesize(expression)) + .WithAdditionalAnnotations(Simplifier.Annotation); + } + + private static ExpressionSyntax Parenthesize(ExpressionSyntax expression) => + expression.IsKind(SyntaxKind.ParenthesizedExpression) + ? expression + : ParenthesizedExpression( + openParenToken: Token(SyntaxTriviaList.Empty, SyntaxKind.OpenParenToken, SyntaxTriviaList.Empty), + expression: expression, + closeParenToken: Token(SyntaxTriviaList.Empty, SyntaxKind.CloseParenToken, SyntaxTriviaList.Empty)) + .WithAdditionalAnnotations(Simplifier.Annotation); + } +} diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs new file mode 100644 index 0000000000000..907f7302b8e0a --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs @@ -0,0 +1,55 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString +{ + internal partial class ConvertToInterpolatedStringRefactoringProvider + { + private class InterpolatedStringRewriter : CSharpSyntaxRewriter + { + private readonly ImmutableArray expandedArguments; + + private InterpolatedStringRewriter(ImmutableArray expandedArguments) + { + this.expandedArguments = expandedArguments; + } + + public override SyntaxNode VisitInterpolation(InterpolationSyntax node) + { + var literalExpression = node.Expression as LiteralExpressionSyntax; + if (literalExpression != null && literalExpression.IsKind(SyntaxKind.NumericLiteralExpression)) + { + var index = (int)literalExpression.Token.Value; + if (index >= 0 && index < expandedArguments.Length) + { + return node.WithExpression(FixTrivia(expandedArguments[index])); + } + } + + return base.VisitInterpolation(node); + } + + /// + /// Since C# interpolations cannot be on more than one line, we need to remove newlines if possible. + /// + public static ExpressionSyntax FixTrivia(ExpressionSyntax node) => + node.ReplaceTokens(node.DescendantTokens(descendIntoTrivia: true), RemoveTriviaForTokens); + + private static SyntaxToken RemoveTriviaForTokens(SyntaxToken originalToken, SyntaxToken rewrittenToken) => + rewrittenToken + .WithLeadingTrivia( + ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.LeadingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)) + .WithTrailingTrivia( + ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.TrailingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)); + + private static SyntaxTriviaList ReplaceSyntaxKindsWithElasticSpace(SyntaxTriviaList trivialList, params SyntaxKind[] kinds) => + trivialList.Select(x => kinds.Any(y => x.IsKind(y)) ? SyntaxFactory.ElasticSpace : x).ToSyntaxTriviaList(); + + public static InterpolatedStringExpressionSyntax Visit(InterpolatedStringExpressionSyntax interpolatedString, ImmutableArray expandedArguments) + { + return ((InterpolatedStringExpressionSyntax)new InterpolatedStringRewriter(expandedArguments).Visit(interpolatedString)).WithAdditionalAnnotations(SpecializedFormattingAnnotation); + } + } + } +} diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs new file mode 100644 index 0000000000000..8e3383c2c17d7 --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Microsoft.CodeAnalysis.Formatting.Rules; +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString +{ + internal partial class ConvertToInterpolatedStringRefactoringProvider + { + private class MultiLineCommentInInterpolatedStringFormattingRule : AbstractFormattingRule + { + private bool ForceSingleSpace(SyntaxToken previousToken, SyntaxToken currentToken) + { + return currentToken.GetAllTrivia().Any(t => t.IsKind(SyntaxKind.MultiLineCommentTrivia)); + } + + public override AdjustNewLinesOperation GetAdjustNewLinesOperation(SyntaxToken previousToken, SyntaxToken currentToken, OptionSet optionSet, NextOperation nextOperation) + { + if (ForceSingleSpace(previousToken, currentToken)) + { + return null; + } + + return base.GetAdjustNewLinesOperation(previousToken, currentToken, optionSet, nextOperation); + } + + public override AdjustSpacesOperation GetAdjustSpacesOperation(SyntaxToken previousToken, SyntaxToken currentToken, OptionSet optionSet, NextOperation nextOperation) + { + if (ForceSingleSpace(previousToken, currentToken)) + { + return new AdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces); + } + + return base.GetAdjustSpacesOperation(previousToken, currentToken, optionSet, nextOperation); + } + } + } +} 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 0000000000000..a2385396500c9 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Formatting.Rules; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CodeRefactorings +{ + internal abstract class AbstractConvertToInterpolatedStringRefactoringProvider : CodeRefactoringProvider + where TInterpolatedStringExpressionSyntax : SyntaxNode + where TInvocationExpressionSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + where TArgumentSyntax : SyntaxNode + where TLiteralExpressionSyntax : SyntaxNode + { + protected static SyntaxAnnotation SpecializedFormattingAnnotation = new SyntaxAnnotation(); + protected abstract SeparatedSyntaxList? GetArguments(TInvocationExpressionSyntax invocation); + protected abstract ImmutableArray GetExpandedArguments(SemanticModel semanticModel, SeparatedSyntaxList arguments); + protected abstract TLiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments); + protected abstract IEnumerable GetFormattingRules(Document document); + protected abstract TInterpolatedStringExpressionSyntax GetInterpolatedString(string text); + protected abstract string GetText(SeparatedSyntaxList arguments); + protected abstract bool IsArgumentListCorrect(TInvocationExpressionSyntax invocation, ISymbol invocationSymbol, ImmutableArray formatMethods, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract bool IsStringLiteral(TLiteralExpressionSyntax firstArgument); + protected abstract TInterpolatedStringExpressionSyntax VisitArguments(ImmutableArray expandedArguments, TInterpolatedStringExpressionSyntax interpolatedString); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + var stringType = semanticModel.Compilation.GetTypeByMetadataName("System.String"); + if (stringType == null) + { + return; + } + + var formatMethods = stringType + .GetMembers("Format") + .RemoveAll(ShouldRemoveStringFormatMethod); + + if (formatMethods.Length == 0) + { + return; + } + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + TInvocationExpressionSyntax invocation; + ISymbol invocationSymbol; + if (TryFindInvocation(context.Span, root, semanticModel, formatMethods, context.CancellationToken, out invocation, out invocationSymbol) && + IsArgumentListCorrect(invocation, invocationSymbol, formatMethods, semanticModel, context.CancellationToken)) + { + context.RegisterRefactoring( + new ConvertToInterpolatedStringCodeAction(FeaturesResources.ConvertToInterpolatedString, c => CreateInterpolatedString(invocation, context.Document, c))); + } + } + + private bool TryFindInvocation( + TextSpan span, + SyntaxNode root, + SemanticModel semanticModel, + ImmutableArray formatMethods, + CancellationToken cancellationToken, + out TInvocationExpressionSyntax invocation, + out ISymbol invocationSymbol) + { + invocationSymbol = null; + invocation = root.FindNode(span, getInnermostNodeForTie: true)?.FirstAncestorOrSelf(); + while (invocation != null) + { + var nullableArguments = GetArguments(invocation); + if (nullableArguments != null) + { + var arguments = nullableArguments.Value; + if (arguments.Count >= 2) + { + var firstArgument = GetFirstArgument(arguments); + if (IsStringLiteral(firstArgument)) + { + invocationSymbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (formatMethods.Contains(invocationSymbol)) + { + break; + } + } + } + } + + invocation = invocation.Parent?.FirstAncestorOrSelf(); + } + + return invocation != null; + } + + + private async Task CreateInterpolatedString(TInvocationExpressionSyntax invocation, Document document, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var arguments = GetArguments(invocation).Value; + string text = GetText(arguments); + var expandedArguments = GetExpandedArguments(semanticModel, arguments); + var interpolatedString = GetInterpolatedString(text); + var newInterpolatedString = VisitArguments(expandedArguments, interpolatedString); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(invocation, newInterpolatedString.WithTriviaFrom(invocation)); + newRoot = await FormatAsync(newRoot, document, cancellationToken).ConfigureAwait(false); + return document.WithSyntaxRoot(newRoot); + } + + private async Task FormatAsync(SyntaxNode newRoot, Document document, CancellationToken cancellationToken) + { + var formattingRules = GetFormattingRules(document); + if (formattingRules == null) + { + return newRoot; + } + + return await Formatter.FormatAsync( + newRoot, + SpecializedFormattingAnnotation, + document.Project.Solution.Workspace, + options: null, + rules: formattingRules, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private static bool ShouldRemoveStringFormatMethod(ISymbol symbol) + { + if (symbol.Kind != SymbolKind.Method || !symbol.IsStatic) + { + return true; + } + + var methodSymbol = (IMethodSymbol)symbol; + if (methodSymbol.Parameters.Length == 0) + { + return true; + } + + var firstParameter = methodSymbol.Parameters[0]; + if (firstParameter?.Name != "format") + { + return true; + } + + return false; + } + + protected bool IsArgumentListNotPassingArrayToParams( + SyntaxNode expression, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var formatMethodsAcceptingParamsArray = formatMethods + .OfType() + .Where(x => + x.Parameters.Length > 1 + ? x.Parameters[1].Type.Kind == SymbolKind.ArrayType + : false); + 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 f36cae8e6ede6..a4da83de80be9 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 985ff9954107f..5f52ec2fd1657 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 5744a5a490b6b..4c7efa8bc563b 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -619,6 +619,15 @@ internal static string ContainsAnonymousType { } } + /// + /// 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 e0f9a62c05185..7697d82e53ab4 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 1ca44cd83793c..53c5a72918883 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -126,6 +126,8 @@ + + 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 0000000000000..3e1b42583e6c7 --- /dev/null +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb @@ -0,0 +1,104 @@ +Imports System.Collections.Immutable +Imports System.Composition +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.Formatting.Rules +Imports Microsoft.CodeAnalysis.Simplification +Imports Microsoft.CodeAnalysis.SyntaxTriviaList +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory + + +Partial Friend Class ConvertToInterpolatedStringRefactoringProvider + Inherits AbstractConvertToInterpolatedStringRefactoringProvider(Of InterpolatedStringExpressionSyntax, InvocationExpressionSyntax, ExpressionSyntax, ArgumentSyntax, LiteralExpressionSyntax) + + Protected Overrides Function GetArguments(invocation As InvocationExpressionSyntax) As SeparatedSyntaxList(Of ArgumentSyntax) ? + Return invocation?.ArgumentList?.Arguments + End Function + + Protected Overrides Function GetExpandedArguments(semanticModel As SemanticModel, arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As ImmutableArray(Of ExpressionSyntax) + Dim builder = ImmutableArray.CreateBuilder(Of ExpressionSyntax) + For index = 1 To arguments.Count - 1 + builder.Add(CastAndParenthesize(arguments(index).GetExpression, semanticModel)) + Next + + Return builder.ToImmutable() + End Function + + Protected Overrides Function GetFirstArgument(arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As LiteralExpressionSyntax + Return TryCast(arguments(0)?.GetExpression(), LiteralExpressionSyntax) + End Function + + Protected Overrides Function GetFormattingRules(document As Document) As IEnumerable(Of IFormattingRule) + ' VB does not have multiline comments so we don't need to format them + Return Nothing + End Function + + Protected Overrides Function GetInterpolatedString(text As String) As InterpolatedStringExpressionSyntax + Return CType(ParseExpression("$" + text), InterpolatedStringExpressionSyntax) + End Function + + Protected Overrides Function GetText(arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As String + Dim text = CType(arguments(0).GetExpression, LiteralExpressionSyntax).Token.ToString + ' We need to escape braces as this is an ambiguous case in interpolated strings that is + ' not ambiguous in verbatim strings + Return text.Replace("'{'", "'{{'").Replace("'}'", "'}}'") + End Function + + Protected Overrides Function IsArgumentListCorrect( + invocation As InvocationExpressionSyntax, + invocationSymbol As ISymbol, + formatMethods As ImmutableArray(Of ISymbol), + semanticModel As SemanticModel, + cancellationToken As CancellationToken) As Boolean + If (invocation.ArgumentList IsNot Nothing AndAlso + invocation.ArgumentList.Arguments.Count >= 2 AndAlso + invocation.ArgumentList.Arguments(0).GetExpression().IsKind(SyntaxKind.StringLiteralExpression)) Then + ' We do not want to substitute the expression if it is being passed to params array argument + ' Example: + ' Dim args as String() + ' String.Format("{0}{1}{2}", args) + Return IsArgumentListNotPassingArrayToParams(invocation.ArgumentList.Arguments(1).GetExpression, + invocationSymbol, + formatMethods, + semanticModel, + cancellationToken) + End If + + Return False + End Function + + Protected Overrides Function IsStringLiteral(firstArgument As LiteralExpressionSyntax) As Boolean + Return If(firstArgument Is Nothing, False, firstArgument.Token.IsKind(SyntaxKind.StringLiteralToken)) + End Function + + Protected Overrides Function VisitArguments(expandedArguments As ImmutableArray(Of ExpressionSyntax), interpolatedString As InterpolatedStringExpressionSyntax) As InterpolatedStringExpressionSyntax + Return InterpolatedStringRewriter.Visit(interpolatedString, expandedArguments) + End Function + + Private Shared Function CastAndParenthesize(expression As ExpressionSyntax, semanticModel As SemanticModel) As ExpressionSyntax + Dim targetType = semanticModel.GetTypeInfo(expression).ConvertedType + Return Parenthesize(Cast(expression, targetType)) + End Function + + Private Shared Function Cast(expression As ExpressionSyntax, targetType As ITypeSymbol) As ExpressionSyntax + If targetType Is Nothing Then + Return expression + End If + + Dim type = ParseTypeName(targetType.ToDisplayString) + Return CTypeExpression(Parenthesize(expression), type).WithAdditionalAnnotations(Simplifier.Annotation) + End Function + + Private Shared Function Parenthesize(expression As ExpressionSyntax) As ExpressionSyntax + Return If(expression.IsKind(SyntaxKind.ParenthesizedExpression), + expression, + ParenthesizedExpression( + openParenToken:=Token(Empty, SyntaxKind.OpenParenToken, Empty), + expression:=expression, + closeParenToken:=Token(Empty, SyntaxKind.CloseParenToken, Empty)). + WithAdditionalAnnotations(Simplifier.Annotation)) + End Function +End Class diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb new file mode 100644 index 0000000000000..31946c66d2087 --- /dev/null +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb @@ -0,0 +1,33 @@ +Imports System.Collections.Immutable +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Partial Friend Class ConvertToInterpolatedStringRefactoringProvider + Private Class InterpolatedStringRewriter + Inherits VisualBasicSyntaxRewriter + + Private ReadOnly expandedArguments As ImmutableArray(Of ExpressionSyntax) + + Private Sub New(expandedArguments As ImmutableArray(Of ExpressionSyntax)) + Me.expandedArguments = expandedArguments + End Sub + + Public Overrides Function VisitInterpolation(node As InterpolationSyntax) As SyntaxNode + Dim literalExpression = CType(node.Expression, LiteralExpressionSyntax) + If literalExpression IsNot Nothing AndAlso literalExpression.IsKind(SyntaxKind.NumericLiteralExpression) Then + Dim index = CType(literalExpression.Token.Value, Integer) + If index >= 0 AndAlso index < expandedArguments.Length Then + Return node.WithExpression(expandedArguments(index)) + End If + End If + + Return MyBase.VisitInterpolation(node) + End Function + + Public Overloads Shared Function Visit(interpolatedString As InterpolatedStringExpressionSyntax, expandedArguments As ImmutableArray(Of ExpressionSyntax)) As InterpolatedStringExpressionSyntax + Return CType(New InterpolatedStringRewriter(expandedArguments).Visit(interpolatedString), InterpolatedStringExpressionSyntax) + End Function + + End Class +End Class From 725c75d09b83d96a19c1f4112b5484345f896c2a Mon Sep 17 00:00:00 2001 From: Jonathon Marolf Date: Tue, 31 May 2016 16:08:58 -0700 Subject: [PATCH 2/3] responding to feedback. removing syntax rewriter and incorporating formatting rules into formatter --- .../ConvertToInterpolatedStringTests.cs | 12 ++-- .../CSharp/Portable/CSharpFeatures.csproj | 2 - ...ToInterpolatedStringRefactoringProvider.cs | 55 ++++++++++++++----- .../InterpolatedStringRewriter.cs | 55 ------------------- ...mmentInInterpolatedStringFormattingRule.cs | 37 ------------- ...ToInterpolatedStringRefactoringProvider.cs | 23 -------- .../VisualBasic/Portable/BasicFeatures.vbproj | 1 - ...ToInterpolatedStringRefactoringProvider.vb | 23 ++++++-- .../InterpolatedStringRewriter.vb | 33 ----------- .../Rules/SuppressFormattingRule.cs | 7 +++ 10 files changed, 71 insertions(+), 177 deletions(-) delete mode 100644 src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs delete mode 100644 src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs delete mode 100644 src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb diff --git a/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs index 730029c1e65c0..bfbd24a122bb9 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs @@ -164,7 +164,7 @@ class T { void M() { - var a = $""{(object)new object}""; + var a = $""{ (object)new object}""; } }"); } @@ -281,7 +281,7 @@ class T void M() { Decimal pricePerOunce = 17.36m; - String s = $""The current price is { pricePerOunce:C2} per ounce.""; + String s = $""The current price is { pricePerOunce:C2} per ounce.""; } }"); } @@ -334,7 +334,7 @@ void M() 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""; + s += $""{ years[index],6} {population[index],15:N0}\n""; } }"); } @@ -405,7 +405,7 @@ public static void M() 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}""; + 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}""; } } }"); @@ -469,7 +469,7 @@ public static void M() int value1 = 16932; int value2 = 15421; string result = $@"" - {value1,10} ({value1,8:X8}) + { value1,10} ({ value1,8:X8}) And {value2,10} ({value2,8:X8}) = {value1 & value2,10} ({value1 & value2,8:X8})""; } @@ -520,7 +520,7 @@ public class T { public static void M() { - string result = $@""The text has {'{'} '{{' characters and {'}'} '}}' characters.""; + string result = $@""The text has { '{'} '{{' characters and {'}'} '}}' characters.""; } }"); } diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index d463fb6913d8b..c3b229500f26c 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -93,8 +93,6 @@ - - diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs index 4e51b2ea98921..d8b6209d3d469 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Composition; using System.Threading; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Simplification; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using System.Linq; namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString { @@ -34,14 +32,6 @@ protected override ImmutableArray GetExpandedArguments( protected override LiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments) => arguments[0]?.Expression as LiteralExpressionSyntax; - protected override IEnumerable GetFormattingRules(Document document) - { - var rules = new List { new MultiLineCommentInInterpolatedStringFormattingRule() }; - rules.AddRange(Formatter.GetDefaultFormattingRules(document)); - - return rules; - } - protected override InterpolatedStringExpressionSyntax GetInterpolatedString(string text) => (InterpolatedStringExpressionSyntax)ParseExpression("$" + text); @@ -91,8 +81,45 @@ protected override bool IsStringLiteral(LiteralExpressionSyntax firstArgument) = protected override InterpolatedStringExpressionSyntax VisitArguments( ImmutableArray expandedArguments, - InterpolatedStringExpressionSyntax interpolatedString) => - InterpolatedStringRewriter.Visit(interpolatedString, expandedArguments); + InterpolatedStringExpressionSyntax interpolatedString) + { + return interpolatedString.ReplaceNodes(interpolatedString.Contents, (oldNode, newNode) => + { + var node = newNode as InterpolationSyntax; + if (node == null) + { + return newNode; + } + + var literalExpression = node.Expression as LiteralExpressionSyntax; + if (literalExpression != null && literalExpression.IsKind(SyntaxKind.NumericLiteralExpression)) + { + var index = (int)literalExpression.Token.Value; + if (index >= 0 && index < expandedArguments.Length) + { + return node.WithExpression(FixTrivia(expandedArguments[index])); + } + } + + return newNode; + }); + } + + /// + /// Since C# interpolations cannot be on more than one line, we need to remove newlines if possible. + /// + public static ExpressionSyntax FixTrivia(ExpressionSyntax node) => + node.ReplaceTokens(node.DescendantTokens(descendIntoTrivia: true), RemoveTriviaForTokens); + + private static SyntaxToken RemoveTriviaForTokens(SyntaxToken originalToken, SyntaxToken rewrittenToken) => + rewrittenToken + .WithLeadingTrivia( + ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.LeadingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)) + .WithTrailingTrivia( + ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.TrailingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)); + + private static SyntaxTriviaList ReplaceSyntaxKindsWithElasticSpace(SyntaxTriviaList trivialList, params SyntaxKind[] kinds) => + trivialList.Select(x => kinds.Any(y => x.IsKind(y)) ? SyntaxFactory.ElasticSpace : x).ToSyntaxTriviaList(); private static ExpressionSyntax CastAndParenthesize(ExpressionSyntax expression, SemanticModel semanticModel) => Parenthesize(Cast(expression, semanticModel.GetTypeInfo(expression).ConvertedType)); diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs deleted file mode 100644 index 907f7302b8e0a..0000000000000 --- a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString -{ - internal partial class ConvertToInterpolatedStringRefactoringProvider - { - private class InterpolatedStringRewriter : CSharpSyntaxRewriter - { - private readonly ImmutableArray expandedArguments; - - private InterpolatedStringRewriter(ImmutableArray expandedArguments) - { - this.expandedArguments = expandedArguments; - } - - public override SyntaxNode VisitInterpolation(InterpolationSyntax node) - { - var literalExpression = node.Expression as LiteralExpressionSyntax; - if (literalExpression != null && literalExpression.IsKind(SyntaxKind.NumericLiteralExpression)) - { - var index = (int)literalExpression.Token.Value; - if (index >= 0 && index < expandedArguments.Length) - { - return node.WithExpression(FixTrivia(expandedArguments[index])); - } - } - - return base.VisitInterpolation(node); - } - - /// - /// Since C# interpolations cannot be on more than one line, we need to remove newlines if possible. - /// - public static ExpressionSyntax FixTrivia(ExpressionSyntax node) => - node.ReplaceTokens(node.DescendantTokens(descendIntoTrivia: true), RemoveTriviaForTokens); - - private static SyntaxToken RemoveTriviaForTokens(SyntaxToken originalToken, SyntaxToken rewrittenToken) => - rewrittenToken - .WithLeadingTrivia( - ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.LeadingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)) - .WithTrailingTrivia( - ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.TrailingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)); - - private static SyntaxTriviaList ReplaceSyntaxKindsWithElasticSpace(SyntaxTriviaList trivialList, params SyntaxKind[] kinds) => - trivialList.Select(x => kinds.Any(y => x.IsKind(y)) ? SyntaxFactory.ElasticSpace : x).ToSyntaxTriviaList(); - - public static InterpolatedStringExpressionSyntax Visit(InterpolatedStringExpressionSyntax interpolatedString, ImmutableArray expandedArguments) - { - return ((InterpolatedStringExpressionSyntax)new InterpolatedStringRewriter(expandedArguments).Visit(interpolatedString)).WithAdditionalAnnotations(SpecializedFormattingAnnotation); - } - } - } -} diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs deleted file mode 100644 index 8e3383c2c17d7..0000000000000 --- a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/MultiLineCommentInInterpolatedStringFormattingRule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis.Formatting.Rules; -using Microsoft.CodeAnalysis.Options; - -namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString -{ - internal partial class ConvertToInterpolatedStringRefactoringProvider - { - private class MultiLineCommentInInterpolatedStringFormattingRule : AbstractFormattingRule - { - private bool ForceSingleSpace(SyntaxToken previousToken, SyntaxToken currentToken) - { - return currentToken.GetAllTrivia().Any(t => t.IsKind(SyntaxKind.MultiLineCommentTrivia)); - } - - public override AdjustNewLinesOperation GetAdjustNewLinesOperation(SyntaxToken previousToken, SyntaxToken currentToken, OptionSet optionSet, NextOperation nextOperation) - { - if (ForceSingleSpace(previousToken, currentToken)) - { - return null; - } - - return base.GetAdjustNewLinesOperation(previousToken, currentToken, optionSet, nextOperation); - } - - public override AdjustSpacesOperation GetAdjustSpacesOperation(SyntaxToken previousToken, SyntaxToken currentToken, OptionSet optionSet, NextOperation nextOperation) - { - if (ForceSingleSpace(previousToken, currentToken)) - { - return new AdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces); - } - - return base.GetAdjustSpacesOperation(previousToken, currentToken, optionSet, nextOperation); - } - } - } -} diff --git a/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs index a2385396500c9..f8b6d87f23c7f 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CodeRefactorings @@ -18,11 +15,9 @@ internal abstract class AbstractConvertToInterpolatedStringRefactoringProvider? GetArguments(TInvocationExpressionSyntax invocation); protected abstract ImmutableArray GetExpandedArguments(SemanticModel semanticModel, SeparatedSyntaxList arguments); protected abstract TLiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments); - protected abstract IEnumerable GetFormattingRules(Document document); protected abstract TInterpolatedStringExpressionSyntax GetInterpolatedString(string text); protected abstract string GetText(SeparatedSyntaxList arguments); protected abstract bool IsArgumentListCorrect(TInvocationExpressionSyntax invocation, ISymbol invocationSymbol, ImmutableArray formatMethods, SemanticModel semanticModel, CancellationToken cancellationToken); @@ -107,27 +102,9 @@ private async Task CreateInterpolatedString(TInvocationExpressionSynta var newInterpolatedString = VisitArguments(expandedArguments, interpolatedString); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var newRoot = root.ReplaceNode(invocation, newInterpolatedString.WithTriviaFrom(invocation)); - newRoot = await FormatAsync(newRoot, document, cancellationToken).ConfigureAwait(false); return document.WithSyntaxRoot(newRoot); } - private async Task FormatAsync(SyntaxNode newRoot, Document document, CancellationToken cancellationToken) - { - var formattingRules = GetFormattingRules(document); - if (formattingRules == null) - { - return newRoot; - } - - return await Formatter.FormatAsync( - newRoot, - SpecializedFormattingAnnotation, - document.Project.Solution.Workspace, - options: null, - rules: formattingRules, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - private static bool ShouldRemoveStringFormatMethod(ISymbol symbol) { if (symbol.Kind != SymbolKind.Method || !symbol.IsStatic) diff --git a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj index 53c5a72918883..bdc2d590414c2 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -126,7 +126,6 @@ - diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb index 3e1b42583e6c7..2f926f236a44b 100644 --- a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb @@ -31,11 +31,6 @@ Partial Friend Class ConvertToInterpolatedStringRefactoringProvider Return TryCast(arguments(0)?.GetExpression(), LiteralExpressionSyntax) End Function - Protected Overrides Function GetFormattingRules(document As Document) As IEnumerable(Of IFormattingRule) - ' VB does not have multiline comments so we don't need to format them - Return Nothing - End Function - Protected Overrides Function GetInterpolatedString(text As String) As InterpolatedStringExpressionSyntax Return CType(ParseExpression("$" + text), InterpolatedStringExpressionSyntax) End Function @@ -75,7 +70,23 @@ Partial Friend Class ConvertToInterpolatedStringRefactoringProvider End Function Protected Overrides Function VisitArguments(expandedArguments As ImmutableArray(Of ExpressionSyntax), interpolatedString As InterpolatedStringExpressionSyntax) As InterpolatedStringExpressionSyntax - Return InterpolatedStringRewriter.Visit(interpolatedString, expandedArguments) + Return interpolatedString.ReplaceNodes(interpolatedString.Contents, + Function(oldNode, newNode) + Dim node = TryCast(newNode, InterpolationSyntax) + If node Is Nothing Then + Return newNode + End If + + Dim literalExpression = CType(node.Expression, LiteralExpressionSyntax) + If literalExpression IsNot Nothing AndAlso literalExpression.IsKind(SyntaxKind.NumericLiteralExpression) Then + Dim index = CType(literalExpression.Token.Value, Integer) + If index >= 0 AndAlso index < expandedArguments.Length Then + Return node.WithExpression(expandedArguments(index)) + End If + End If + + Return newNode + End Function) End Function Private Shared Function CastAndParenthesize(expression As ExpressionSyntax, semanticModel As SemanticModel) As ExpressionSyntax diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb deleted file mode 100644 index 31946c66d2087..0000000000000 --- a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/InterpolatedStringRewriter.vb +++ /dev/null @@ -1,33 +0,0 @@ -Imports System.Collections.Immutable -Imports Microsoft.CodeAnalysis -Imports Microsoft.CodeAnalysis.VisualBasic -Imports Microsoft.CodeAnalysis.VisualBasic.Syntax - -Partial Friend Class ConvertToInterpolatedStringRefactoringProvider - Private Class InterpolatedStringRewriter - Inherits VisualBasicSyntaxRewriter - - Private ReadOnly expandedArguments As ImmutableArray(Of ExpressionSyntax) - - Private Sub New(expandedArguments As ImmutableArray(Of ExpressionSyntax)) - Me.expandedArguments = expandedArguments - End Sub - - Public Overrides Function VisitInterpolation(node As InterpolationSyntax) As SyntaxNode - Dim literalExpression = CType(node.Expression, LiteralExpressionSyntax) - If literalExpression IsNot Nothing AndAlso literalExpression.IsKind(SyntaxKind.NumericLiteralExpression) Then - Dim index = CType(literalExpression.Token.Value, Integer) - If index >= 0 AndAlso index < expandedArguments.Length Then - Return node.WithExpression(expandedArguments(index)) - End If - End If - - Return MyBase.VisitInterpolation(node) - End Function - - Public Overloads Shared Function Visit(interpolatedString As InterpolatedStringExpressionSyntax, expandedArguments As ImmutableArray(Of ExpressionSyntax)) As InterpolatedStringExpressionSyntax - Return CType(New InterpolatedStringRewriter(expandedArguments).Visit(interpolatedString), InterpolatedStringExpressionSyntax) - End Function - - End Class -End Class diff --git a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs index 1fd0233f37a13..6203b70b2c783 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Options; +using System.Linq; namespace Microsoft.CodeAnalysis.CSharp.Formatting { @@ -157,6 +158,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) From baf565ffa284e64d4465b2a6b3d65fc6b52e0649 Mon Sep 17 00:00:00 2001 From: Jonathon Marolf Date: Wed, 1 Jun 2016 19:04:15 -0700 Subject: [PATCH 3/3] responding to cryus' feedback - general logic cleanup - moving methods to abstract base class --- .../Core/Portable/PublicAPI.Unshipped.txt | 2 + .../Portable/Syntax/SeparatedSyntaxList.cs | 10 + .../ConvertToInterpolatedStringTests.cs | 15 +- .../CrefCompletionProviderTests.cs | 37 +++- .../ConvertToInterpolatedStringTests.vb | 22 --- .../CrefCompletionProviderTests.vb | 30 ++- ...ToInterpolatedStringRefactoringProvider.cs | 145 +------------- ...ToInterpolatedStringRefactoringProvider.cs | 185 +++++++++++++----- ...ToInterpolatedStringRefactoringProvider.vb | 108 +--------- ...SyntaxNodeExtensions.SingleLineRewriter.cs | 24 ++- .../Extensions/SyntaxNodeExtensions.cs | 4 +- .../CSharpSyntaxFactsService.cs | 31 ++- .../SyntaxFactsService/ISyntaxFactsService.cs | 8 +- .../RenameLocation.ReferenceProcessing.cs | 2 +- .../Portable/Extensions/SingleLineRewriter.vb | 24 ++- .../Extensions/SyntaxNodeExtensions.vb | 4 +- .../VisualBasicSyntaxFactsService.vb | 30 ++- 17 files changed, 325 insertions(+), 356 deletions(-) diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 58b6f0b9c3bd9..da45a7f9029ad 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 41a958ce95cc8..f02042e5bff38 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/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs index bfbd24a122bb9..77ff48956989f 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs @@ -281,7 +281,7 @@ class T void M() { Decimal pricePerOunce = 17.36m; - String s = $""The current price is { pricePerOunce:C2} per ounce.""; + String s = $""The current price is { pricePerOunce:C2} per ounce.""; } }"); } @@ -334,7 +334,7 @@ void M() 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""; + s += $""{years[index],6} {population[index],15:N0}\n""; } }"); } @@ -405,7 +405,7 @@ public static void M() 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}""; + 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}""; } } }"); @@ -469,7 +469,7 @@ public static void M() int value1 = 16932; int value2 = 15421; string result = $@"" - { value1,10} ({ value1,8:X8}) + {value1,10} ({value1,8:X8}) And {value2,10} ({value2,8:X8}) = {value1 & value2,10} ({value1 & value2,8:X8})""; } @@ -501,7 +501,7 @@ public static void M() } [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] - public async Task TestBraceEscape() + public async Task TestInvalidInteger() { await TestAsync( @"using System; @@ -510,8 +510,7 @@ public class T { public static void M() { - string result = [|String.Format(@""The text has {0} '{' characters and {1} '}' characters."", - '{', '}')|]; + string result = [|String.Format(""{0L}"", 5)|]; } }", @"using System; @@ -520,7 +519,7 @@ public class T { public static void M() { - string result = $@""The text has { '{'} '{{' characters and {'}'} '}}' characters.""; + string result = $""{5}""; } }"); } diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs index 384bb2ea19bd4..d0b301120056b 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/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb index a77972de3d330..2715ef2fd44f1 100644 --- a/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb +++ b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb @@ -417,26 +417,4 @@ Module T End Module.ConvertTestSourceTag() Await TestMissingAsync(text) End Function - - - Public Async Function TestMultilineStringLiteral() As Task - Dim text = -Imports System -Module T - Sub M(args As String()) - Dim s = [|String.Format("The text has {0} '{' characters and {1} '}' characters.", - "{"c, "}"c)|] - End Sub -End Module.ConvertTestSourceTag() - - Dim expected = -Imports System -Module T - Sub M(args As String()) - Dim s = $"The text has {"{"c} '{{' characters and {"}"c} '}}' characters." - End Sub -End Module.ConvertTestSourceTag() - - Await TestAsync(text, expected) - End Function End Class diff --git a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb index d40810ea07cd8..9cbc3ebfb3bdc 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/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs index d8b6209d3d469..2cf9375a0d41e 100644 --- a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs @@ -1,149 +1,18 @@ -using System.Collections.Immutable; +using System; using System.Composition; -using System.Threading; +using System.Linq; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using System.Linq; +using Microsoft.CodeAnalysis.Editing; namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString { [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] - internal partial class ConvertToInterpolatedStringRefactoringProvider : AbstractConvertToInterpolatedStringRefactoringProvider + internal partial class ConvertToInterpolatedStringRefactoringProvider : + AbstractConvertToInterpolatedStringRefactoringProvider { - protected override SeparatedSyntaxList? GetArguments(InvocationExpressionSyntax invocation) => - invocation?.ArgumentList?.Arguments; - - protected override ImmutableArray GetExpandedArguments( - SemanticModel semanticModel, - SeparatedSyntaxList arguments) - { - var builder = ImmutableArray.CreateBuilder(); - for (int i = 1; i < arguments.Count; i++) - { - builder.Add(CastAndParenthesize(arguments[i].Expression, semanticModel)); - } - - var expandedArguments = builder.ToImmutable(); - return expandedArguments; - } - - protected override LiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments) => - arguments[0]?.Expression as LiteralExpressionSyntax; - - protected override InterpolatedStringExpressionSyntax GetInterpolatedString(string text) => - (InterpolatedStringExpressionSyntax)ParseExpression("$" + text); - - protected override string GetText(SeparatedSyntaxList arguments) - { - var stringToken = ((LiteralExpressionSyntax)arguments[0].Expression).Token; - var text = stringToken.ToString(); - - if (stringToken.IsVerbatimStringLiteral()) - { - // We need to escape braces as this is an ambiguous case in interpolated strings that is - // not ambiguous in verbatim strings - text = text.Replace(@"'{'", @"'{{'").Replace(@"'}'", @"'}}'"); - } - - return text; - } - - protected override bool IsArgumentListCorrect( - InvocationExpressionSyntax invocation, - ISymbol invocationSymbol, - ImmutableArray formatMethods, - SemanticModel semanticModel, - CancellationToken cancellationToken) - { - if (invocation.ArgumentList != null && - invocation.ArgumentList.Arguments.Count >= 2 && - invocation.ArgumentList.Arguments[0].Expression.IsKind(SyntaxKind.StringLiteralExpression)) - { - // 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( - invocation.ArgumentList.Arguments[1].Expression, - invocationSymbol, - formatMethods, - semanticModel, - cancellationToken); - } - - return false; - } - - protected override bool IsStringLiteral(LiteralExpressionSyntax firstArgument) => - firstArgument?.Token.IsKind(SyntaxKind.StringLiteralToken) == true; - - protected override InterpolatedStringExpressionSyntax VisitArguments( - ImmutableArray expandedArguments, - InterpolatedStringExpressionSyntax interpolatedString) - { - return interpolatedString.ReplaceNodes(interpolatedString.Contents, (oldNode, newNode) => - { - var node = newNode as InterpolationSyntax; - if (node == null) - { - return newNode; - } - - var literalExpression = node.Expression as LiteralExpressionSyntax; - if (literalExpression != null && literalExpression.IsKind(SyntaxKind.NumericLiteralExpression)) - { - var index = (int)literalExpression.Token.Value; - if (index >= 0 && index < expandedArguments.Length) - { - return node.WithExpression(FixTrivia(expandedArguments[index])); - } - } - - return newNode; - }); - } - - /// - /// Since C# interpolations cannot be on more than one line, we need to remove newlines if possible. - /// - public static ExpressionSyntax FixTrivia(ExpressionSyntax node) => - node.ReplaceTokens(node.DescendantTokens(descendIntoTrivia: true), RemoveTriviaForTokens); - - private static SyntaxToken RemoveTriviaForTokens(SyntaxToken originalToken, SyntaxToken rewrittenToken) => - rewrittenToken - .WithLeadingTrivia( - ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.LeadingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)) - .WithTrailingTrivia( - ReplaceSyntaxKindsWithElasticSpace(rewrittenToken.TrailingTrivia, SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia)); - - private static SyntaxTriviaList ReplaceSyntaxKindsWithElasticSpace(SyntaxTriviaList trivialList, params SyntaxKind[] kinds) => - trivialList.Select(x => kinds.Any(y => x.IsKind(y)) ? SyntaxFactory.ElasticSpace : x).ToSyntaxTriviaList(); - - private static ExpressionSyntax CastAndParenthesize(ExpressionSyntax expression, SemanticModel semanticModel) => - Parenthesize(Cast(expression, semanticModel.GetTypeInfo(expression).ConvertedType)); - - private static ExpressionSyntax Cast(ExpressionSyntax expression, ITypeSymbol targetType) - { - if (targetType == null) - { - return expression; - } - - var type = ParseTypeName(targetType.ToDisplayString()); - - return CastExpression(type, Parenthesize(expression)) - .WithAdditionalAnnotations(Simplifier.Annotation); - } - - private static ExpressionSyntax Parenthesize(ExpressionSyntax expression) => - expression.IsKind(SyntaxKind.ParenthesizedExpression) - ? expression - : ParenthesizedExpression( - openParenToken: Token(SyntaxTriviaList.Empty, SyntaxKind.OpenParenToken, SyntaxTriviaList.Empty), - expression: expression, - closeParenToken: Token(SyntaxTriviaList.Empty, SyntaxKind.CloseParenToken, SyntaxTriviaList.Empty)) - .WithAdditionalAnnotations(Simplifier.Annotation); + 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 index f8b6d87f23c7f..f5fa5236f34df 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs @@ -5,30 +5,27 @@ 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 TInterpolatedStringExpressionSyntax : SyntaxNode - where TInvocationExpressionSyntax : SyntaxNode + internal abstract class AbstractConvertToInterpolatedStringRefactoringProvider : CodeRefactoringProvider where TExpressionSyntax : SyntaxNode + where TInvocationExpressionSyntax : TExpressionSyntax where TArgumentSyntax : SyntaxNode where TLiteralExpressionSyntax : SyntaxNode { - protected abstract SeparatedSyntaxList? GetArguments(TInvocationExpressionSyntax invocation); - protected abstract ImmutableArray GetExpandedArguments(SemanticModel semanticModel, SeparatedSyntaxList arguments); - protected abstract TLiteralExpressionSyntax GetFirstArgument(SeparatedSyntaxList arguments); - protected abstract TInterpolatedStringExpressionSyntax GetInterpolatedString(string text); - protected abstract string GetText(SeparatedSyntaxList arguments); - protected abstract bool IsArgumentListCorrect(TInvocationExpressionSyntax invocation, ISymbol invocationSymbol, ImmutableArray formatMethods, SemanticModel semanticModel, CancellationToken cancellationToken); - protected abstract bool IsStringLiteral(TLiteralExpressionSyntax firstArgument); - protected abstract TInterpolatedStringExpressionSyntax VisitArguments(ImmutableArray expandedArguments, TInterpolatedStringExpressionSyntax interpolatedString); + 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.GetTypeByMetadataName("System.String"); + var stringType = semanticModel.Compilation.GetSpecialType(SpecialType.System_String); if (stringType == null) { return; @@ -36,21 +33,31 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte var formatMethods = stringType .GetMembers("Format") - .RemoveAll(ShouldRemoveStringFormatMethod); + .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, context.CancellationToken, out invocation, out invocationSymbol) && - IsArgumentListCorrect(invocation, invocationSymbol, formatMethods, semanticModel, context.CancellationToken)) + 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, c))); + new ConvertToInterpolatedStringCodeAction( + FeaturesResources.ConvertToInterpolatedString, + c => CreateInterpolatedString(invocation, context.Document, syntaxFactsService, c))); } } @@ -58,7 +65,8 @@ private bool TryFindInvocation( TextSpan span, SyntaxNode root, SemanticModel semanticModel, - ImmutableArray formatMethods, + ImmutableArray formatMethods, + ISyntaxFactsService syntaxFactsService, CancellationToken cancellationToken, out TInvocationExpressionSyntax invocation, out ISymbol invocationSymbol) @@ -67,20 +75,16 @@ private bool TryFindInvocation( invocation = root.FindNode(span, getInnermostNodeForTie: true)?.FirstAncestorOrSelf(); while (invocation != null) { - var nullableArguments = GetArguments(invocation); - if (nullableArguments != null) + var arguments = syntaxFactsService.GetArgumentsForInvocationExpression(invocation); + if (arguments.Count >= 2) { - var arguments = nullableArguments.Value; - if (arguments.Count >= 2) + var firstArgumentExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + if (firstArgumentExpression != null && syntaxFactsService.IsStringLiteral(firstArgumentExpression.GetFirstToken())) { - var firstArgument = GetFirstArgument(arguments); - if (IsStringLiteral(firstArgument)) + invocationSymbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (formatMethods.Contains(invocationSymbol)) { - invocationSymbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; - if (formatMethods.Contains(invocationSymbol)) - { - break; - } + break; } } } @@ -91,55 +95,142 @@ private bool TryFindInvocation( 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, CancellationToken cancellationToken) + private async Task CreateInterpolatedString( + TInvocationExpressionSyntax invocation, + Document document, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken) { var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var arguments = GetArguments(invocation).Value; - string text = GetText(arguments); - var expandedArguments = GetExpandedArguments(semanticModel, arguments); + 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); + 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 static bool ShouldRemoveStringFormatMethod(ISymbol symbol) + 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 (symbol.Kind != SymbolKind.Method || !symbol.IsStatic) + if (!methodSymbol.IsStatic) { - return true; + return false; } - var methodSymbol = (IMethodSymbol)symbol; if (methodSymbol.Parameters.Length == 0) { - return true; + return false; } var firstParameter = methodSymbol.Parameters[0]; if (firstParameter?.Name != "format") { - return true; + return false; } - return false; + return true; } - protected bool IsArgumentListNotPassingArrayToParams( + private static bool IsArgumentListNotPassingArrayToParams( SyntaxNode expression, ISymbol invocationSymbol, - ImmutableArray formatMethods, + ImmutableArray formatMethods, SemanticModel semanticModel, CancellationToken cancellationToken) { var formatMethodsAcceptingParamsArray = formatMethods - .OfType() - .Where(x => - x.Parameters.Length > 1 - ? x.Parameters[1].Type.Kind == SymbolKind.ArrayType - : false); + .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; diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb index 2f926f236a44b..5095a1ece4f3d 100644 --- a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb @@ -1,115 +1,15 @@ -Imports System.Collections.Immutable -Imports System.Composition -Imports System.Threading +Imports System.Composition Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.CodeRefactorings -Imports Microsoft.CodeAnalysis.Formatting.Rules -Imports Microsoft.CodeAnalysis.Simplification -Imports Microsoft.CodeAnalysis.SyntaxTriviaList Imports Microsoft.CodeAnalysis.VisualBasic Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory Partial Friend Class ConvertToInterpolatedStringRefactoringProvider - Inherits AbstractConvertToInterpolatedStringRefactoringProvider(Of InterpolatedStringExpressionSyntax, InvocationExpressionSyntax, ExpressionSyntax, ArgumentSyntax, LiteralExpressionSyntax) + Inherits AbstractConvertToInterpolatedStringRefactoringProvider(Of InvocationExpressionSyntax, ExpressionSyntax, ArgumentSyntax, LiteralExpressionSyntax) - Protected Overrides Function GetArguments(invocation As InvocationExpressionSyntax) As SeparatedSyntaxList(Of ArgumentSyntax) ? - Return invocation?.ArgumentList?.Arguments - End Function - - Protected Overrides Function GetExpandedArguments(semanticModel As SemanticModel, arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As ImmutableArray(Of ExpressionSyntax) - Dim builder = ImmutableArray.CreateBuilder(Of ExpressionSyntax) - For index = 1 To arguments.Count - 1 - builder.Add(CastAndParenthesize(arguments(index).GetExpression, semanticModel)) - Next - - Return builder.ToImmutable() - End Function - - Protected Overrides Function GetFirstArgument(arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As LiteralExpressionSyntax - Return TryCast(arguments(0)?.GetExpression(), LiteralExpressionSyntax) - End Function - - Protected Overrides Function GetInterpolatedString(text As String) As InterpolatedStringExpressionSyntax - Return CType(ParseExpression("$" + text), InterpolatedStringExpressionSyntax) - End Function - - Protected Overrides Function GetText(arguments As SeparatedSyntaxList(Of ArgumentSyntax)) As String - Dim text = CType(arguments(0).GetExpression, LiteralExpressionSyntax).Token.ToString - ' We need to escape braces as this is an ambiguous case in interpolated strings that is - ' not ambiguous in verbatim strings - Return text.Replace("'{'", "'{{'").Replace("'}'", "'}}'") - End Function - - Protected Overrides Function IsArgumentListCorrect( - invocation As InvocationExpressionSyntax, - invocationSymbol As ISymbol, - formatMethods As ImmutableArray(Of ISymbol), - semanticModel As SemanticModel, - cancellationToken As CancellationToken) As Boolean - If (invocation.ArgumentList IsNot Nothing AndAlso - invocation.ArgumentList.Arguments.Count >= 2 AndAlso - invocation.ArgumentList.Arguments(0).GetExpression().IsKind(SyntaxKind.StringLiteralExpression)) Then - ' We do not want to substitute the expression if it is being passed to params array argument - ' Example: - ' Dim args as String() - ' String.Format("{0}{1}{2}", args) - Return IsArgumentListNotPassingArrayToParams(invocation.ArgumentList.Arguments(1).GetExpression, - invocationSymbol, - formatMethods, - semanticModel, - cancellationToken) - End If - - Return False - End Function - - Protected Overrides Function IsStringLiteral(firstArgument As LiteralExpressionSyntax) As Boolean - Return If(firstArgument Is Nothing, False, firstArgument.Token.IsKind(SyntaxKind.StringLiteralToken)) - End Function - - Protected Overrides Function VisitArguments(expandedArguments As ImmutableArray(Of ExpressionSyntax), interpolatedString As InterpolatedStringExpressionSyntax) As InterpolatedStringExpressionSyntax - Return interpolatedString.ReplaceNodes(interpolatedString.Contents, - Function(oldNode, newNode) - Dim node = TryCast(newNode, InterpolationSyntax) - If node Is Nothing Then - Return newNode - End If - - Dim literalExpression = CType(node.Expression, LiteralExpressionSyntax) - If literalExpression IsNot Nothing AndAlso literalExpression.IsKind(SyntaxKind.NumericLiteralExpression) Then - Dim index = CType(literalExpression.Token.Value, Integer) - If index >= 0 AndAlso index < expandedArguments.Length Then - Return node.WithExpression(expandedArguments(index)) - End If - End If - - Return newNode - End Function) - End Function - - Private Shared Function CastAndParenthesize(expression As ExpressionSyntax, semanticModel As SemanticModel) As ExpressionSyntax - Dim targetType = semanticModel.GetTypeInfo(expression).ConvertedType - Return Parenthesize(Cast(expression, targetType)) - End Function - - Private Shared Function Cast(expression As ExpressionSyntax, targetType As ITypeSymbol) As ExpressionSyntax - If targetType Is Nothing Then - Return expression - End If - - Dim type = ParseTypeName(targetType.ToDisplayString) - Return CTypeExpression(Parenthesize(expression), type).WithAdditionalAnnotations(Simplifier.Annotation) - End Function - - Private Shared Function Parenthesize(expression As ExpressionSyntax) As ExpressionSyntax - Return If(expression.IsKind(SyntaxKind.ParenthesizedExpression), - expression, - ParenthesizedExpression( - openParenToken:=Token(Empty, SyntaxKind.OpenParenToken, Empty), - expression:=expression, - closeParenToken:=Token(Empty, SyntaxKind.CloseParenToken, Empty)). - WithAdditionalAnnotations(Simplifier.Annotation)) + 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 778903afcc990..4d247970c2598 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 f389e67875a12..92a4beab7de92 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs @@ -372,7 +372,7 @@ public static T With( 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/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 26b623afc95f6..dcf8f2db9fede 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 201ea939af414..aa30d07fea9ac 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 4d89abef8ae52..eee0fe90c13b0 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 3e57ffd474c6d..70dfe41ab1988 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 c38701f93a50c..e5c3a9fad6fe2 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 e1803c555e49e..4836c270fad30 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