From e3b9c5d43cd3a7b74821ce1be30569726ef7f7d7 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Thu, 13 Apr 2017 22:56:50 -0700 Subject: [PATCH] Refactoring on inferred tuple names (C#): Complexify makes tuple inferred names explicit, and simplify removes them. Do the same for anonymous types --- .../CSharp/Portable/PublicAPI.Unshipped.txt | 4 +- .../CSharp/Portable/Syntax/SyntaxFacts.cs | 24 ++ .../Portable/Syntax/SyntaxNodeExtensions.cs | 4 +- .../Emit/CodeGen/CodeGenDeconstructTests.cs | 30 ++ .../Test/Emit/CodeGen/CodeGenTupleTest.cs | 100 +++++ .../CSharp/Test/Syntax/Syntax/SyntaxTests.cs | 32 ++ .../Test/Emit/CodeGen/CodeGenTuples.vb | 90 ++++- .../ExtractMethod/ExtractMethodTests.cs | 30 ++ .../InlineTemporary/InlineTemporaryTests.cs | 358 ++++++++++++++++++ .../IntroduceVariableTests.cs | 203 ++++++++++ .../UpgradeProject/UpgradeProjectTests.cs | 34 ++ ...IntroduceVariableService_IntroduceLocal.cs | 4 +- .../CSharp/Portable/CSharpWorkspace.csproj | 2 + ...ParenthesizedExpressionSyntaxExtensions.cs | 15 + ...SharpReducer.AbstractExpressionRewriter.cs | 10 + ...SharpInferredMemberNameReducer.Rewriter.cs | 36 ++ .../CSharpInferredMemberNameReducer.cs | 52 +++ .../CSharpSimplificationService.Expander.cs | 74 ++++ .../CSharpSimplificationService.cs | 37 +- .../Portable/Simplification/Simplifier.cs | 12 +- 20 files changed, 1138 insertions(+), 13 deletions(-) create mode 100755 src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.Rewriter.cs create mode 100755 src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.cs diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index ca0ea725229..b167ff18644 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -4,4 +4,6 @@ Microsoft.CodeAnalysis.CSharp.SyntaxKind.ConflictMarkerTrivia = 8564 -> Microsof Microsoft.CodeAnalysis.CSharp.SyntaxKind.DefaultLiteralExpression = 8755 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind static Microsoft.CodeAnalysis.CSharp.LanguageVersionFacts.MapSpecifiedToEffectiveVersion(this Microsoft.CodeAnalysis.CSharp.LanguageVersion version) -> Microsoft.CodeAnalysis.CSharp.LanguageVersion static Microsoft.CodeAnalysis.CSharp.LanguageVersionFacts.ToDisplayString(this Microsoft.CodeAnalysis.CSharp.LanguageVersion version) -> string -static Microsoft.CodeAnalysis.CSharp.LanguageVersionFacts.TryParse(this string version, out Microsoft.CodeAnalysis.CSharp.LanguageVersion result) -> bool \ No newline at end of file +static Microsoft.CodeAnalysis.CSharp.LanguageVersionFacts.TryParse(this string version, out Microsoft.CodeAnalysis.CSharp.LanguageVersion result) -> bool +static Microsoft.CodeAnalysis.CSharp.SyntaxFacts.IsReservedTupleElementName(string elementName) -> bool +static Microsoft.CodeAnalysis.CSharp.SyntaxFacts.TryGetInferredMemberName(this Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax input) -> string \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs index 0b57907c44b..177ac0d7ac5 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CSharp.SyntaxKind; @@ -425,5 +426,28 @@ internal static bool IsDeclarationExpressionType(SyntaxNode node, out Declaratio parent = node.Parent as DeclarationExpressionSyntax; return node == parent?.Type; } + + /// + /// Given an initializer expression infer the name of anonymous property or tuple element. + /// Returns null if unsuccessful + /// + public static string TryGetInferredMemberName(this ExpressionSyntax input) + { + var nameToken = input.ExtractAnonymousTypeMemberName(); + return nameToken.Kind() == SyntaxKind.IdentifierToken ? nameToken.ValueText : null; + } + + /// + /// Checks whether the element name is reserved. + /// + /// For example: + /// "Item3" is reserved (at certain positions). + /// "Rest", "ToString" and other members of System.ValueTuple are reserved (in any position). + /// Names that are not reserved return false. + /// + public static bool IsReservedTupleElementName(string elementName) + { + return TupleTypeSymbol.IsElementNameReserved(elementName) != -1; + } } } \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeExtensions.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeExtensions.cs index 9c173869e2b..25e0c0217ca 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeExtensions.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeExtensions.cs @@ -117,8 +117,8 @@ internal static CSharpSyntaxNode AnonymousFunctionBody(this SyntaxNode lambda) } /// - /// Given an initializer expression infer the name of anonymous property. - /// Returns default(SyntaxToken) if unsuccessful + /// Given an initializer expression infer the name of anonymous property or tuple element. + /// Returns default if unsuccessful /// internal static SyntaxToken ExtractAnonymousTypeMemberName(this ExpressionSyntax input) { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs index 936b74be86e..e7b24c03ac1 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs @@ -4929,6 +4929,36 @@ static void Main() Assert.Equal("System.Int32", model.GetTypeInfo(discard).Type.ToTestDisplayString()); } + [Fact] + public void EscapedUnderscoreInDeclaration() + { + var source = +@" +class C +{ + static void Main() + { + (@_, var x) = (1, 2); + } +} +"; + + var comp = CreateStandardCompilation(source, options: TestOptions.DebugExe, references: s_valueTupleRefs); + comp.VerifyDiagnostics( + // (6,10): error CS0103: The name '_' does not exist in the current context + // (@_, var x) = (1, 2); + Diagnostic(ErrorCode.ERR_NameNotInContext, "@_").WithArguments("_").WithLocation(6, 10), + // (6,9): error CS8184: A deconstruction cannot mix declarations and expressions on the left-hand-side. + // (@_, var x) = (1, 2); + Diagnostic(ErrorCode.ERR_MixedDeconstructionUnsupported, "(@_, var x)").WithLocation(6, 9) + ); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + + Assert.Empty(GetDiscardIdentifiers(tree)); + } + [Fact] public void UnderscoreLocalInDeconstructDeclaration() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs index dc4945c21e7..481daf0d8ce 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs @@ -3664,6 +3664,54 @@ void M() verifier.VerifyDiagnostics(); } + [Fact] + public void TupleCreationWithInferredNames2() + { + var source = @" +class C +{ + private int e = 5; +} +class C2 +{ + C instance = null; + C2 instance2 = null; + int M() + { + var y = (instance?.e, (instance.e, instance2.M(), checked(instance.e), default(int))); + System.Console.Write(y); + return 42; + } +} +"; + + var compilation = CreateStandardCompilation(source, parseOptions: TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp7_1), + references: new[] { MscorlibRef, ValueTupleRef, SystemRuntimeFacadeRef }); + compilation.VerifyDiagnostics( + // (12,27): error CS0122: 'C.e' is inaccessible due to its protection level + // var y = (instance?.e, (instance.e, instance2.M(), checked(instance.e), default(int))); + Diagnostic(ErrorCode.ERR_BadAccess, ".e").WithArguments("C.e").WithLocation(12, 27), + // (12,41): error CS0122: 'C.e' is inaccessible due to its protection level + // var y = (instance?.e, (instance.e, instance2.M(), checked(instance.e), default(int))); + Diagnostic(ErrorCode.ERR_BadAccess, "e").WithArguments("C.e").WithLocation(12, 41), + // (12,76): error CS0122: 'C.e' is inaccessible due to its protection level + // var y = (instance?.e, (instance.e, instance2.M(), checked(instance.e), default(int))); + Diagnostic(ErrorCode.ERR_BadAccess, "e").WithArguments("C.e").WithLocation(12, 76), + // (4,17): warning CS0414: The field 'C.e' is assigned but its value is never used + // private int e = 5; + Diagnostic(ErrorCode.WRN_UnreferencedFieldAssg, "e").WithArguments("C.e") + ); + + var tree = compilation.SyntaxTrees.First(); + var model = compilation.GetSemanticModel(tree); + var nodes = tree.GetCompilationUnitRoot().DescendantNodes(); + + // PROTOTYPE(tuple-names) The type for int? was not picked up + var yTuple = nodes.OfType().ElementAt(0); + Assert.Equal("(? e, (System.Int32 e, System.Int32, System.Int32, System.Int32))", + model.GetTypeInfo(yTuple).Type.ToTestDisplayString()); + } + [Fact] public void InferredNamesInLinq() { @@ -3700,6 +3748,58 @@ static void M(IEnumerable list) verifier.VerifyDiagnostics(); } + [Fact] + public void InferredNamesInTernary() + { + var source = @" +class C +{ + static void Main() + { + var i = 1; + var flag = false; + var t = flag ? (i, 2) : (i, 3); + System.Console.Write(t.i); + } +} +"; + + var verifier = CompileAndVerify(source, parseOptions: TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp7_1), + additionalRefs: new[] { MscorlibRef, ValueTupleRef, SystemRuntimeFacadeRef, LinqAssemblyRef }, + expectedOutput:"1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void InferredNames_ExtensionInvokedInCSharp7ButNotCSharp7_1() + { + var source = @" +using System; +class C +{ + static void Main() + { + Action M = () => Console.Write(""lambda""); + (1, M).M(); + } +} +static class Extension +{ + public static void M(this (int, Action) t) => Console.Write(""extension""); +} +"; + + var verifier7 = CompileAndVerify(source, parseOptions: TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp7), + additionalRefs: new[] { MscorlibRef, ValueTupleRef, SystemRuntimeFacadeRef, SystemCoreRef }, + expectedOutput: "extension"); + verifier7.VerifyDiagnostics(); + + var verifier7_1 = CompileAndVerify(source, parseOptions: TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp7_1), + additionalRefs: new[] { MscorlibRef, ValueTupleRef, SystemRuntimeFacadeRef, SystemCoreRef }, + expectedOutput: "lambda"); + verifier7_1.VerifyDiagnostics(); + } + [Fact] public void LongTupleWithArgumentEvaluation() { diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxTests.cs index 06a0567bc99..e322ad50155 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxTests.cs @@ -156,5 +156,37 @@ public void TestBug991510() var span = section.Span; Assert.Equal(default(TextSpan), span); } + + [Theory] + [InlineData("x", "x")] + [InlineData("x.y", "y")] + [InlineData("x?.y", "y")] + [InlineData("this.y", "y")] + [InlineData("M()", null)] + [InlineData("x.M()", null)] + [InlineData("default(x)", null)] + [InlineData("typeof(x)", null)] + public void ExtractAnonymousTypeMemberName(string source, string expected) + { + var expr = SyntaxFactory.ParseExpression(source, options: TestOptions.Regular); + var actual = SyntaxFacts.TryGetInferredMemberName(expr); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("Item0", false)] + [InlineData("Item1", true)] + [InlineData("Item2", true)] + [InlineData("Item10", true)] + [InlineData("Rest", true)] + [InlineData("ToString", true)] + [InlineData("GetHashCode", true)] + [InlineData("item1", false)] + [InlineData("item10", false)] + [InlineData("Alice", false)] + public void IsReservedTupleElementName(string elementName, bool isReserved) + { + Assert.Equal(isReserved, SyntaxFacts.IsReservedTupleElementName(elementName)); + } } } diff --git a/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenTuples.vb b/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenTuples.vb index 65ffd1f6cf3..73157aa211e 100644 --- a/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenTuples.vb +++ b/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenTuples.vb @@ -5896,7 +5896,7 @@ End Class End Sub - Public Sub TupleCreationWithInferredNamesWithVB15_3() + Public Sub TupleCreationWithInferredNames() Dim verifier = CompileAndVerify( @@ -5939,6 +5939,39 @@ End Class verifier.VerifyDiagnostics() End Sub + + Public Sub TupleCreationWithInferredNames2() + Dim verifier = CompileAndVerify( + + +Class C + Dim e As Integer = 5 + Dim instance As C = Nothing + Function M() As Integer + Dim y As (Integer?, object) = (instance?.e, (e, instance.M())) + System.Console.Write(y) + Return 42 + End Function +End Class + +, + additionalRefs:=s_valueTupleRefs, + parseOptions:=TestOptions.Regular.WithLanguageVersion(LanguageVersion.VisualBasic15_3), + sourceSymbolValidator:= + Sub(m As ModuleSymbol) + Dim compilation = m.DeclaringCompilation + Dim tree = compilation.SyntaxTrees.First() + Dim model = compilation.GetSemanticModel(tree) + Dim nodes = tree.GetCompilationUnitRoot().DescendantNodes() + + Dim yTuple = nodes.OfType(Of TupleExpressionSyntax)().ElementAt(0) + Assert.Equal("(e As System.Nullable(Of System.Int32), (e As System.Int32, M As System.Int32))", + model.GetTypeInfo(yTuple).Type.ToTestDisplayString()) + End Sub) + + verifier.VerifyDiagnostics() + End Sub + Public Sub MissingMemberAccessWithVB15() Dim comp = CreateCompilationWithMscorlibAndVBRuntime( @@ -6030,6 +6063,61 @@ End Class verifier.VerifyDiagnostics() End Sub + + Public Sub InferredNamesInTernary() + Dim verifier = CompileAndVerify( + + +Class C + Shared Sub Main() + Dim i = 1 + Dim flag = False + Dim t = If(flag, (i, 2), (i, 3)) + System.Console.Write(t.i) + End Sub +End Class + +, + additionalRefs:={ValueTupleRef, SystemRuntimeFacadeRef, LinqAssemblyRef}, + parseOptions:=TestOptions.Regular.WithLanguageVersion(LanguageVersion.VisualBasic15_3), + expectedOutput:="1") + + verifier.VerifyDiagnostics() + End Sub + + + Public Sub InferredNames_ExtensionInvokedInVB15ButNotVB15_3() + Dim source = + +Imports System +Class C + Shared Sub Main() + Dim M As Action = Sub() Console.Write("lambda") + Dim t = (1, M) + t.M() + End Sub +End Class +Module Extensions + <System.Runtime.CompilerServices.Extension()> + Public Sub M(self As (Integer, Action)) + Console.Write("extension") + End Sub +End Module + + + Dim verifier15 = CompileAndVerify(source, + additionalRefs:={ValueTupleRef, SystemRuntimeFacadeRef, SystemCoreRef}, + parseOptions:=TestOptions.Regular.WithLanguageVersion(LanguageVersion.VisualBasic15), + expectedOutput:="extension") + verifier15.VerifyDiagnostics() + + Dim verifier15_3 = CompileAndVerify(source, + additionalRefs:={ValueTupleRef, SystemRuntimeFacadeRef, SystemCoreRef}, + parseOptions:=TestOptions.Regular.WithLanguageVersion(LanguageVersion.VisualBasic15_3), + expectedOutput:="lambda") + verifier15_3.VerifyDiagnostics() + End Sub + Public Sub LongTupleWithArgumentEvaluation() diff --git a/src/EditorFeatures/CSharpTest/CodeActions/ExtractMethod/ExtractMethodTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/ExtractMethod/ExtractMethodTests.cs index 87ce492355c..882e989ad73 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/ExtractMethod/ExtractMethodTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/ExtractMethod/ExtractMethodTests.cs @@ -1892,5 +1892,35 @@ private static int GetValue(int value) } }"); } + + [Fact, Trait(Traits.Feature, Traits.Features.ExtractMethod)] + public async Task TestTupleWithInferredNames() + { + await TestAsync(@" +class Program +{ + void M() + { + int a = 1; + var t = [|(a, b: 2)|]; + System.Console.Write(t.a); + } +}", +@" +class Program +{ + void M() + { + int a = 1; + var t = {|Rename:GetT|}(a); + System.Console.Write(t.a); + } + + private static (int a, int b) GetT(int a) + { + return (a, b: 2); + } +}", TestOptions.Regular7_1); + } } } \ No newline at end of file diff --git a/src/EditorFeatures/CSharpTest/CodeActions/InlineTemporary/InlineTemporaryTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/InlineTemporary/InlineTemporaryTests.cs index cbb962fa817..33e5042cb3d 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/InlineTemporary/InlineTemporaryTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/InlineTemporary/InlineTemporaryTests.cs @@ -4135,5 +4135,363 @@ void M() await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = (i, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (i: 1 + 2, 3); + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_Trivia() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = ( /*comment*/ i, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = ( /*comment*/ i: 1 + 2, 3); + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_Trivia2() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = ( + /*comment*/ i, + /*comment*/ 3 + ); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = ( + /*comment*/ i: 1 + 2, + /*comment*/ 3 + ); + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_NoDuplicateNames() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = (i, i); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (1 + 2, 1 + 2); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_DeconstructionDeclaration() + { + var code = @" +class C +{ + static int y = 1; + void M() + { + int [||]i = C.y; + var t = ((i, (i, _)) = (1, (i, 3))); + } +}"; + + var expected = @" +class C +{ + static int y = 1; + void M() + { + var t = ((C.y, (C.y, _)) = (1, (C.y, 3))); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_DeconstructionDeclaration2() + { + var code = @" +class C +{ + static int y = 1; + void M() + { + int [||]i = C.y; + var t = ((i, _) = (1, 2)); + } +}"; + + var expected = @" +class C +{ + static int y = 1; + void M() + { + var t = ((C.y, _) = (1, 2)); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_NoReservedNames() + { + var code = @" +class C +{ + void M() + { + int [||]Rest = 1 + 2; + var t = (Rest, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (1 + 2, 3); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_NoReservedNames2() + { + var code = @" +class C +{ + void M() + { + int [||]Item1 = 1 + 2; + var t = (Item1, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (1 + 2, 3); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_EscapeKeywords() + { + var code = @" +class C +{ + void M() + { + int [||]@int = 1 + 2; + var t = (@int, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (@int: 1 + 2, 3); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitTupleNameAdded_DoNotEscapeContextualKeywords() + { + var code = @" +class C +{ + void M() + { + int [||]@where = 1 + 2; + var t = (@where, 3); + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = (where: 1 + 2, 3); + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitAnonymousTypeMemberNameAdded_DuplicateNames() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = new { i, i }; // error already + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = new { i = 1 + 2, i = 1 + 2 }; // error already + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitAnonymousTypeMemberNameAdded_AssignmentEpression() + { + var code = @" +class C +{ + void M() + { + int j = 0; + int [||]i = j = 1; + var t = new { i, k = 3 }; + } +}"; + + var expected = @" +class C +{ + void M() + { + int j = 0; + var t = new { i = j = 1, k = 3 }; + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitAnonymousTypeMemberNameAdded_Comment() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = new { /*comment*/ i, j = 3 }; + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = new { /*comment*/ i = 1 + 2, j = 3 }; + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTemporary)] + public async Task ExplicitAnonymousTypeMemberNameAdded_Comment2() + { + var code = @" +class C +{ + void M() + { + int [||]i = 1 + 2; + var t = new { + /*comment*/ i, + /*comment*/ j = 3 + }; + } +}"; + + var expected = @" +class C +{ + void M() + { + var t = new { + /*comment*/ + i = 1 + 2, + /*comment*/ j = 3 + }; + } +}"; + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } } } diff --git a/src/EditorFeatures/CSharpTest/CodeActions/IntroduceVariable/IntroduceVariableTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/IntroduceVariable/IntroduceVariableTests.cs index 79db6fd3ad5..9b5a317f94b 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/IntroduceVariable/IntroduceVariableTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/IntroduceVariable/IntroduceVariableTests.cs @@ -4051,5 +4051,208 @@ void Method(MySpan span) } }"); } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task TupleWithInferredName_LeaveExplicitName() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int a = 1; + var t = (a, x: [|C.y|]); + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int a = 1; + int {|Rename:y1|} = C.y; + var t = (a, x: y1); + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task TupleWithInferredName_InferredNameBecomesExplicit() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int x = 1; + var t = (x, [|C.y|]); + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int x = 1; + int {|Rename:y1|} = C.y; + var t = (x, y: y1); + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task TupleWithInferredName_AllOccurrences() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int x = 1; + var t = (x, [|C.y|]); + var t2 = (C.y, x); + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int x = 1; + int {|Rename:y1|} = C.y; + var t = (x, y: y1); + var t2 = (y: y1, x); + } +}"; + await TestInRegularAndScriptAsync(code, expected, index: 1, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task TupleWithInferredName_NoDuplicateNames() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int x = 1; + var t = (C.y, [|C.y|]); + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int x = 1; + int {|Rename:y1|} = C.y; + var t = (y1, y1); + } +}"; + await TestInRegularAndScriptAsync(code, expected, index: 1, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task AnonymousTypeWithInferredName_LeaveExplicitName() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int a = 1; + var t = new { a, x= [|C.y|] }; + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int a = 1; + int {|Rename:y1|} = C.y; + var t = new { a, x= y1 }; + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task AnonymousTypeWithInferredName_InferredNameBecomesExplicit() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int x = 1; + var t = new { x, [|C.y|] }; + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int x = 1; + int {|Rename:y1|} = C.y; + var t = new { x, y = y1 }; + } +}"; + + await TestInRegularAndScriptAsync(code, expected, ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsIntroduceVariable)] + public async Task AnonymousTypeWithInferredName_NoDuplicatesAllowed() + { + var code = +@"class C +{ + static int y = 2; + void M() + { + int x = 1; + var t = new { C.y, [|C.y|] }; // this is an error already + } +}"; + + var expected = + @"class C +{ + static int y = 2; + void M() + { + int x = 1; + int {|Rename:y1|} = C.y; + var t = new { y= y1, y= y1 }; // this is an error already + } +}"; + + await TestInRegularAndScriptAsync(code, expected, index: 1, ignoreTrivia: false); + } } } \ No newline at end of file diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/UpgradeProject/UpgradeProjectTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/UpgradeProject/UpgradeProjectTests.cs index 43e9b3c594c..6ab39853e20 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/UpgradeProject/UpgradeProjectTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/UpgradeProject/UpgradeProjectTests.cs @@ -71,6 +71,40 @@ void A() index: 1); } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUpgradeProject)] + public async Task UpgradeProjectFromCSharp5ToCSharp6() + { + await TestLanguageVersionUpgradedAsync( +@" +class Program +{ + void A() + { + var x = [|nameof(A)|]; + } +}", + LanguageVersion.CSharp6, + new CSharpParseOptions(LanguageVersion.CSharp5), + index: 1); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUpgradeProject)] + public async Task UpgradeProjectFromCSharp4ToCSharp5() + { + await TestLanguageVersionUpgradedAsync( +@" +class Program +{ + void A() + { + Func> f = [|async|] x => x; + } +}", + LanguageVersion.CSharp5, + new CSharpParseOptions(LanguageVersion.CSharp4), + index: 1); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUpgradeProject)] public async Task UpgradeProjectFromCSharp7ToLatest() { diff --git a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs index 265f8caf5fc..cd968721731 100644 --- a/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs +++ b/src/Features/CSharp/Portable/IntroduceVariable/CSharpIntroduceVariableService_IntroduceLocal.cs @@ -303,9 +303,7 @@ private bool CanUseVar(ITypeSymbol typeSymbol) var matches = FindMatches(document, expression, document, oldOutermostBlock, allOccurrences, cancellationToken); Debug.Assert(matches.Contains(expression)); - var complexified = await ComplexifyParentingStatements(document, matches, cancellationToken).ConfigureAwait(false); - document = complexified.Item1; - matches = complexified.Item2; + (document, matches) = await ComplexifyParentingStatements(document, matches, cancellationToken).ConfigureAwait(false); // Our original expression should have been one of the matches, which were tracked as part // of complexification, so we can retrieve the latest version of the expression here. diff --git a/src/Workspaces/CSharp/Portable/CSharpWorkspace.csproj b/src/Workspaces/CSharp/Portable/CSharpWorkspace.csproj index cab0a9ad932..2f65b502395 100644 --- a/src/Workspaces/CSharp/Portable/CSharpWorkspace.csproj +++ b/src/Workspaces/CSharp/Portable/CSharpWorkspace.csproj @@ -212,6 +212,8 @@ + + diff --git a/src/Workspaces/CSharp/Portable/Extensions/ParenthesizedExpressionSyntaxExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ParenthesizedExpressionSyntaxExtensions.cs index 5ba94870b5e..14966876a02 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ParenthesizedExpressionSyntaxExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ParenthesizedExpressionSyntaxExtensions.cs @@ -115,6 +115,21 @@ public static bool CanRemoveParentheses(this ParenthesizedExpressionSyntax node, return true; } + // Cases: + // new {(x)} -> {x} + // new { a = (x)} -> { a = x } + // new { a = (x = c)} -> { a = x = c } + if (node.Parent is AnonymousObjectMemberDeclaratorSyntax anonymousDeclarator) + { + // Assignment expressions are not allowed unless member is named + if (anonymousDeclarator.NameEquals == null && expression.IsAnyAssignExpression()) + { + return false; + } + + return true; + } + // Cases: // where (x + 1 > 14) -> where x + 1 > 14 if (node.Parent is QueryClauseSyntax) diff --git a/src/Workspaces/CSharp/Portable/Simplification/AbstractCSharpReducer.AbstractExpressionRewriter.cs b/src/Workspaces/CSharp/Portable/Simplification/AbstractCSharpReducer.AbstractExpressionRewriter.cs index 301e1ab1f40..81a17a13e64 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/AbstractCSharpReducer.AbstractExpressionRewriter.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/AbstractCSharpReducer.AbstractExpressionRewriter.cs @@ -46,6 +46,16 @@ private static SyntaxNode GetParentNode(SyntaxNode node) return GetParentNode(cref); } + if (node.IsKind(SyntaxKind.Argument) && node.Parent.IsKind(SyntaxKind.TupleExpression)) + { + return node.Parent; + } + + if (node.IsKind(SyntaxKind.AnonymousObjectMemberDeclarator)) + { + return node.Parent; + } + return null; } diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.Rewriter.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.Rewriter.cs new file mode 100755 index 00000000000..ec7d3985e1d --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.Rewriter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.CSharp.Simplification +{ + internal partial class CSharpInferredMemberNameReducer + { + private class Rewriter : AbstractExpressionRewriter + { + public Rewriter(OptionSet optionSet, CancellationToken cancellationToken) + : base(optionSet, cancellationToken) + { + } + + public override SyntaxNode VisitArgument(ArgumentSyntax node) + { + return SimplifyExpression( + node, + newNode: base.VisitArgument(node), + simplifier: SimplifyTupleName); + } + + public override SyntaxNode VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) + { + return SimplifyExpression( + node, + newNode: base.VisitAnonymousObjectMemberDeclarator(node), + simplifier: SimplifyAnonymousTypeMemberName); + } + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.cs new file mode 100755 index 00000000000..d1e0f0e1763 --- /dev/null +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpInferredMemberNameReducer.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.CSharp.Simplification +{ + internal partial class CSharpInferredMemberNameReducer : AbstractCSharpReducer + { + public override IExpressionRewriter CreateExpressionRewriter(OptionSet optionSet, CancellationToken cancellationToken) + { + return new Rewriter(optionSet, cancellationToken); + } + + private static ArgumentSyntax SimplifyTupleName(ArgumentSyntax node, SemanticModel semanticModel, OptionSet optionSet, CancellationToken cancellationToken) + { + if (node.NameColon == null || !node.IsParentKind(SyntaxKind.TupleExpression)) + { + return node; + } + + var inferredName = node.Expression.TryGetInferredMemberName(); + + if (inferredName == null || inferredName != node.NameColon.Name.Identifier.ValueText) + { + return node; + } + + return node.WithNameColon(null).WithTriviaFrom(node); + } + + + private static SyntaxNode SimplifyAnonymousTypeMemberName(AnonymousObjectMemberDeclaratorSyntax node, SemanticModel semanticModel, OptionSet optionSet, CancellationToken canellationToken) + { + if (node.NameEquals == null) + { + return node; + } + + var inferredName = node.Expression.TryGetInferredMemberName(); + + if (inferredName == null || inferredName != node.NameEquals.Name.Identifier.ValueText) + { + return node; + } + + return node.WithNameEquals(null).WithTriviaFrom(node); + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.Expander.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.Expander.cs index 1e046122698..4763a06f015 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.Expander.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.Expander.cs @@ -257,6 +257,24 @@ public override SyntaxNode VisitArgument(ArgumentSyntax node) var newArgument = (ArgumentSyntax)base.VisitArgument(node); + if (node.NameColon == null + && node.Parent is TupleExpressionSyntax tuple + && !IsTupleInDeconstruction(tuple)) // The language currently does not allow explicit element names in deconstruction + { + var inferredName = node.Expression.TryGetInferredMemberName(); + if (CanMakeNameExplicitInTuple(tuple, inferredName)) + { + var identifier = SyntaxFactory.Identifier(inferredName); + identifier = TryEscapeIdentifierToken(identifier, node, _semanticModel); + + newArgument = newArgument + .WithoutLeadingTrivia() + .WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(identifier))) + .WithAdditionalAnnotations(Simplifier.Annotation) + .WithLeadingTrivia(node.GetLeadingTrivia()); + } + } + var argumentType = _semanticModel.GetTypeInfo(node.Expression).ConvertedType; if (argumentType != null && !IsPassedToDelegateCreationExpression(node, argumentType) && @@ -271,6 +289,62 @@ public override SyntaxNode VisitArgument(ArgumentSyntax node) return newArgument; } + private static bool CanMakeNameExplicitInTuple(TupleExpressionSyntax tuple, string name) + { + if (name == null || SyntaxFacts.IsReservedTupleElementName(name)) + { + return false; + } + + bool found = false; + foreach (var argument in tuple.Arguments) + { + string elementName = null; + if (argument.NameColon != null) + { + elementName = argument.NameColon.Name.Identifier.ValueText; + } + else + { + elementName = argument.Expression?.TryGetInferredMemberName(); + } + + if (elementName?.Equals(name, StringComparison.Ordinal) == true) + { + if (found) + { + // No duplicate names allowed + return false; + } + found = true; + } + } + + return true; + } + + public override SyntaxNode VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) + { + var newDeclarator = (AnonymousObjectMemberDeclaratorSyntax)base.VisitAnonymousObjectMemberDeclarator(node); + if (node.NameEquals == null) + { + var inferredName = node.Expression.TryGetInferredMemberName(); + if (inferredName != null) + { + var identifier = SyntaxFactory.Identifier(inferredName); + identifier = TryEscapeIdentifierToken(identifier, node, _semanticModel); + + newDeclarator = newDeclarator + .WithoutLeadingTrivia() + .WithNameEquals(SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName(identifier)) + .WithLeadingTrivia(node.GetLeadingTrivia())) + .WithAdditionalAnnotations(Simplifier.Annotation); + } + } + + return newDeclarator; + } + public override SyntaxNode VisitBinaryExpression(BinaryExpressionSyntax node) { _cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.cs index fee06023aa4..89c78c6fe55 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.cs @@ -30,7 +30,8 @@ internal partial class CSharpSimplificationService : AbstractSimplificationServi new CSharpExtensionMethodReducer(), new CSharpParenthesesReducer(), new CSharpEscapingReducer(), - new CSharpMiscellaneousReducer()); + new CSharpMiscellaneousReducer(), + new CSharpInferredMemberNameReducer()); public CSharpSimplificationService() : base(s_reducers) { @@ -90,6 +91,11 @@ public static SyntaxToken TryEscapeIdentifierToken(SyntaxToken syntaxToken, Synt return syntaxToken; } + if (SyntaxFacts.GetContextualKeywordKind(syntaxToken.ValueText) == SyntaxKind.UnderscoreToken) + { + return syntaxToken; + } + var parent = parentOfToken.Parent; if (parentOfToken is SimpleNameSyntax && parent.Kind() == SyntaxKind.XmlNameAttribute) { @@ -184,5 +190,34 @@ protected override void GetUnusedNamespaceImports(SemanticModel model, HashSet public static partial class Simplifier { -- GitLab