diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 58b6f0b9c3bd9031d8e939e04f78ade527d91b0f..da45a7f9029adb275e3499292e1830c476c48a58 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 41a958ce95cc8d732db3ac59a2463b1de738c023..f02042e5bff38929495687b9771001a73f943b98 100644 --- a/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs +++ b/src/Compilers/Core/Portable/Syntax/SeparatedSyntaxList.cs @@ -573,5 +573,15 @@ IEnumerator IEnumerable.GetEnumerator() return SpecializedCollections.EmptyEnumerator(); } + + public static implicit operator SeparatedSyntaxList(SeparatedSyntaxList nodes) + { + return new SeparatedSyntaxList(nodes._list); + } + + public static implicit operator SeparatedSyntaxList(SeparatedSyntaxList nodes) + { + return new SeparatedSyntaxList(nodes._list); + } } } diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj index 8ecca19b3b28fb2aefb978a6c3db2b39b77859dc..fa186b39778dc583640b71fd6e071a9856642b0d 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 0000000000000000000000000000000000000000..77ff48956989ffe090e49db1a03f34d4c0233949 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.cs @@ -0,0 +1,527 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.ConvertToInterpolatedString +{ + public class ConvertToInterpolatedStringTests : AbstractCSharpCodeActionTest + { + protected override object CreateCodeRefactoringProvider(Workspace workspace) => + new ConvertToInterpolatedStringRefactoringProvider(); + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestSingleItemSubstitution() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", 1)|]; + } + }", +@"using System; +class T +{ + void M() + { + var a = $""{1}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{1}{2}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{2}{3}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering2() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{2}{1}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{3}{2}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOrdering3() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{0}{0}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{1}{1}{1}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemOutsideRange() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{4}{5}{6}"", 1, 2, 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{4}{5}{6}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemDoNotHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}{1}{2}"", 0.5, ""Hello"", 3)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{0.5}{""Hello""}{3}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemWithSyntaxErrorDoesHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", new object)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{ (object)new object}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestItemWithoutSyntaxErrorDoesNotHaveCast() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", new object())|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{new object()}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestParenthesisAddedForTernaryExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", true ? ""Yes"" : ""No"")|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : ""No"")}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestDoesNotAddDoubleParenthesisForTernaryExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format(""{0}"", (true ? ""Yes"" : ""No""))|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : ""No"")}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestMultiLineExpression() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|string.Format( + ""{0}"", + true + ? ""Yes"" + : false as object)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{(true ? ""Yes"" : false as object)}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + Decimal pricePerOunce = 17.36m; + String s = [|String.Format(""The current price is { 0:C2} per ounce."", + pricePerOunce)|]; + } +}", +@"using System; +class T +{ + void M() + { + Decimal pricePerOunce = 17.36m; + String s = $""The current price is { pricePerOunce:C2} per ounce.""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers2() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + string s = [|String.Format(""It is now {0:d} at {0:t}"", DateTime.Now)|]; + } +}", +@"using System; +class T +{ + void M() + { + string s = $""It is now {DateTime.Now:d} at {DateTime.Now:t}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers3() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + int[] years = { 2013, 2014, 2015 }; + int[] population = { 1025632, 1105967, 1148203 }; + String s = String.Format(""{0,6} {1,15}\n\n"", ""Year"", ""Population""); + for (int index = 0; index < years.Length; index++) + s += [|String.Format(""{0,6} {1,15:N0}\n"", + years[index], population[index])|]; + } +}", +@"using System; +class T +{ + void M() + { + int[] years = { 2013, 2014, 2015 }; + int[] population = { 1025632, 1105967, 1148203 }; + String s = String.Format(""{0,6} {1,15}\n\n"", ""Year"", ""Population""); + for (int index = 0; index < years.Length; index++) + s += $""{years[index],6} {population[index],15:N0}\n""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers4() + { + await TestAsync( +@"using System; +class T +{ + void M() + { + var a = [|String.Format(""{ 0,-10:C}"", 126347.89m)|]; + } +}", +@"using System; +class T +{ + void M() + { + var a = $""{ 126347.89m,-10:C}""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers5() + { + await TestAsync( +@"using System; +public class T +{ + public static void M() + { + Tuple[] cities = + { Tuple.Create(""Los Angeles"", new DateTime(1940, 1, 1), 1504277, + new DateTime(1950, 1, 1), 1970358), + Tuple.Create(""New York"", new DateTime(1940, 1, 1), 7454995, + new DateTime(1950, 1, 1), 7891957), + Tuple.Create(""Chicago"", new DateTime(1940, 1, 1), 3396808, + new DateTime(1950, 1, 1), 3620962), + Tuple.Create(""Detroit"", new DateTime(1940, 1, 1), 1623452, + new DateTime(1950, 1, 1), 1849568) }; + string output; + foreach (var city in cities) + { + output = [|String.Format(""{0,-12}{1,8:yyyy}{2,12:N0}{3,8:yyyy}{4,12:N0}{5,14:P1}"", + city.Item1, city.Item2, city.Item3, city.Item4, city.Item5, + (city.Item5 - city.Item3) / (double)city.Item3)|]; + } + } +}", +@"using System; +public class T +{ + public static void M() + { + Tuple[] cities = + { Tuple.Create(""Los Angeles"", new DateTime(1940, 1, 1), 1504277, + new DateTime(1950, 1, 1), 1970358), + Tuple.Create(""New York"", new DateTime(1940, 1, 1), 7454995, + new DateTime(1950, 1, 1), 7891957), + Tuple.Create(""Chicago"", new DateTime(1940, 1, 1), 3396808, + new DateTime(1950, 1, 1), 3620962), + Tuple.Create(""Detroit"", new DateTime(1940, 1, 1), 1623452, + new DateTime(1950, 1, 1), 1849568) }; + string output; + foreach (var city in cities) + { + output = $""{city.Item1,-12}{city.Item2,8:yyyy}{city.Item3,12:N0}{city.Item4,8:yyyy}{city.Item5,12:N0}{(city.Item5 - city.Item3) / (double)city.Item3,14:P1}""; + } + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatSpecifiers6() + { + await TestAsync( +@"using System; +public class T +{ + public static void M() + { + short[] values = { Int16.MinValue, -27, 0, 1042, Int16.MaxValue }; + foreach (short value in values) + { + string formatString = [|String.Format(""{0,10:G}: {0,10:X}"", value)|]; + } + } +}", +@"using System; +public class T +{ + public static void M() + { + short[] values = { Int16.MinValue, -27, 0, 1042, Int16.MaxValue }; + foreach (short value in values) + { + string formatString = $""{value,10:G}: {value,10:X}""; + } + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestVerbatimStringLiteral() + { + await TestAsync( +@"using System; + +public class T +{ + public static void M() + { + int value1 = 16932; + int value2 = 15421; + string result = [|string.Format(@"" + {0,10} ({0,8:X8}) +And {1,10} ({1,8:X8}) + = {2,10} ({2,8:X8})"", + value1, value2, value1 & value2)|]; + } +}", +@"using System; + +public class T +{ + public static void M() + { + int value1 = 16932; + int value2 = 15421; + string result = $@"" + {value1,10} ({value1,8:X8}) +And {value2,10} ({value2,8:X8}) + = {value1 & value2,10} ({value1 & value2,8:X8})""; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestFormatWithParams() + { + await TestMissingAsync( +@"using System; + +public class T +{ + public static void M() + { + DateTime date1 = new DateTime(2009, 7, 1); + TimeSpan hiTime = new TimeSpan(14, 17, 32); + decimal hiTemp = 62.1m; + TimeSpan loTime = new TimeSpan(3, 16, 10); + decimal loTemp = 54.8m; + + string result = [|String.Format(@""Temperature on {0:d}: + {1,11}: {2} degrees (hi) + {3,11}: {4} degrees (lo)"", + new object[] { date1, hiTime, hiTemp, loTime, loTemp })|]; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertToInterpolatedString)] + public async Task TestInvalidInteger() + { + await TestAsync( +@"using System; + +public class T +{ + public static void M() + { + string result = [|String.Format(""{0L}"", 5)|]; + } +}", +@"using System; + +public class T +{ + public static void M() + { + string result = $""{5}""; + } +}"); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs index 384bb2ea19bd48e7c804798e09ccdcb34ee5cc7e..d0b301120056b7b9fecdd6e5241afef0262a4950 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CrefCompletionProviderTests.cs @@ -455,11 +455,6 @@ public bool ContainsInMemberBody(SyntaxNode node, TextSpan span) throw new NotImplementedException(); } - public SyntaxNode ConvertToSingleLine(SyntaxNode node) - { - throw new NotImplementedException(); - } - public SyntaxToken FindTokenOnLeftOfPosition(SyntaxNode node, int position, bool includeSkipped = true, bool includeDirectives = false, bool includeDocumentationComments = false) { throw new NotImplementedException(); @@ -807,7 +802,7 @@ public bool IsStartOfUnicodeEscapeSequence(char c) throw new NotImplementedException(); } - public bool IsStringLiteral(SyntaxToken token) + public bool IsStringLiteralOrInterpolatedStringLiteral(SyntaxToken token) { throw new NotImplementedException(); } @@ -961,6 +956,36 @@ public bool IsOperandOfIncrementExpression(SyntaxNode node) { throw new NotImplementedException(); } + + public bool IsNumericLiteralExpression(SyntaxNode node) + { + throw new NotImplementedException(); + } + + public SyntaxNode GetExpressionOfInterpolation(SyntaxNode node) + { + throw new NotImplementedException(); + } + + public SyntaxList GetContentsOfInterpolatedString(SyntaxNode interpolatedString) + { + throw new NotImplementedException(); + } + + public bool IsStringLiteral(SyntaxToken token) + { + throw new NotImplementedException(); + } + + public SeparatedSyntaxList GetArgumentsForInvocationExpression(SyntaxNode invocationExpression) + { + throw new NotImplementedException(); + } + + public SyntaxNode ConvertToSingleLine(SyntaxNode node, bool useElasticTrivia = false) + { + throw new NotImplementedException(); + } } } } diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index 1fc5c58682c54c2df67b9ffa99a581c1fc759c5a..f26f5a910076be53061d95eb5e893919027405e5 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 b67be1915ea3ba44d362bed4658c25ba5e46c127..5abbec5950e270fd5dc911e21eadbfe226e838d7 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 0000000000000000000000000000000000000000..2715ef2fd44f13d39e41e1ed42c602e9e19478dd --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/CodeActions/ConvertToInterpolatedString/ConvertToInterpolatedStringTests.vb @@ -0,0 +1,420 @@ +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings + +Public Class ConvertToInterpolatedStringTests + Inherits AbstractVisualBasicCodeActionTest + + Protected Overrides Function CreateCodeRefactoringProvider(workspace As Workspace) As Object + Return New ConvertToInterpolatedStringRefactoringProvider() + End Function + + + Public Async Function TestSingleItemSubstitution() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", 1)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{2}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{2}{1}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{3}{2}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOrdering3() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{0}{0}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{1}{1}{1}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemOutsideRange() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{4}{5}{6}", 1, 2, 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{4}{5}{6}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemDoNotHaveCast() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 0.5, "Hello", 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{0.5}{"Hello"}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestItemWithoutSyntaxErrorDoesNotHaveCast() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}{1}{2}", 0.5, "Hello", 3)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{0.5}{"Hello"}{3}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestPreserveParenthesis() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", (New Object))|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{(New Object)}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestMultiLineExpression() As Task + Dim text = +Imports System +Module T + Sub M() + Dim a = [|String.Format("{0}", If(True, + "Yes", + TryCast(False, Object)))|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim a = $"{If(True, + "Yes", + TryCast(False, Object))}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers() As Task + Dim text = +Imports System +Module T + Sub M() + Dim pricePerOunce As Decimal = 17.36 + Dim s = [|String.Format("The current price Is {0:C2} per ounce.", + pricePerOunce)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim pricePerOunce As Decimal = 17.36 + Dim s = $"The current price Is {pricePerOunce:C2} per ounce." + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim s = [|String.Format("It Is now {0:d} at {0:T}", DateTime.Now)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim s = $"It Is now {DateTime.Now:d} at {DateTime.Now:T}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers3() As Task + Dim text = +Imports System +Module T + Sub M() + Dim years As Integer() = {2013, 2014, 2015} + Dim population As Integer() = {1025632, 1105967, 1148203} + Dim s = String.Format("{0,6} {1,15}\n\n", "Year", "Population") + For index = 0 To years.Length - 1 + s += [|String.Format("{0, 6} {1, 15: N0}\n", + years(index), population(index))|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim years As Integer() = {2013, 2014, 2015} + Dim population As Integer() = {1025632, 1105967, 1148203} + Dim s = String.Format("{0,6} {1,15}\n\n", "Year", "Population") + For index = 0 To years.Length - 1 + s += $"{years(index),6} {population(index),15: N0}\n" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers4() As Task + Dim text = +Imports System +Module T + Sub M() + Dim s = [|String.Format("{0,-10:C}", 126347.89)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim s = $"{126347.89,-10:C}" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers5() As Task + Dim text = +Imports System +Module T + Sub M() + Dim cities As Tuple(Of String, DateTime, Integer, DateTime, Integer)() = + {Tuple.Create("Los Angeles", New DateTime(1940, 1, 1), 1504277, + New DateTime(1950, 1, 1), 1970358), + Tuple.Create("New York", New DateTime(1940, 1, 1), 7454995, + New DateTime(1950, 1, 1), 7891957), + Tuple.Create("Chicago", New DateTime(1940, 1, 1), 3396808, + New DateTime(1950, 1, 1), 3620962), + Tuple.Create("Detroit", New DateTime(1940, 1, 1), 1623452, + New DateTime(1950, 1, 1), 1849568)} + Dim output As String + For Each city In cities + output = [|String.Format("{0,-12}{1,8:yyyy}{2,12:N0}{3,8:yyyy}{4,12:N0}{5,14:P1}", + city.Item1, city.Item2, city.Item3, city.Item4, city.Item5, + (city.Item5 - city.Item3) / CType(city.Item3, Double))|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim cities As Tuple(Of String, DateTime, Integer, DateTime, Integer)() = + {Tuple.Create("Los Angeles", New DateTime(1940, 1, 1), 1504277, + New DateTime(1950, 1, 1), 1970358), + Tuple.Create("New York", New DateTime(1940, 1, 1), 7454995, + New DateTime(1950, 1, 1), 7891957), + Tuple.Create("Chicago", New DateTime(1940, 1, 1), 3396808, + New DateTime(1950, 1, 1), 3620962), + Tuple.Create("Detroit", New DateTime(1940, 1, 1), 1623452, + New DateTime(1950, 1, 1), 1849568)} + Dim output As String + For Each city In cities + output = $"{city.Item1,-12}{city.Item2,8:yyyy}{city.Item3,12:N0}{city.Item4,8:yyyy}{city.Item5,12:N0}{(city.Item5 - city.Item3) / CType(city.Item3, Double),14:P1}" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestFormatSpecifiers6() As Task + Dim text = +Imports System +Module T + Sub M() + Dim values As Short() = {Int16.MaxValue, -27, 0, 1042, Int16.MaxValue} + For Each value In values + Dim s = [|String.Format("{0,10:G}: {0,10:X}", value)|] + Next + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim values As Short() = {Int16.MaxValue, -27, 0, 1042, Int16.MaxValue} + For Each value In values + Dim s = $"{value,10:G}: {value,10:X}" + Next + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestMultilineStringLiteral2() As Task + Dim text = +Imports System +Module T + Sub M() + Dim value1 = 16932 + Dim value2 = 15421 + Dim result = [|String.Format(" + {0,10} ({0,8:X8}) +And {1,10} ({1,8:X8}) + = {2,10} ({2,8:X8})", + value1, value2, value1 And value2)|] + End Sub +End Module.ConvertTestSourceTag() + + Dim expected = +Imports System +Module T + Sub M() + Dim value1 = 16932 + Dim value2 = 15421 + Dim result = $" + {value1,10} ({value1,8:X8}) +And {value2,10} ({value2,8:X8}) + = {value1 And value2,10} ({value1 And value2,8:X8})" + End Sub +End Module.ConvertTestSourceTag() + + Await TestAsync(text, expected) + End Function + + + Public Async Function TestParamsArray() As Task + Dim text = +Imports System +Module T + Sub M(args As String()) + Dim s = [|String.Format("{0}", args)|] + End Sub +End Module.ConvertTestSourceTag() + Await TestMissingAsync(text) + End Function +End Class diff --git a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb index d40810ea07cd81ce9f70e1fa6c24bb7d067c5523..9cbc3ebfb3bdc7c778e59545ddeb28c2409534d8 100644 --- a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/CrefCompletionProviderTests.vb @@ -454,10 +454,6 @@ End Class]]>.Value.NormalizeLineEndings() Throw New NotImplementedException() End Function - Public Function ConvertToSingleLine(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine - Throw New NotImplementedException() - End Function - Public Function FindTokenOnLeftOfPosition(node As SyntaxNode, position As Integer, Optional includeSkipped As Boolean = True, Optional includeDirectives As Boolean = False, Optional includeDocumentationComments As Boolean = False) As SyntaxToken Implements ISyntaxFactsService.FindTokenOnLeftOfPosition Throw New NotImplementedException() End Function @@ -733,7 +729,7 @@ End Class]]>.Value.NormalizeLineEndings() Throw New NotImplementedException() End Function - Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Public Function IsStringLiteralOrInterpolatedStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteralOrInterpolatedStringLiteral Throw New NotImplementedException() End Function @@ -852,6 +848,30 @@ End Class]]>.Value.NormalizeLineEndings() Public Function IsOperandOfIncrementOrDecrementExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsOperandOfIncrementOrDecrementExpression Throw New NotImplementedException() End Function + + Public Function IsNumericLiteralExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsNumericLiteralExpression + Throw New NotImplementedException() + End Function + + Public Function GetExpressionOfInterpolation(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.GetExpressionOfInterpolation + Throw New NotImplementedException() + End Function + + Public Function GetContentsOfInterpolatedString(interpolatedString As SyntaxNode) As SyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetContentsOfInterpolatedString + Throw New NotImplementedException() + End Function + + Public Function IsStringLiteral(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsStringLiteral + Throw New NotImplementedException() + End Function + + Public Function GetArgumentsForInvocationExpression(invocationExpression As SyntaxNode) As SeparatedSyntaxList(Of SyntaxNode) Implements ISyntaxFactsService.GetArgumentsForInvocationExpression + Throw New NotImplementedException() + End Function + + Public Function ConvertToSingleLine(node As SyntaxNode, Optional useElasticTrivia As Boolean = False) As SyntaxNode Implements ISyntaxFactsService.ConvertToSingleLine + Throw New NotImplementedException() + End Function End Class End Class End Namespace diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index ddc1d128a3de5ec7d0a69c689d91b07175a33c57..cc33c05ca21c947a5318fb160a648affeb68d999 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -92,6 +92,7 @@ + diff --git a/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..2cf9375a0d41e68936494b0347bb50420801957e --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ConvertToInterpolatedString +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString), Shared] + internal partial class ConvertToInterpolatedStringRefactoringProvider : + AbstractConvertToInterpolatedStringRefactoringProvider + { + protected override SyntaxNode GetInterpolatedString(string text) => + ParseExpression("$" + text) as InterpolatedStringExpressionSyntax; + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5fa5236f34df22e78a4e0320ce535233e8a2791 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/ConvertToInterpolatedString/AbstractConvertToInterpolatedStringRefactoringProvider.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Formatting; + +namespace Microsoft.CodeAnalysis.CodeRefactorings +{ + internal abstract class AbstractConvertToInterpolatedStringRefactoringProvider : CodeRefactoringProvider + where TExpressionSyntax : SyntaxNode + where TInvocationExpressionSyntax : TExpressionSyntax + where TArgumentSyntax : SyntaxNode + where TLiteralExpressionSyntax : SyntaxNode + { + protected abstract SyntaxNode GetInterpolatedString(string text); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + var stringType = semanticModel.Compilation.GetSpecialType(SpecialType.System_String); + if (stringType == null) + { + return; + } + + var formatMethods = stringType + .GetMembers("Format") + .OfType() + .Where(ShouldIncludeFormatMethod) + .ToImmutableArray(); + + if (formatMethods.Length == 0) + { + return; + } + + var syntaxFactsService = context.Document.GetLanguageService(); + if (syntaxFactsService == null) + { + return; + } + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + TInvocationExpressionSyntax invocation; + ISymbol invocationSymbol; + if (TryFindInvocation(context.Span, root, semanticModel, formatMethods, syntaxFactsService, context.CancellationToken, out invocation, out invocationSymbol) && + IsArgumentListCorrect(syntaxFactsService.GetArgumentsForInvocationExpression(invocation), invocationSymbol, formatMethods, semanticModel, syntaxFactsService, context.CancellationToken)) + { + context.RegisterRefactoring( + new ConvertToInterpolatedStringCodeAction( + FeaturesResources.ConvertToInterpolatedString, + c => CreateInterpolatedString(invocation, context.Document, syntaxFactsService, c))); + } + } + + private bool TryFindInvocation( + TextSpan span, + SyntaxNode root, + SemanticModel semanticModel, + ImmutableArray formatMethods, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken, + out TInvocationExpressionSyntax invocation, + out ISymbol invocationSymbol) + { + invocationSymbol = null; + invocation = root.FindNode(span, getInnermostNodeForTie: true)?.FirstAncestorOrSelf(); + while (invocation != null) + { + var arguments = syntaxFactsService.GetArgumentsForInvocationExpression(invocation); + if (arguments.Count >= 2) + { + var firstArgumentExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + if (firstArgumentExpression != null && syntaxFactsService.IsStringLiteral(firstArgumentExpression.GetFirstToken())) + { + invocationSymbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol; + if (formatMethods.Contains(invocationSymbol)) + { + break; + } + } + } + + invocation = invocation.Parent?.FirstAncestorOrSelf(); + } + + return invocation != null; + } + + private bool IsArgumentListCorrect( + SeparatedSyntaxList? nullableArguments, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken) + { + var arguments = nullableArguments.Value; + var firstExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + if (arguments.Count >= 2 && + firstExpression != null && + syntaxFactsService.IsStringLiteral(firstExpression.GetFirstToken())) + { + // We do not want to substitute the expression if it is being passed to params array argument + // Example: + // string[] args; + // String.Format("{0}{1}{2}", args); + return IsArgumentListNotPassingArrayToParams( + syntaxFactsService.GetExpressionOfArgument(arguments[1]), + invocationSymbol, + formatMethods, + semanticModel, + cancellationToken); + } + + return false; + } + + + private async Task CreateInterpolatedString( + TInvocationExpressionSyntax invocation, + Document document, + ISyntaxFactsService syntaxFactsService, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var arguments = syntaxFactsService.GetArgumentsForInvocationExpression(invocation); + var literalExpression = syntaxFactsService.GetExpressionOfArgument(arguments[0]) as TLiteralExpressionSyntax; + var text = literalExpression.GetFirstToken().ToString(); + var syntaxGenerator = document.Project.LanguageServices.GetService(); + var expandedArguments = GetExpandedArguments(semanticModel, arguments, syntaxGenerator, syntaxFactsService); + var interpolatedString = GetInterpolatedString(text); + var newInterpolatedString = VisitArguments(expandedArguments, interpolatedString, syntaxFactsService); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newRoot = root.ReplaceNode(invocation, newInterpolatedString.WithTriviaFrom(invocation)); + return document.WithSyntaxRoot(newRoot); + } + + private ImmutableArray GetExpandedArguments( + SemanticModel semanticModel, + SeparatedSyntaxList arguments, + SyntaxGenerator syntaxGenerator, + ISyntaxFactsService syntaxFactsService) + { + var builder = ImmutableArray.CreateBuilder(); + for (int i = 1; i < arguments.Count; i++) + { + var argumentExpression = syntaxFactsService.GetExpressionOfArgument(arguments[i]); + var convertedType = semanticModel.GetTypeInfo(argumentExpression).ConvertedType; + if (convertedType == null) + { + builder.Add(syntaxFactsService.Parenthesize(argumentExpression) as TExpressionSyntax); + } + else + { + var castExpression = syntaxGenerator.CastExpression(convertedType, syntaxFactsService.Parenthesize(argumentExpression)).WithAdditionalAnnotations(Simplifier.Annotation); + builder.Add(castExpression as TExpressionSyntax); + } + } + + var expandedArguments = builder.ToImmutable(); + return expandedArguments; + } + + private SyntaxNode VisitArguments( + ImmutableArray expandedArguments, + SyntaxNode interpolatedString, + ISyntaxFactsService syntaxFactsService) + { + return interpolatedString.ReplaceNodes(syntaxFactsService.GetContentsOfInterpolatedString(interpolatedString), (oldNode, newNode) => + { + var interpolationSyntaxNode = newNode; + if (interpolationSyntaxNode != null) + { + var literalExpression = syntaxFactsService.GetExpressionOfInterpolation(interpolationSyntaxNode) as TLiteralExpressionSyntax; + if (literalExpression != null && syntaxFactsService.IsNumericLiteralExpression(literalExpression)) + { + int index; + + if (int.TryParse(literalExpression.GetFirstToken().ValueText, out index)) + { + if (index >= 0 && index < expandedArguments.Length) + { + return interpolationSyntaxNode.ReplaceNode( + syntaxFactsService.GetExpressionOfInterpolation(interpolationSyntaxNode), + syntaxFactsService.ConvertToSingleLine(expandedArguments[index], useElasticTrivia: true).WithAdditionalAnnotations(Formatter.Annotation)); + } + } + } + } + + return newNode; + }); + } + + private static bool ShouldIncludeFormatMethod(IMethodSymbol methodSymbol) + { + if (!methodSymbol.IsStatic) + { + return false; + } + + if (methodSymbol.Parameters.Length == 0) + { + return false; + } + + var firstParameter = methodSymbol.Parameters[0]; + if (firstParameter?.Name != "format") + { + return false; + } + + return true; + } + + private static bool IsArgumentListNotPassingArrayToParams( + SyntaxNode expression, + ISymbol invocationSymbol, + ImmutableArray formatMethods, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var formatMethodsAcceptingParamsArray = formatMethods + .Where(x => x.Parameters.Length > 1 && x.Parameters[1].Type.Kind == SymbolKind.ArrayType); + if (formatMethodsAcceptingParamsArray.Contains(invocationSymbol)) + { + return semanticModel.GetTypeInfo(expression, cancellationToken).Type?.Kind != SymbolKind.ArrayType; + } + + return true; + } + + private class ConvertToInterpolatedStringCodeAction : CodeAction.DocumentChangeAction + { + public ConvertToInterpolatedStringCodeAction(string title, Func> createChangedDocument) : + base(title, createChangedDocument) + { + } + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index f36cae8e6ede63bec6ffe34ab1e22fac289f1704..a4da83de80be9bb2aac7ebfebb831433a156e451 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 78f02508c43d5a2586760260a58983fa23c17108..ee309cb1f1b5066eb67b3fd5e25e95d0a3eb8eab 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 5744a5a490b6b1ada094660ec92ecdda638d9a19..4c7efa8bc563bca407ce4d98bac2df64f1d1afde 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -619,6 +619,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Convert to interpolated string. + /// + internal static string ConvertToInterpolatedString { + get { + return ResourceManager.GetString("ConvertToInterpolatedString", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not extract interface: The selection is not inside a class/interface/struct.. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index e0f9a62c051859937038672d49ce0cb58acb34a6..7697d82e53ab458071507de6c5a002402d35088a 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 fde63f0a791fd17b98d55ea9780b45a3e3af2316..5bd6d9fa290f7d20f8fd2bc4744f7e22b144b613 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -126,6 +126,7 @@ + diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb new file mode 100644 index 0000000000000000000000000000000000000000..5095a1ece4f3d6121946636c92460b62cf45357a --- /dev/null +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/ConvertToInterpolatedString/ConvertToInterpolatedStringRefactoringProvider.vb @@ -0,0 +1,15 @@ +Imports System.Composition +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory + + +Partial Friend Class ConvertToInterpolatedStringRefactoringProvider + Inherits AbstractConvertToInterpolatedStringRefactoringProvider(Of InvocationExpressionSyntax, ExpressionSyntax, ArgumentSyntax, LiteralExpressionSyntax) + + Protected Overrides Function GetInterpolatedString(text As String) As SyntaxNode + Return TryCast(ParseExpression("$" + text), InterpolatedStringExpressionSyntax) + End Function +End Class diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.SingleLineRewriter.cs index 778903afcc9907b4a233767207f303799c6a30e3..4d247970c25980ed09ef081603b4c319184d2a5b 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 f389e67875a1245238d7e0aa5a33b07e806f2972..92a4beab7de92b7eb683fbf85c05b1fa226fe707 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs @@ -372,7 +372,7 @@ public static bool IsReturnableConstruct(this SyntaxNode node) return node.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia); } - public static TNode ConvertToSingleLine(this TNode node) + public static TNode ConvertToSingleLine(this TNode node, bool useElasticTrivia = false) where TNode : SyntaxNode { if (node == null) @@ -380,7 +380,7 @@ public static TNode ConvertToSingleLine(this TNode node) return node; } - var rewriter = new SingleLineRewriter(); + var rewriter = new SingleLineRewriter(useElasticTrivia); return (TNode)rewriter.Visit(node); } diff --git a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs index e79d5a430ce6cda65e52b14ed8adb5b71669dcb4..8fbc3f0a692db7bb98c20941ddc784f286255987 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/Rules/SuppressFormattingRule.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Options; +using System.Linq; namespace Microsoft.CodeAnalysis.CSharp.Formatting { @@ -174,6 +175,12 @@ private void AddSpecificNodesSuppressOperations(List list, Sy AddSuppressWrappingIfOnSingleLineOperation(list, finallyClause.FinallyKeyword, finallyClause.Block.CloseBraceToken); } } + + var interpolatedStringExpression = node as InterpolatedStringExpressionSyntax; + if (interpolatedStringExpression != null) + { + AddSuppressWrappingIfOnSingleLineOperation(list, interpolatedStringExpression.StringStartToken, interpolatedStringExpression.StringEndToken); + } } private void AddStatementExceptBlockSuppressOperations(List list, SyntaxNode node) diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 26b623afc95f67ac7151e3132eb62b84f1d15279..dcf8f2db9fedeb790e0a45e0f5259234af13f19d 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 201ea939af4149140a6404dfc6a50bb026a03c44..aa30d07fea9ac4cf1eae6765ce56d72badd2d797 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 4d89abef8ae5296bd1b40119b34e0fda8728ada3..eee0fe90c13b05004e2b18017458421cbd4f18c9 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 3e57ffd474c6db5cd286055a64d8cc3b590bbb1b..70dfe41ab19885680f22b6c6bef60169e81c000d 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 c38701f93a50cfa3701fc97095bb416d8e6565ac..e5c3a9fad6fe28fa730dc44b2ec49870df480ef5 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 e1803c555e49e7867b98261b538d80e348d11dc2..4836c270fad30cd4951e230ee111ded14626131d 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