From 0cca3deb17ba080f1050a72f6d58ffe0083bd588 Mon Sep 17 00:00:00 2001 From: tmat Date: Mon, 17 Jul 2023 17:09:07 -0700 Subject: [PATCH] Improve sequence point placement in primary constructors and records --- .../CSharp/Test/Emit/PDB/PDBTests.cs | 236 ++++++ .../ActiveStatementTests.Methods.cs | 2 +- .../EditAndContinue/ActiveStatementTests.cs | 510 +++++++++++- .../EditAndContinue/BreakpointSpansTests.cs | 5 +- .../Helpers/EditingTestBase.cs | 9 +- .../EditAndContinue/LineEditTests.cs | 330 ++++++-- .../EditAndContinue/StatementMatchingTests.cs | 6 +- .../EditAndContinue/TopLevelEditingTests.cs | 526 ++++++++++-- .../ActiveStatementsMapTests.cs | 27 + .../ActiveStatementsDescription.cs | 7 +- .../EditAndContinueTestHelpers.cs | 2 +- .../EditAndContinue/SourceMarkers.cs | 68 +- .../EditAndContinue/ActiveStatementTests.vb | 4 +- .../EditAndContinue/BreakpointSpansTests.vb | 2 +- .../Helpers/EditingTestBase.vb | 2 +- .../EditAndContinue/StatementMatchingTests.vb | 6 +- .../EditAndContinue/TopLevelEditingTests.vb | 86 +- .../EditAndContinue/BreakpointSpans.cs | 122 ++- .../CSharpEditAndContinueAnalyzer.cs | 277 ++++--- .../DeclarationBody/CSharpLambdaBody.cs | 7 +- .../CopyConstructorDeclarationBody.cs | 48 ++ ...icitAutoPropertyAccessorDeclarationBody.cs | 33 + .../FieldWithInitializerDeclarationBody.cs | 6 +- .../InstanceConstructorDeclarationBody.cs | 85 +- ...inaryInstanceConstructorDeclarationBody.cs | 30 + ...WithExplicitInitializerDeclarationBody.cs} | 14 +- ...WithImplicitInitializerDeclarationBody.cs} | 14 +- .../PrimaryConstructorDeclarationBody.cs | 29 + ...rWithExplicitInitializerDeclarationBody.cs | 43 + ...rWithImplicitInitializerDeclarationBody.cs | 50 ++ ...ropertyOrIndexerAccessorDeclarationBody.cs | 153 ++++ ...AccessorWithExplicitBodyDeclarationBody.cs | 43 + ...rIndexerWithExplicitBodyDeclarationBody.cs | 38 + .../RecordParameterDeclarationBody.cs | 30 + .../DeclarationBody/SimpleMemberBody.cs | 4 +- .../TopLevelCodeDeclarationBody.cs | 6 +- .../EditAndContinue/SyntaxUtilities.cs | 34 +- .../AbstractEditAndContinueAnalyzer.cs | 752 ++++++++++-------- .../AbstractSimpleMemberBody.cs | 3 +- .../ActiveStatementEnvelope.cs | 16 - .../EditAndContinue/ActiveStatementsMap.cs | 13 +- .../EditAndContinue/DeclarationBody.cs | 25 +- .../Portable/EditAndContinue/MemberBody.cs | 38 +- .../Utilities/BidirectionalMap.cs | 2 + .../EditAndContinue/Utilities/Extensions.cs | 14 +- .../EditAndContinue/BreakpointSpans.vb | 4 +- .../FieldOrPropertyDeclarationBody.vb | 23 +- ...dWithMultipleAsNewClauseDeclarationBody.vb | 11 +- .../DeclarationBody/MethodBody.vb | 4 +- .../PropertyWithInitializerDeclarationBody.vb | 2 +- .../PropertyWithNewClauseDeclarationBody.vb | 2 +- .../DeclarationBody/VisualBasicLambdaBody.vb | 25 +- .../VisualBasicEditAndContinueAnalyzer.vb | 128 ++- .../Core/Portable/Differencing/Edit.cs | 7 +- .../MapBasedLongestCommonSubsequence.cs | 24 + 55 files changed, 3110 insertions(+), 877 deletions(-) create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CopyConstructorDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/ExplicitAutoPropertyAccessorDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorDeclarationBody.cs rename src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/{InstanceConstructorWithExplicitInitializerDeclarationBody.cs => OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody.cs} (67%) rename src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/{InstanceConstructorWithImplicitInitializerDeclarationBody.cs => OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody.cs} (63%) create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithExplicitInitializerDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithImplicitInitializerDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerWithExplicitBodyDeclarationBody.cs create mode 100644 src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/RecordParameterDeclarationBody.cs delete mode 100644 src/Features/Core/Portable/EditAndContinue/ActiveStatementEnvelope.cs create mode 100644 src/Workspaces/Core/Portable/Differencing/MapBasedLongestCommonSubsequence.cs diff --git a/src/Compilers/CSharp/Test/Emit/PDB/PDBTests.cs b/src/Compilers/CSharp/Test/Emit/PDB/PDBTests.cs index 40709f0d06d4b..bc7812ba4a9af 100644 --- a/src/Compilers/CSharp/Test/Emit/PDB/PDBTests.cs +++ b/src/Compilers/CSharp/Test/Emit/PDB/PDBTests.cs @@ -12202,6 +12202,242 @@ .maxstack 2 "); } + [Fact] + public void SyntaxOffset_OutVarInPrimaryConstructorInitializer() + { + var source = @" +class B(int x, int y) +{ +} + +class C(int x) : B(F(out var y), y) +{ + int Z = F(out var z); + static int F(out int a) => a = 1; +}"; + + var c = CreateCompilationWithMscorlib40AndSystemCore(source, options: TestOptions.DebugDll); + c.VerifyPdb(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + +", options: SyntaxOffsetPdbValidationOptions); + } + + #endregion + + #region Primary Constructors + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/63299")] + public void SequencePoints_PrimaryConstructor_ExplicitBaseInitializer() + { + var source = @" +class B() : object() +{ +} + +class C(int x) : B() +{ + int y = 1; +}"; + + var c = CompileAndVerify(source); + c.VerifyMethodBody("B..ctor", @" +{ + // Code size 7 (0x7) + .maxstack 1 + // sequence point: object() + IL_0000: ldarg.0 + IL_0001: call ""object..ctor()"" + IL_0006: ret +} +"); + c.VerifyMethodBody("C..ctor", @" + { + // Code size 14 (0xe) + .maxstack 2 + // sequence point: int y = 1; + IL_0000: ldarg.0 + IL_0001: ldc.i4.1 + IL_0002: stfld ""int C.y"" + // sequence point: B() + IL_0007: ldarg.0 + IL_0008: call ""B..ctor()"" + IL_000d: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/63299")] + public void SequencePoints_PrimaryConstructor_ImplicitBaseInitializer() + { + var source = @" +class B() +{ +} + +class C(int x) : B +{ +}"; + + var c = CompileAndVerify(source); + c.VerifyMethodBody("B..ctor", @" +{ + // Code size 7 (0x7) + .maxstack 1 + // sequence point: B() + IL_0000: ldarg.0 + IL_0001: call ""object..ctor()"" + IL_0006: ret +} +"); + c.VerifyMethodBody("C..ctor", @" +{ + // Code size 7 (0x7) + .maxstack 1 + // sequence point: C(int x) + IL_0000: ldarg.0 + IL_0001: call ""B..ctor()"" + IL_0006: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/63299")] + public void SequencePoints_RecordConstructors_ModifiersAndDefault() + { + var source = @" +record C([A]in T P = default) where T : struct; + +class A : System.Attribute {} +" + IsExternalInitTypeDefinition; + + var c = CompileAndVerify(source, verify: Verification.Skipped); + + // primary constructor + c.VerifyMethodBody("C..ctor(in T)", @" +{ + // Code size 19 (0x13) + .maxstack 2 + // sequence point: + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldobj ""T"" + IL_0007: stfld ""T C.

k__BackingField"" + // sequence point: C([A]in T P = default) + IL_000c: ldarg.0 + IL_000d: call ""object..ctor()"" + IL_0012: ret +} +"); + // copy constructor + c.VerifyMethodBody("C..ctor(C)", @" +{ + // Code size 19 (0x13) + .maxstack 2 + // sequence point: + IL_0000: ldarg.0 + IL_0001: call ""object..ctor()"" + IL_0006: ldarg.0 + IL_0007: ldarg.1 + IL_0008: ldfld ""T C.

k__BackingField"" + IL_000d: stfld ""T C.

k__BackingField"" + // sequence point: C + IL_0012: ret +} +"); + // primary auto-property getter + c.VerifyMethodBody("C.P.get", @" +{ + // Code size 7 (0x7) + .maxstack 1 + // sequence point: in T P + IL_0000: ldarg.0 + IL_0001: ldfld ""T C.

k__BackingField"" + IL_0006: ret +} +"); + // primary auto-property setter + c.VerifyMethodBody("C.P.init", @" +{ + // Code size 8 (0x8) + .maxstack 2 + // sequence point: in T P + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld ""T C.

k__BackingField"" + IL_0007: ret +} +"); + } + + [Theory] + [InlineData("int[] P = default", "int[] P")] + [InlineData("[A]int[] P", "int[] P")] + [InlineData("params int[] P", "params int[] P")] + [WorkItem("https://github.com/dotnet/roslyn/issues/63299")] + public void SequencePoints_RecordPropertyAccessors(string parameterSyntax, string expectedSpan) + { + var source = + "record C(" + parameterSyntax + ");" + + "class A : System.Attribute { }" + + IsExternalInitTypeDefinition; + + var c = CompileAndVerify(source, verify: Verification.Skipped); + + // primary auto-property getter + c.VerifyMethodBody("C.P.get", $@" +{{ + // Code size 7 (0x7) + .maxstack 1 + // sequence point: {expectedSpan} + IL_0000: ldarg.0 + IL_0001: ldfld ""int[] C.

k__BackingField"" + IL_0006: ret +}} +"); + // primary auto-property setter + c.VerifyMethodBody("C.P.init", $@" +{{ + // Code size 8 (0x8) + .maxstack 2 + // sequence point: {expectedSpan} + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: stfld ""int[] C.

k__BackingField"" + IL_0007: ret +}} +"); + } + #endregion [WorkItem(4370, "https://github.com/dotnet/roslyn/issues/4370")] diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs index 1c8ed6f215dca..9e9b8c7c9040f 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs @@ -595,7 +595,7 @@ class C var active = GetActiveStatements(src1, src2); edits.VerifySemanticDiagnostics(active, - Diagnostic(RudeEditKind.DeleteActiveStatement, "get", FeaturesResources.code)); + Diagnostic(RudeEditKind.ActiveStatementUpdate, "{")); } [Fact] diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.cs index f156aef35956f..b3dfce1029209 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.cs @@ -739,7 +739,7 @@ static void Main() } } }"; - var src2 = @""; + var src2 = @""; var edits = GetTopEdits(src1, src2); var active = GetActiveStatements(src1, src2); @@ -1488,6 +1488,231 @@ public C(int a) {} edits.VerifySemanticDiagnostics(active); } + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/68708")] + public void Constructor_Instance_ImplicitInitializer_ParameterChange() + { + var src1 = "class C { C(int P) {} }"; + var src2 = "class C { C(byte P) {} }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + // TODO: should be rude edit (https://github.com/dotnet/roslyn/issues/68708) + edits.VerifySemanticDiagnostics( + active, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + + [Theory(Skip = "https://github.com/dotnet/roslyn/issues/68708")] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + [WorkItem("https://github.com/dotnet/roslyn/issues/68708")] + public void Constructor_Instance_Primary_ImplicitInitializer_ParameterChange(string keyword) + { + var src1 = keyword + " C(int P);"; + var src2 = keyword + " C(byte P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + // TODO: should be rude edit (https://github.com/dotnet/roslyn/issues/68708) + edits.VerifySemanticDiagnostics( + active, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public void Constructor_Instance_Primary_ExplicitInitializer(string keyword) + { + var src1 = keyword + " C(int P) : B(1);"; + var src2 = keyword + " C(int P) : B(2);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Primary_Record_Parameter_BaseInitializerChange() + { + var src1 = "record C(int P) : B(1);"; + var src2 = "record C(int P) : B(2);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Primary_Record_Parameter_TypeAttributeChange(string keyword) + { + var src1 = "class A(int x) : System.Attribute; [A( 1)]" + keyword + " C(int P);"; + var src2 = "class A(int x) : System.Attribute; [A(10)]" + keyword + " C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics( + active, + capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Primary_Record_Parameter_TypeParameterAttributeChange(string keyword) + { + var src1 = "class A(int x) : System.Attribute; " + keyword + " C<[A( 1)] T>(int P);"; + var src2 = "class A(int x) : System.Attribute; " + keyword + " C<[A(10)] T>(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics( + active, + diagnostics: new[] + { + Diagnostic(RudeEditKind.GenericTypeUpdate, "T") + }, + capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); + } + + [Fact] + public void Constructor_Instance_Copy_BaseInitializerChange() + { + var src1 = "record C(int P) : B(1);"; + var src2 = "record C(int P) : B(2);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Copy_TypeAttributeChange(string keyword) + { + var src1 = "class A(int x) : System.Attribute; [A( 1)]" + keyword + " C(int P);"; + var src2 = "class A(int x) : System.Attribute; [A(10)]" + keyword + " C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics( + active, + capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Copy_TypeParameterAttributeChange(string keyword) + { + var src1 = "class A(int x) : System.Attribute; " + keyword + " C<[A( 1)] T>(int P);"; + var src2 = "class A(int x) : System.Attribute; " + keyword + " C<[A(10)] T>(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics( + active, + diagnostics: new[] + { + Diagnostic(RudeEditKind.GenericTypeUpdate, "T"), + }, + capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); + } + + [Fact] + public void Constructor_Instance_Copy_ReplacingPrimaryWithNonPrimary() + { + var src1 = @" +public record C(int P);"; + var src2 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Copy_ReplacingNonPrimaryWithPrimary_Initializer() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var src2 = @" +public record C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Copy_ReplacingNonPrimaryWithPrimary_Body1() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var src2 = @" +public record C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Copy_ReplacingNonPrimaryWithPrimary_Body2() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var src2 = @" +public record C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + [Theory] [InlineData("class ")] [InlineData("struct")] @@ -1503,6 +1728,285 @@ public void Constructor_Instance_Delete_Parameterless(string typeKind) Diagnostic(RudeEditKind.DeleteActiveStatement, "partial " + typeKind + " C", DeletedSymbolDisplay(FeaturesResources.constructor, "C()"))); } + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Delete_Primary(string typeKind) + { + var src1 = typeKind + " C() { }"; + var src2 = "" + typeKind + " C { }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.DeleteActiveStatement, typeKind + " C", GetResource("constructor", "C()"))); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public void Constructor_Instance_Delete_BaseInitializer_Internal(string typeKind) + { + var src1 = "var c = new C(); " + typeKind + " C() : B() { }"; + var src2 = "var c = new C(); " + typeKind + " C() : B { }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.ActiveStatementUpdate, "C()")); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public void Constructor_Instance_Delete_BaseInitializer_Leaf(string typeKind) + { + var src1 = typeKind + " C() : B() { }"; + var src2 = typeKind + " C() : B { }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public void Constructor_Instance_Insert_BaseInitializer_Internal(string typeKind) + { + var src1 = "var c = new C(); " + typeKind + " C() : B { }"; + var src2 = "var c = new C(); " + typeKind + " C() : B() { }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.ActiveStatementUpdate, "B()")); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public void Constructor_Instance_Insert_BaseInitializer_Leaf(string typeKind) + { + var src1 = typeKind + " C() : B { }"; + var src2 = typeKind + " C() : B() { }"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public void Constructor_Instance_Unchanged(string typeKind) + { + var src1 = "var c = new C(1); " + typeKind + " C(int a);"; + var src2 = "var c = new C(2); " + typeKind + " C(int a);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Primary_Record_ReplacingPrimaryWithNonPrimary() + { + var src1 = @" +public record C(int P);"; + var src2 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Primary_Record_ReplacingNonPrimaryWithPrimary_Initializer() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var src2 = @" +public record C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_Primary_Record_ReplacingNonPrimaryWithPrimary_Body() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var src2 = @" +public record C(int P);"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Constructor_Instance_ReplacingNonPrimaryWithPrimary_Record() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +}"; + var src2 = @" +public record C(int P); +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.ActiveStatementUpdate, "C ( int P )"), + Diagnostic(RudeEditKind.ActiveStatementUpdate, "C"), + Diagnostic(RudeEditKind.ActiveStatementUpdate, "int P")); + } + + [Fact] + public void Constructor_Instance_ReplacingPrimaryWithNonPrimary_Record_PrimaryConstructor2() + { + var src1 = @" +public record C(int P);"; + var src2 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) { P = original.P; } +} +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.ActiveStatementUpdate, "public C(int P)"), + Diagnostic(RudeEditKind.ActiveStatementUpdate, "protected C(C original)")); + } + + #endregion + + #region Properties + + [Fact] + public void Property_Auto_Record_ReplacingNonPrimaryWithPrimary_Getter() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) {P = original.P; } +}"; + var src2 = @" +public record C(int P); +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Property_Auto_Record_ReplacingNonPrimaryWithPrimary_Setter() + { + var src1 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) {P = original.P; } +}"; + var src2 = @" +public record C(int P); +"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Property_Auto_Record_ReplacingPrimaryWithNonPrimary_Getter() + { + // Consider: Just looking at the span we do not know whether the active statement is in the getter of the setter. + // We could resolve the method token associated with the method to find out and map the statement accordingly. + + var src1 = @" +public record C(int P); +"; + var src2 = @" +public record C +{ + public int P { get; init; } + public C(int P) { } + protected C(C original) {P = original.P; } +}"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + + [Fact] + public void Property_Auto_Record_ReplacingPrimaryWithNonPrimary_Setter() + { + // Consider: Just looking at the span we do not know whether the active statement is in the getter of the setter. + // We could resolve the method token associated with the method to find out and map the statement accordingly. + + var src1 = @" +public record C(int P); +"; + var src2 = @" +public record C +{ + public int P { init; get; } + public C(int P) { } + protected C(C original) {P = original.P; } +}"; + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + edits.VerifySemanticDiagnostics(active); + } + #endregion #region Field and Property Initializers @@ -6632,7 +7136,7 @@ public static IEnumerable M() { return from a in new[] { 1 } - where F() switch { 0 => true, _ => false}/**/ + where F() switch { 0 => true, _ => false} select a; } }"; @@ -6643,7 +7147,7 @@ public static IEnumerable M() { return from a in new[] { 2 } - where F() switch { 0 => true, _ => false}/**/ + where F() switch { 0 => true, _ => false} select a; } }"; diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/BreakpointSpansTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/BreakpointSpansTests.cs index 328eda13dd290..2bff5f95d5329 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/BreakpointSpansTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/BreakpointSpansTests.cs @@ -10,6 +10,7 @@ using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.EditAndContinue; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Test.Utilities; @@ -87,7 +88,7 @@ private static void VerifyAllSpansInDeclaration(string markup) var expectedEnvelope = expectedSpans.IsEmpty ? default : TextSpan.FromBounds(expectedSpans[0].Start, expectedSpans[^1].End); Assert.NotNull(declarationNode); - var actualEnvelope = SyntaxUtilities.TryGetDeclarationBody(declarationNode)?.Envelope ?? default; + var actualEnvelope = SyntaxUtilities.TryGetDeclarationBody(declarationNode, symbol: null)?.Envelope ?? default; Assert.Equal(expectedEnvelope, actualEnvelope); } @@ -97,7 +98,7 @@ public static IEnumerable GetBreakpointSequence(SyntaxNode root, int p var endPosition = root.Span.End; for (var p = position; p < endPosition; p++) { - if (BreakpointSpans.TryGetClosestBreakpointSpan(root, p, out var span) && span.Start > lastSpan.Start) + if (BreakpointSpans.TryGetClosestBreakpointSpan(root, p, minLength: 0, out var span) && span.Start > lastSpan.Start) { lastSpan = span; yield return span; diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/Helpers/EditingTestBase.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/Helpers/EditingTestBase.cs index 3285d6f6da6f7..309b6d3402ca1 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/Helpers/EditingTestBase.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/Helpers/EditingTestBase.cs @@ -152,7 +152,8 @@ internal static Match GetMethodMatch(string src1, string src2, Metho var m1 = MakeMethodBody(src1, kind); var m2 = MakeMethodBody(src2, kind); - var match = m1.ComputeMatch(m2, knownMatches: null); + var match = m1.ComputeSingleRootMatch(m2, knownMatches: null); + Contract.ThrowIfNull(match); var stateMachineInfo1 = m1.GetStateMachineInfo(); var stateMachineInfo2 = m2.GetStateMachineInfo(); @@ -190,7 +191,7 @@ internal static MemberBody MakeMethodBody( if (kind == MethodKind.ConstructorWithParameters) { - var body = SyntaxUtilities.TryGetDeclarationBody(declaration); + var body = SyntaxUtilities.TryGetDeclarationBody(declaration, symbol: null); Contract.ThrowIfNull(body); return body; } @@ -222,11 +223,11 @@ internal static SyntaxMapDescription GetSyntaxMap(string oldSource, string newSo internal static void VerifyPreserveLocalVariables(EditScript edits, bool preserveLocalVariables) { var oldDeclaration = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.OldRoot).Members[0]).Members[0]; - var oldBody = SyntaxUtilities.TryGetDeclarationBody(oldDeclaration); + var oldBody = SyntaxUtilities.TryGetDeclarationBody(oldDeclaration, symbol: null); Contract.ThrowIfNull(oldBody); var newDeclaration = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.NewRoot).Members[0]).Members[0]; - var newBody = SyntaxUtilities.TryGetDeclarationBody(newDeclaration); + var newBody = SyntaxUtilities.TryGetDeclarationBody(newDeclaration, symbol: null); Contract.ThrowIfNull(newBody); _ = oldBody.ComputeMatch(newBody, knownMatches: null); diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/LineEditTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/LineEditTests.cs index da86bfe33903f..ec54b9af6cc60 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/LineEditTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/LineEditTests.cs @@ -771,33 +771,58 @@ public C(int a) } [Fact] - public void Constructor_LineChange1() + public void Constructor_ImplicitInitializer_BlockBody_LineChange() { var src1 = @" class C { public C(int a) - : base() - { - } + + {} } "; var src2 = @" class C { - public C(int a) - : base() - { - } + public C(int a) + {} +}"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits(new[] + { + new SourceLineUpdate(3, 4), + new SourceLineUpdate(4, 4) + }); + } + + [Fact] + public void Constructor_ImplicitInitializer_BlockBody_Recompile() + { + var src1 = @" +class C +{ + public C(int a + ) + {} +} +"; + var src2 = @" +class C +{ + public C(int a + ) + {} }"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - new[] { new SourceLineUpdate(4, 5) }); + Array.Empty(), + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(), preserveLocalVariables: true) }); + } [Fact] - public void Constructor_ExpressionBodied_LineChange1() + public void Constructor_ImplicitInitializer_ExpressionBodied_LineChange1() { var src1 = @" class C @@ -821,7 +846,7 @@ public C(int a) => } [Fact] - public void Constructor_ExpressionBodied_LineChange2() + public void Constructor_ImplicitInitializer_ExpressionBodied_LineChange2() { var src1 = @" class C @@ -845,7 +870,7 @@ public C(int a) } [Fact] - public void Constructor_ExpressionBodied_LineChange3() + public void Constructor_ImplicitInitializer_ExpressionBodied_LineChange3() { var src1 = @" class C @@ -869,7 +894,7 @@ public C(int a) => } [Fact] - public void Constructor_ExpressionBodied_LineChange4() + public void Constructor_ImplicitInitializer_ExpressionBodied_LineChange4() { var src1 = @" class C @@ -896,81 +921,165 @@ public C(int a) } [Fact] - public void Constructor_ExpressionBodiedWithBase_LineChange1() + public void Constructor_ImplicitInitializer_Primary_LineChange() { var src1 = @" -class C -{ - int _a; - public C(int a) - : base() => _a = a; -} +class C(int a); "; var src2 = @" -class C -{ - int _a; - public C(int a) +class + C(int a);"; - : base() => _a = a; -}"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits(new[] + { + new SourceLineUpdate(1, 2) + }); + } + + [Fact] + public void Constructor_ImplicitInitializer_Primary_Recompile1() + { + var src1 = @" +class C (int a); +"; + var src2 = @" +class C(int a); +"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - new[] { new SourceLineUpdate(5, 6) }); + Array.Empty(), + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true) }); + } [Fact] - public void Constructor_ExpressionBodiedWithBase_LineChange2() + public void Constructor_ImplicitInitializer_Primary_Recompile2() { var src1 = @" -class C -{ - int _a; - public C(int a) - : base() => - _a = a; -} +class C(int a); "; var src2 = @" -class C -{ - int _a; - public C(int a) +class C(int a ); +"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + Array.Empty(), + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true) }); - : base() => _a = a; -}"; + } + + [Fact] + public void Constructor_ImplicitInitializer_PrimaryRecord_Recompile1() + { + var src1 = @" +record C (int P); +"; + var src2 = @" +record C(int P); +"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - new SourceLineUpdate[] { new(5, 6) }); + Array.Empty(), + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true), + }); + + } + + [Fact] + public void Constructor_ImplicitInitializer_PrimaryRecord_Recompile2() + { + var src1 = @" +record C(int P); +"; + var src2 = @" +record C(int P ); +"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + Array.Empty(), + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true) + }); + + } + + [Fact] + public void Constructor_ImplicitInitializer_PrimaryAndParameter_Recompile3() + { + var src1 = @" +record C(int P); +"; + var src2 = @" +record C( int P); +"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + Array.Empty(), + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true), + }); + } + + [Fact] + public void Constructor_ImplicitInitializer_PrimaryAndCopyCtorAndParameter_Recompile3() + { + var src1 = @" +record C(int P); +"; + var src2 = @" +record C(int P); +"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + Array.Empty(), + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true), + }, + capabilities: EditAndContinueCapabilities.GenericUpdateMethod); } [Fact] - public void Constructor_ExpressionBodiedWithBase_Recompile1() + public void Constructor_ExplicitInitializer_BlockBody_LineChange1() { var src1 = @" class C { - int _a; public C(int a) - : base() => _a - = a; + : base() + { + } } "; var src2 = @" class C { - int _a; - public C(int a) - : base() => _a = a; + public C(int a) + + : base() + { + } }"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - Array.Empty(), - semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(), preserveLocalVariables: true) }); + new[] { new SourceLineUpdate(4, 5) }); } [Fact] - public void Constructor_PartialBodyLineChange1() + public void Constructor_ExplicitInitializer_BlockBody_Recompile() { var src1 = @" class C @@ -985,18 +1094,18 @@ public C(int a) class C { public C(int a) - : base() - + : base() { } }"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - new SourceLineUpdate[] { new(5, 6) }); + Array.Empty(), + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(), preserveLocalVariables: true) }); } [Fact] - public void Constructor_Recompile2() + public void Constructor_ExplicitInitializer_BlockBody_PartialBodyLineChange1() { var src1 = @" class C @@ -1011,18 +1120,18 @@ public C(int a) class C { public C(int a) - : base() + : base() + { } }"; var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - Array.Empty(), - semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(), preserveLocalVariables: true) }); + new SourceLineUpdate[] { new(5, 6) }); } [Fact] - public void Constructor_RudeRecompile1() + public void Constructor_ExplicitInitializer_BlockBody_RudeRecompile1() { var src1 = @" class C @@ -1053,6 +1162,80 @@ public C(int a) capabilities: EditAndContinueCapabilities.GenericUpdateMethod); } + [Fact] + public void Constructor_ExplicitInitializer_ExpressionBodied_LineChange1() + { + var src1 = @" +class C +{ + int _a; + public C(int a) + : base() => _a = a; +} +"; + var src2 = @" +class C +{ + int _a; + public C(int a) + + : base() => _a = a; +}"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + new[] { new SourceLineUpdate(5, 6) }); + } + + [Fact] + public void Constructor_ExplicitInitializer_ExpressionBodied_LineChange2() + { + var src1 = @" +class C +{ + int _a; + public C(int a) + : base() => + _a = a; +} +"; + var src2 = @" +class C +{ + int _a; + public C(int a) + + : base() => _a = a; +}"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + new SourceLineUpdate[] { new(5, 6) }); + } + + [Fact] + public void Constructor_ExplicitInitializer_ExpressionBodied_Recompile1() + { + var src1 = @" +class C +{ + int _a; + public C(int a) + : base() => _a + = a; +} +"; + var src2 = @" +class C +{ + int _a; + public C(int a) + : base() => _a = a; +}"; + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + Array.Empty(), + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(), preserveLocalVariables: true) }); + } + #endregion #region Destructors @@ -1641,9 +1824,11 @@ class C int P { get; } = 1; }"; + // We can only apply one delta per line, but that affects both getter and initializer. So we need to recompile one of them. var edits = GetTopEdits(src1, src2); edits.VerifyLineEdits( - new[] { new SourceLineUpdate(3, 4) }); + lineEdits: new[] { new SourceLineUpdate(3, 4) }, + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")) }); } [Fact] @@ -1657,6 +1842,29 @@ class C "; var src2 = @" class C +{ + int P + { get; } = + 1; +}"; + // We can only apply one delta per line, but that affects both getter and initializer. So we need to recompile one of them. + var edits = GetTopEdits(src1, src2); + edits.VerifyLineEdits( + lineEdits: new[] { new SourceLineUpdate(3, 5) }, + semanticEdits: new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")) }); + } + + [Fact] + public void Property_Initializer4() + { + var src1 = @" +class C +{ + int P { get; } = 1; +} +"; + var src2 = @" +class C { int P { get; } = 1; }"; diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/StatementMatchingTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/StatementMatchingTests.cs index a14a212411f3b..444f976b4dbbe 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/StatementMatchingTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/StatementMatchingTests.cs @@ -39,7 +39,7 @@ public void KnownMatches() // pre-matched: - var match = m1.ComputeMatch(m2, knownMatches); + var match = m1.ComputeSingleRootMatch(m2, knownMatches); var actual = ToMatchingPairs(match); @@ -53,7 +53,7 @@ public void KnownMatches() // not pre-matched: - match = m1.ComputeMatch(m2, knownMatches: null); + match = m1.ComputeSingleRootMatch(m2, knownMatches: null); actual = ToMatchingPairs(match); @@ -81,7 +81,7 @@ public void KnownMatches_Root() var m2 = MakeMethodBody(src2); var knownMatches = new[] { new KeyValuePair(m1.RootNodes.First(), m2.RootNodes.First()) }; - var match = m1.ComputeMatch(m2, knownMatches); + var match = m1.ComputeSingleRootMatch(m2, knownMatches); var actual = ToMatchingPairs(match); var expected = new MatchingPairs diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs index f2fe37be1db0f..2e405e7cca4a3 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs @@ -2004,7 +2004,6 @@ enum E { } Diagnostic(RudeEditKind.InsertOperator, "public static int operator +(I a, I b)", FeaturesResources.operator_), Diagnostic(RudeEditKind.InsertIntoInterface, "static int StaticProperty1", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertIntoInterface, "static int StaticProperty2", FeaturesResources.property_), - Diagnostic(RudeEditKind.InsertIntoInterface, "static int StaticProperty2", CSharpFeaturesResources.property_getter), Diagnostic(RudeEditKind.InsertVirtual, "virtual int VirtualProperty1", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertVirtual, "virtual int VirtualProperty2", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertVirtual, "int VirtualProperty3", FeaturesResources.property_), @@ -2012,9 +2011,7 @@ enum E { } Diagnostic(RudeEditKind.InsertVirtual, "abstract int AbstractProperty1", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertVirtual, "abstract int AbstractProperty2", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertIntoInterface, "sealed int NonVirtualProperty", FeaturesResources.property_), - Diagnostic(RudeEditKind.InsertIntoInterface, "sealed int NonVirtualProperty", CSharpFeaturesResources.property_getter), Diagnostic(RudeEditKind.InsertVirtual, "int this[byte virtualIndexer]", FeaturesResources.indexer_), - Diagnostic(RudeEditKind.InsertVirtual, "int this[byte virtualIndexer]", CSharpFeaturesResources.indexer_getter), Diagnostic(RudeEditKind.InsertVirtual, "int this[sbyte virtualIndexer]", FeaturesResources.indexer_), Diagnostic(RudeEditKind.InsertVirtual, "virtual int this[ushort virtualIndexer]", FeaturesResources.indexer_), Diagnostic(RudeEditKind.InsertVirtual, "virtual int this[short virtualIndexer]", FeaturesResources.indexer_), @@ -2156,6 +2153,7 @@ class C edits.VerifySemanticDiagnostics(new[] { + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "int P", GetResource("auto-property")), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F1", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F2", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "SF", FeaturesResources.field), @@ -3297,12 +3295,50 @@ public void Record_Property_Delete_ReadOnly(string getter) capabilities: EditAndContinueCapabilities.Baseline); } - [Theory] - [InlineData("get;")] - [InlineData("get => 1;")] - public void Record_Property_Delete_ReadOnly_ReplacingCustomWithSynthesized(string getter) + [Fact] + public void Record_Property_Delete_ReadOnly_ReplacingCustomWithSynthesized() { - var src1 = "record C(int X) { public int X { " + getter + " } }"; + var src1 = "record C(int X) { public int X { get => 1; } }"; + var src2 = "record C(int X);"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemanticDiagnostics( + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); + + edits.VerifySemanticDiagnostics( + new[] { Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "record C", GetResource("property getter", "X.get")) }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Record_Property_Delete_ReadOnly_ReplacingCustomWithSynthesized_Generic() + { + var src1 = "record C(int X) { public int X { get => 1; } }"; + var src2 = "record C(int X);"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemanticDiagnostics( + capabilities: + EditAndContinueCapabilities.AddInstanceFieldToExistingType | + EditAndContinueCapabilities.GenericAddFieldToExistingType | + EditAndContinueCapabilities.GenericUpdateMethod); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "int X", GetResource("property")), + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "record C", GetResource("property getter", "X.get")), + Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "(int X)", GetResource("constructor")) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Record_Property_Delete_ReadOnly_ReplacingCustomWithSynthesized_AutoProp() + { + var src1 = "record C(int X) { public int X { get; } }"; var src2 = "record C(int X);"; var edits = GetTopEdits(src1, src2); @@ -3397,6 +3433,36 @@ public void Record_Property_Delete_ReplacingCustomWithSynthesized_Auto(string in SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); } + [Fact] + public void Record_Property_Delete_ReplacingCustomWithSynthesized_Struct() + { + var src1 = "record struct C(int X) { public int X { get; init; } }"; + var src2 = "record struct C(int X);"; + + var edits = GetTopEdits(src1, src2); + + // synthesized setter is writable in non-readonly struct + edits.VerifySemanticDiagnostics( + Diagnostic(RudeEditKind.AccessorKindUpdate, "record struct C", GetResource("property setter", "X.init"))); + } + + [Fact] + public void Record_Property_Delete_ReplacingCustomWithSynthesized_ReadOnlyStruct() + { + var src1 = "readonly record struct C(int X) { public int X { get; init; } }"; + var src2 = "readonly record struct C(int X);"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), + SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_X")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_X")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); + } + [Theory] [InlineData("")] [InlineData(" = 1;")] @@ -3442,15 +3508,17 @@ public void Record_Property_Delete_ReplacingCustomWithSynthesized_WithBody(strin // Methods using backing field must be updated, unless they are explicitly declared. edits.VerifySemantics( - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), - SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), - SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").GetMethod), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").SetMethod), - SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); - - edits.VerifySemanticDiagnostics(); + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), + SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").GetMethod), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").SetMethod), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true), + }, + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Theory] @@ -3490,7 +3558,9 @@ public void Record_Property_Delete_ReplacingCustomWithSynthesized_WithBodyAndMet expectedEdits.Add(SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").SetMethod)); expectedEdits.Add(SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); - edits.VerifySemantics(expectedEdits.ToArray()); + edits.VerifySemantics( + expectedEdits.ToArray(), + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Theory] @@ -3523,7 +3593,8 @@ public void Record_Property_Delete_ReplacingCustomWithSynthesized_WithBody_Parti SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.X").SetMethod), SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), partialType : "C", preserveLocalVariables: true), }) - }); + }, + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -3669,11 +3740,13 @@ public C(bool b) : this(1) { } capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType | EditAndContinueCapabilities.AddMethodToExistingType); } - [Fact] - public void Record_Property_Insert_ReplacingSynthesizedWithCustom_WithSetter() + [Theory] + [InlineData("record")] + [InlineData("readonly record struct")] + public void Record_Property_Insert_ReplacingSynthesizedWithCustom_WithSetter(string keyword) { - var src1 = "record C(int X);"; - var src2 = "record C(int X) { public int X { get; set; } }"; + var src1 = keyword + " C(int P);"; + var src2 = keyword + " C(int P) { public int P { get; set; } }"; var edits = GetTopEdits(src1, src2); @@ -3681,6 +3754,25 @@ public void Record_Property_Insert_ReplacingSynthesizedWithCustom_WithSetter() Diagnostic(RudeEditKind.AccessorKindUpdate, "set", GetResource("property setter"))); } + [Fact] + public void Record_Property_Insert_ReplacingSynthesizedWithCustom_WithSetter_WritableStruct() + { + var src1 = "record struct C(int P);"; + var src2 = "record struct C(int P) { public int P { get; set; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics(new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), + SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true), + }); + } + [Fact] public void Record_Property_Insert_ReplacingSynthesizedWithCustom_ReadOnly() { @@ -10180,6 +10272,8 @@ public void Constructor_Parameter_AddAttribute_Record(string target) new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), }, capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); } @@ -10258,10 +10352,6 @@ public void Constructor_Parameter_AddAttribute_Record_ReplacingCustomPropertyWit new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), - SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), - SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), @@ -10359,10 +10449,14 @@ public void Constructor_Parameter_ChangeType_Record() SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); edits.VerifySemanticDiagnostics( - new[] { Diagnostic(RudeEditKind.ChangingTypeNotSupportedByRuntime, "int x", FeaturesResources.parameter) }, + new[] + { + Diagnostic(RudeEditKind.ChangingTypeNotSupportedByRuntime, "int x", GetResource("parameter")), + Diagnostic(RudeEditKind.ChangingTypeNotSupportedByRuntime, "int x", GetResource("property")) + }, capabilities: EditAndContinueCapabilities.Baseline); } @@ -10378,7 +10472,7 @@ public void Constructor_Parameter_ChangeType_ReplacingClassWithRecord() { Diagnostic(RudeEditKind.TypeKindUpdate, "record C") }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10734,7 +10828,7 @@ public void Constructor_Parameter_Insert_Primary_Record() SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.Z")), SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.U")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10781,7 +10875,7 @@ public void Constructor_Parameter_Insert_Primary_Record_WithCustomDeconstructor_ SemanticEdit(SemanticEditKind.Delete, c => c.GetPrimaryConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10804,7 +10898,7 @@ public void Constructor_Parameter_Insert_Primary_Record_WithCustomDeconstructor_ SemanticEdit(SemanticEditKind.Delete, c => c.GetPrimaryConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10828,7 +10922,7 @@ public void Constructor_Parameter_Insert_Primary_Record_WithCustomDeconstructor_ SemanticEdit(SemanticEditKind.Delete, c => c.GetPrimaryConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10852,7 +10946,7 @@ public void Constructor_Parameter_Insert_Primary_Record_WithCustomDeconstructor_ SemanticEdit(SemanticEditKind.Delete, c => c.GetPrimaryConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10876,7 +10970,7 @@ public void Constructor_Parameter_Insert_Primary_Record_WithCustomDeconstructor_ SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), SemanticEdit(SemanticEditKind.Delete, c => c.GetPrimaryDeconstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")), }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -10895,27 +10989,52 @@ public void Constructor_Parameter_Update_In() } [Theory] - [InlineData("class")] - [InlineData("struct")] - [InlineData("record")] - [InlineData("record struct")] - public void Constructor_Parameter_DeleteInsert_Primary(string keyword) + [InlineData("partial class")] + [InlineData("partial struct")] + [InlineData("readonly partial struct")] + public void Constructor_Parameter_DeleteInsert_Primary(string keywords) { - var srcA1 = "partial " + keyword + " C(int P);"; - var srcB1 = "partial " + keyword + " C;"; + var srcA1 = keywords + " C(int P);"; + var srcB1 = keywords + " C;"; - var srcA2 = "partial " + keyword + " C;"; - var srcB2 = "partial " + keyword + " C(int P);"; + var srcA2 = keywords + " C;"; + var srcB2 = keywords + " C(int P);"; EditAndContinueValidation.VerifySemantics( new[] { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, new[] { DocumentResults(), + DocumentResults( + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), partialType: "C", preserveLocalVariables: true) + }), + }); + } + + [Theory] + [InlineData("partial record")] + [InlineData("partial record struct")] + [InlineData("readonly partial record struct")] + public void Constructor_Parameter_DeleteInsert_Primary_Record(string keywords) + { + var srcA1 = keywords + " C(int P);"; + var srcB1 = keywords + " C;"; + + var srcA2 = keywords + " C;"; + var srcB2 = keywords + " C(int P);"; + EditAndContinueValidation.VerifySemantics( + new[] { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, + new[] + { + DocumentResults(), DocumentResults( semanticEdits: new[] { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), partialType: "C", preserveLocalVariables: true) }), }); @@ -10994,6 +11113,8 @@ public void Constructor_Parameter_DeleteInsert_ReplacingNonPrimaryWithPrimary_Re DocumentResults( semanticEdits: new[] { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").InstanceConstructors.Single(c => c.Parameters is [{ Name: "P" }]), partialType: "C", preserveLocalVariables: true), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.Deconstruct")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), @@ -11081,7 +11202,13 @@ public void Constructor_Parameter_DeleteInsert_SwappingNonPrimaryWithPrimary_Rec new[] { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, new[] { - DocumentResults(), + DocumentResults( + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.set_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + } + ), DocumentResults( semanticEdits: new[] @@ -11091,7 +11218,7 @@ public void Constructor_Parameter_DeleteInsert_SwappingNonPrimaryWithPrimary_Rec SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), - SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")) + SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), }), }); } @@ -11532,6 +11659,8 @@ public void Constructor_Instance_Update_Modifier_Extern_Add() [Theory] [InlineData("struct")] [InlineData("record struct")] + [InlineData("readonly struct")] + [InlineData("readonly record struct")] public void Constructor_Instance_Insert_Struct(string keyword) { var src1 = keyword + " C { }"; @@ -11571,16 +11700,12 @@ public void Constructor_Instance_Insert_Struct_Primary_Record() var edits = GetTopEdits(src1, src2); - edits.VerifySemantics( + edits.VerifySemanticDiagnostics( new[] { - SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), - SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryDeconstructor("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), - SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + Diagnostic(RudeEditKind.InsertIntoStruct, "int X", GetResource("parameter"), GetResource("record struct")) }, - capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Theory] @@ -11623,13 +11748,37 @@ public void Constructor_Instance_Insert_ReplacingDefault_Class_Primary() public void Constructor_Instance_Insert_ReplacingDefault_Class_Primary_Record() { var src1 = "record C { }"; - var src2 = "record C(int X) { }"; + var src2 = "record C(int P) { }"; var edits = GetTopEdits(src1, src2); edits.VerifySemantics( new[] { + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.P")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryDeconstructor("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), + SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetParameterlessConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); + } + + [Fact] + public void Constructor_Instance_Insert_ReplacingDefault_Class_Primary_Record_Generic() + { + var src1 = "record C { }"; + var src2 = "record C(int P) { }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.P")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryConstructor("C")), SemanticEdit(SemanticEditKind.Insert, c => c.GetPrimaryDeconstructor("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), @@ -11638,6 +11787,19 @@ public void Constructor_Instance_Insert_ReplacingDefault_Class_Primary_Record() SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), SemanticEdit(SemanticEditKind.Delete, c => c.GetParameterlessConstructor("C"), deletedSymbolContainerProvider: c => c.GetMember("C")) }, + capabilities: + EditAndContinueCapabilities.AddMethodToExistingType | + EditAndContinueCapabilities.AddInstanceFieldToExistingType | + EditAndContinueCapabilities.GenericAddFieldToExistingType | + EditAndContinueCapabilities.GenericAddMethodToExistingType | + EditAndContinueCapabilities.GenericUpdateMethod); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "(int P)", GetResource("constructor")), + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "int P", GetResource("parameter")), + }, capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } @@ -11847,6 +12009,8 @@ public void Constructor_Instance_Insert_ReplacingSynthesizedWithCustom_Primary_R [Theory] [InlineData("struct")] [InlineData("record struct")] + [InlineData("readonly struct")] + [InlineData("readonly record struct")] public void Constructor_Instance_Delete_Struct(string keyword) { var src1 = keyword + " C { public C(int X) {} }"; @@ -11881,7 +12045,7 @@ public void Constructor_Instance_Delete_Struct_Primary() [Fact] public void Constructor_Instance_Delete_Struct_Primary_Record() { - var src1 = "record struct C(int X) { }"; + var src1 = "record struct C(int P) { }"; var src2 = "record struct C { }"; var edits = GetTopEdits(src1, src2); @@ -11894,6 +12058,8 @@ public void Constructor_Instance_Delete_Struct_Primary_Record() SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.PrintMembers")), SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.set_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), }, capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } @@ -11937,7 +12103,7 @@ public void Constructor_Instance_Delete_ReplacingWithDefault_Class_Primary() [Fact] public void Constructor_Instance_Delete_ReplacingWithDefault_Class_Primary_Record() { - var src1 = "record C(int X) { }"; + var src1 = "record C(int P) { }"; var src2 = "record C { }"; var edits = GetTopEdits(src1, src2); @@ -11952,6 +12118,8 @@ public void Constructor_Instance_Delete_ReplacingWithDefault_Class_Primary_Recor SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.set_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), }, capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } @@ -12066,7 +12234,7 @@ public void Constructor_Instance_Delete_Primary_Class() [Fact] public void Constructor_Instance_Delete_Primary_Record() { - var src1 = "record C(int a) { }"; + var src1 = "record C(int P) { }"; var src2 = "record C { }"; var edits = GetTopEdits(src1, src2); @@ -12080,6 +12248,8 @@ public void Constructor_Instance_Delete_Primary_Record() SemanticEdit(SemanticEditKind.Update, c => c.GetSpecializedEqualsOverload("C")), SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.GetHashCode")), SemanticEdit(SemanticEditKind.Update, c => c.GetCopyConstructor("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.set_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), }, capabilities: EditAndContinueCapabilities.Baseline); } @@ -12237,19 +12407,17 @@ public void Constructor_Instance_Delete_Primary_ReplacingWithRegular_AbstractTyp capabilities: EditAndContinueCapabilities.Baseline); } - [Theory] - [InlineData("record")] - [InlineData("class")] - public void Constructor_Instance_InsertDelete_Primary_Partial(string keyword) + [Fact] + public void Constructor_Instance_InsertDelete_Primary_Partial_Class() { - var src1 = $$""" - partial {{keyword}} C { } - partial {{keyword}} C(int X); - """; - var src2 = $$""" - partial {{keyword}} C(int X) { } - partial {{keyword}} C; - """; + var src1 = @" +partial class C { } +partial class C(int P); +"; + var src2 = @" +partial class C(int P) { } +partial class C; +"; var edits = GetTopEdits(src1, src2); @@ -12257,6 +12425,26 @@ public void Constructor_Instance_InsertDelete_Primary_Partial(string keyword) SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); } + [Fact] + public void Constructor_Instance_InsertDelete_Primary_Partial_Record() + { + var src1 = @" +partial record C { } +partial record C(int P); +"; + var src2 = @" +partial record C(int P) { } +partial record C; +"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); + } + [Fact] public void Constructor_Instance_Partial_DeletePrivateInsertPrivate() { @@ -13286,7 +13474,8 @@ public void PropertyInitializer_Update3() { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.a").GetMethod), SemanticEdit(SemanticEditKind.Update, c => c.GetParameterlessConstructor("C"), preserveLocalVariables: true) - }); + }, + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); } [Fact] @@ -13456,8 +13645,9 @@ public void FieldInitializerUpdate_InstanceCtorUpdate2(string typeKind) } [Theory] - [InlineData("class ")] + [InlineData("class")] [InlineData("struct")] + [InlineData("readonly struct")] public void PropertyInitializerUpdate_InstanceCtorUpdate2(string typeKind) { var src1 = typeKind + " C { int a { get; } = 1; public C() { } }"; @@ -13643,8 +13833,9 @@ public void FieldInitializerUpdate_StaticCtorInsertExplicit() } [Theory] - [InlineData("class ")] + [InlineData("class")] [InlineData("struct")] + [InlineData("readonly struct")] public void FieldInitializerUpdate_Constructor_Instance_InsertExplicit(string typeKind) { var src1 = typeKind + " C { int a; }"; @@ -13658,8 +13849,9 @@ public void FieldInitializerUpdate_Constructor_Instance_InsertExplicit(string ty } [Theory] - [InlineData("class ")] + [InlineData("class")] [InlineData("struct")] + [InlineData("readonly struct")] public void PropertyInitializerUpdate_Constructor_Instance_InsertExplicit(string typeKind) { var src1 = typeKind + " C { int a { get; } = 1; }"; @@ -16443,7 +16635,12 @@ public void Property_Update_AddAttribute() var edits = GetTopEdits(src1, src2); edits.VerifySemantics( - new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P")) }, + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + }, capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); edits.VerifySemanticDiagnostics( @@ -16478,7 +16675,11 @@ public void PropertyAccessorUpdate_AddAttribute() var edits = GetTopEdits(src1, src2); edits.VerifySemantics( - new[] { SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").GetMember("P").GetMethod) }, + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.get_P")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), + }, capabilities: EditAndContinueCapabilities.ChangeCustomAttributes); edits.VerifySemanticDiagnostics( @@ -16693,6 +16894,48 @@ public void Property_Insert_Auto_Static() capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType); } + [Fact] + public void Property_Insert_Auto_GenericType() + { + var src1 = "class C { }"; + var src2 = "class C { int P { get; set; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] { SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C").GetMember("P")) }, + capabilities: + EditAndContinueCapabilities.AddMethodToExistingType | + EditAndContinueCapabilities.AddInstanceFieldToExistingType | + EditAndContinueCapabilities.GenericAddMethodToExistingType | + EditAndContinueCapabilities.GenericAddFieldToExistingType); + + edits.VerifySemanticDiagnostics( + new[] { Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "int P", GetResource("auto-property")) }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddInstanceFieldToExistingType); + } + + [Fact] + public void Property_Insert_Auto_GenericType_Static() + { + var src1 = "class C { }"; + var src2 = "class C { static int P { get; set; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] { SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C").GetMember("P")) }, + capabilities: + EditAndContinueCapabilities.AddMethodToExistingType | + EditAndContinueCapabilities.AddStaticFieldToExistingType | + EditAndContinueCapabilities.GenericAddMethodToExistingType | + EditAndContinueCapabilities.GenericAddFieldToExistingType); + + edits.VerifySemanticDiagnostics( + new[] { Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "static int P", GetResource("auto-property")) }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType); + } + // Design: Adding private accessors should also be allowed since we now allow adding private methods // and adding public properties and/or public accessors are not allowed. [Fact] @@ -16825,6 +17068,7 @@ public void Property_Auto_Private_AccessorDelete_Get() new[] { SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.set_P")), }, capabilities: EditAndContinueCapabilities.Baseline); } @@ -16900,7 +17144,7 @@ public void Propert_Auto_Private_AccessorDelete_Init() } [Fact] - public void Property_Auto_AccessorUpdate() + public void Property_Auto_Accessor_Update() { var src1 = "class C { int P { get; } }"; var src2 = "class C { int P { set; } }"; @@ -16913,6 +17157,134 @@ public void Property_Auto_AccessorUpdate() Diagnostic(RudeEditKind.AccessorKindUpdate, "set", CSharpFeaturesResources.property_setter)); } + [Fact] + public void Property_Auto_Accessor_Update_ReplacingExplicitWithImplicit() + { + var src1 = "class C { int P { get => 1; } }"; + var src2 = "class C { int P { get; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "get", GetResource("property getter")) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Property_Auto_Accessor_Update_ReplacingExplicitWithImplicit_GenericType() + { + var src1 = "class C { int P { get => 1; } }"; + var src2 = "class C { int P { get; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: + EditAndContinueCapabilities.AddInstanceFieldToExistingType | + EditAndContinueCapabilities.GenericAddFieldToExistingType | + EditAndContinueCapabilities.GenericUpdateMethod); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "get", GetResource("property getter")) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Property_Auto_Accessor_Update_ReplacingImplicitWithExplicit() + { + var src1 = "class C { int P { get; } }"; + var src2 = "class C { int P { get => 1; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Property_Auto_Accessor_Update_ReplacingImplicitWithExplicit_GenericType() + { + var src1 = "class C { int P { get; } }"; + var src2 = "class C { int P { get => 1; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: EditAndContinueCapabilities.GenericUpdateMethod); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "get", GetResource("property getter")) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Property_Auto_Accessor_Update_ReplacingImplicitWithExpressionBodiedProperty() + { + var src1 = "class C { int P { get; } }"; + var src2 = "class C { int P => 1; }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Property_Auto_Accessor_Update_ReplacingImplicitWithExpressionBodiedProperty_GenericType() + { + var src1 = "class C { int P { get; } }"; + var src2 = "class C { int P => 1; }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.P").GetMethod) + }, + capabilities: EditAndContinueCapabilities.GenericUpdateMethod); + + edits.VerifySemanticDiagnostics( + new[] + { + Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "int P", GetResource("property")), + Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "int P", GetResource("property getter")) + }, + capabilities: EditAndContinueCapabilities.Baseline); + } + [Fact] public void Property_ReadOnlyRef_Insert() { diff --git a/src/EditorFeatures/Test/EditAndContinue/ActiveStatementsMapTests.cs b/src/EditorFeatures/Test/EditAndContinue/ActiveStatementsMapTests.cs index cae045628fbe4..8cbcfab49c234 100644 --- a/src/EditorFeatures/Test/EditAndContinue/ActiveStatementsMapTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/ActiveStatementsMapTests.cs @@ -61,6 +61,33 @@ public void GetSpansStartingInSpan2() Assert.Equal(new Range(5, 6), ActiveStatementsMap.GetSpansStartingInSpan(span.Start, span.End, array, startPositionComparer: (x, y) => x.Start.CompareTo(y))); } + [Theory] + [InlineData(/*span*/ 5, 1, 5, 2, /*expected*/ 0, 2)] + [InlineData(/*span*/ 5, 1, 5, 8, /*expected*/ 0, 2)] + public void GetSpansStartingInSpan_MultipleSameStart1(int sl, int sc, int el, int ec, int s, int e) + { + var span = new LinePositionSpan(new(sl, sc), new(el, ec)); + var array = ImmutableArray.Create( + new LinePositionSpan(new(5, 1), new(5, 2)), + new LinePositionSpan(new(5, 1), new(5, 8)), + new LinePositionSpan(new(6, 4), new(6, 18))); + + Assert.Equal(new Range(s, e), ActiveStatementsMap.GetSpansStartingInSpan(span.Start, span.End, array, startPositionComparer: (x, y) => x.Start.CompareTo(y))); + } + + [Theory] + [InlineData(/*span*/ 5, 1, 5, 2, /*expected*/ 0, 3)] + public void GetSpansStartingInSpan_MultipleSameStart2(int sl, int sc, int el, int ec, int s, int e) + { + var span = new LinePositionSpan(new(sl, sc), new(el, ec)); + var array = ImmutableArray.Create( + new LinePositionSpan(new(5, 1), new(5, 2)), + new LinePositionSpan(new(5, 1), new(5, 3)), + new LinePositionSpan(new(5, 1), new(5, 8))); + + Assert.Equal(new Range(s, e), ActiveStatementsMap.GetSpansStartingInSpan(span.Start, span.End, array, startPositionComparer: (x, y) => x.Start.CompareTo(y))); + } + [Fact] public async Task Ordering() { diff --git a/src/EditorFeatures/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs b/src/EditorFeatures/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs index 4e4b0ee6b0b9a..d5b0cdb0c561e 100644 --- a/src/EditorFeatures/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs +++ b/src/EditorFeatures/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs @@ -64,8 +64,11 @@ public ActiveStatementsDescription(string oldMarkedSource, string newMarkedSourc // initialize with deleted spans (they will retain their file path): foreach (var oldStatement in OldStatements) { - newMappedSpans[oldStatement.Statement.Ordinal] = new SourceFileSpan(oldStatement.Statement.FilePath, default); - newMappedRegions[oldStatement.Statement.Ordinal] = ImmutableArray.Empty; + if (oldStatement.Statement != null) + { + newMappedSpans[oldStatement.Statement.Ordinal] = new SourceFileSpan(oldStatement.Statement.FilePath, default); + newMappedRegions[oldStatement.Statement.Ordinal] = ImmutableArray.Empty; + } } // update with spans marked in the new source: diff --git a/src/EditorFeatures/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs b/src/EditorFeatures/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs index 19ee42a9ebc78..5121c0002e647 100644 --- a/src/EditorFeatures/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs +++ b/src/EditorFeatures/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs @@ -456,7 +456,7 @@ private static string DisplaySpan(SyntaxTree tree, SourceFileSpan span) internal static IEnumerable> GetMethodMatches(AbstractEditAndContinueAnalyzer analyzer, Match bodyMatch) { Dictionary? lazyActiveOrMatchedLambdas = null; - var map = analyzer.GetTestAccessor().ComputeMap(bodyMatch, new ArrayBuilder(), ref lazyActiveOrMatchedLambdas); + var map = analyzer.GetTestAccessor().IncludeLambdaBodyMaps(BidirectionalMap.FromMatch(bodyMatch), new ArrayBuilder(), ref lazyActiveOrMatchedLambdas); var result = new Dictionary(); foreach (var pair in map.Forward) diff --git a/src/EditorFeatures/TestUtilities/EditAndContinue/SourceMarkers.cs b/src/EditorFeatures/TestUtilities/EditAndContinue/SourceMarkers.cs index b45db007aec80..141c01a39880d 100644 --- a/src/EditorFeatures/TestUtilities/EditAndContinue/SourceMarkers.cs +++ b/src/EditorFeatures/TestUtilities/EditAndContinue/SourceMarkers.cs @@ -17,14 +17,7 @@ namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests; internal static class SourceMarkers { private static readonly Regex s_tags = new( - @"[<][/]?(AS|ER|N|TS)[:][.0-9,]+[>]", - RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); - - private static readonly Regex s_activeStatementPattern = new( - @"[<]AS[:] (?[0-9,]+) [>] - (?.*) - [<][/]AS[:] (\k) [>]", - RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); + "[<] (?/?) (?(AS|ER|N|TS))[:] (?[.0-9,]+) (?/?) [>]", RegexOptions.IgnorePatternWhitespace); public static readonly Regex ExceptionRegionPattern = new( @"[<]ER[:] (?(?:[0-9]+[.][0-9]+[,]?)+) [>] @@ -38,12 +31,6 @@ internal static class SourceMarkers [<][/]TS[:] (\k) [>]", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); - private static readonly Regex s_nodePattern = new( - @"[<]N[:] (?[0-9]+[.][0-9]+) [>] - (?.*) - [<][/]N[:] (\k) [>]", - RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); - internal static string Clear(string source) => s_tags.Replace(source, m => new string(' ', m.Length)); @@ -55,40 +42,49 @@ internal static string[] Clear(string[] sources) let parts = ids.Split('.') select (int.Parse(parts[0]), (parts.Length > 1) ? int.Parse(parts[1]) : 0); - private static IEnumerable<(TextSpan Span, ImmutableArray<(int major, int minor)> Ids)> GetSpans(string markedSource, Regex regex, string contentGroupName) + private static IEnumerable<((int major, int minor) id, TextSpan span)> GetSpans(string markedSource, string tagName) { - return Recurse(markedSource, offset: 0); + // id -> content start index + var tagMap = new Dictionary<(int major, int minor), (int start, int end)>(); - IEnumerable<(TextSpan Span, ImmutableArray<(int major, int minor)> Ids)> Recurse(string markedSource, int offset) + foreach (var match in s_tags.Matches(markedSource).ToEnumerable()) { - foreach (var match in regex.Matches(markedSource).ToEnumerable()) + if (match.Groups["Name"].Value != tagName) { - var markedSyntax = match.Groups[contentGroupName]; - var ids = ParseIds(match).ToImmutableArray(); - var absoluteOffset = offset + markedSyntax.Index; + continue; + } - var span = markedSyntax.Length != 0 ? new TextSpan(absoluteOffset, markedSyntax.Length) : new TextSpan(); - yield return (span, ids); + var isStartingTag = match.Groups["IsEnd"].Value == "" || match.Groups["IsStartAndEnd"].Value != ""; + var isEndingTag = match.Groups["IsEnd"].Value != "" || match.Groups["IsStartAndEnd"].Value != ""; + Contract.ThrowIfFalse(isStartingTag || isEndingTag); - foreach (var nestedSpan in Recurse(markedSyntax.Value, absoluteOffset)) + foreach (var id in ParseIds(match)) + { + if (isStartingTag && isEndingTag) + { + tagMap.Add(id, (match.Index, match.Index)); + } + else if (isStartingTag) + { + tagMap.Add(id, (match.Index + match.Length, -1)); + } + else { - yield return nestedSpan; + tagMap[id] = (tagMap[id].start, match.Index); } } } - } - public static IEnumerable<(TextSpan Span, int Id)> GetActiveSpans(string markedSource) - { - foreach (var (span, ids) in GetSpans(markedSource, s_activeStatementPattern, "ActiveStatement")) + foreach (var (id, (start, end)) in tagMap.OrderBy(k => k.Key)) { - foreach (var (major, _) in ids) - { - yield return (span, major); - } + Contract.ThrowIfFalse(end >= 0, $"Missing ending tag for {id}"); + yield return (id, TextSpan.FromBounds(start, end)); } } + public static IEnumerable<(TextSpan Span, int Id)> GetActiveSpans(string markedSource) + => GetSpans(markedSource, tagName: "AS").Select(s => (s.span, s.id.major)); + public static TextSpan[] GetTrackingSpans(string src, int count) { var matches = s_trackingStatementPattern.Matches(src); @@ -145,12 +141,8 @@ public static ImmutableArray> GetNodeSpans(string marke { var result = new List>(); - foreach (var (span, ids) in GetSpans(markedSource, s_nodePattern, "Node")) + foreach (var ((major, minor), span) in GetSpans(markedSource, tagName: "N")) { - Debug.Assert(ids.Length == 1); - - var (major, minor) = ids[0]; - EnsureSlot(result, major); result[major] ??= new List(); EnsureSlot(result[major], minor); diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb index 3a07703976c07..07c7a569eaa5d 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb @@ -775,7 +775,7 @@ Module Module1 End Module " - Dim src2 = "" + Dim src2 = "" Dim edits = GetTopEdits(src1, src2) Dim active = GetActiveStatements(src1, src2) @@ -1854,7 +1854,7 @@ Class C Dim a(1), b(2) = 3 Sub Main - Dim c(1), d(2) = 3 + Dim c(1), d(2) = 3 End Sub End Class " diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/BreakpointSpansTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/BreakpointSpansTests.vb index ff3942ea20b4c..03605baee15d8 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/BreakpointSpansTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/BreakpointSpansTests.vb @@ -86,7 +86,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue.UnitTests Dim lastSpanEnd = 0 While position < endPosition Dim span As TextSpan = Nothing - If BreakpointSpans.TryGetEnclosingBreakpointSpan(root, position, minLength:=0, span) AndAlso span.End > lastSpanEnd Then + If BreakpointSpans.TryGetClosestBreakpointSpan(root, position, minLength:=0, span) AndAlso span.End > lastSpanEnd Then position = span.End lastSpanEnd = span.End Yield span diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/Helpers/EditingTestBase.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/Helpers/EditingTestBase.vb index 7d3b24358e166..3ccfb918dd8bb 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/Helpers/EditingTestBase.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/Helpers/EditingTestBase.vb @@ -196,7 +196,7 @@ End Namespace Dim m1 = MakeMethodBody(src1, methodKind) Dim m2 = MakeMethodBody(src2, methodKind) - Dim match = m1.ComputeMatch(m2, knownMatches:=Nothing) + Dim match = m1.ComputeSingleRootMatch(m2, knownMatches:=Nothing) Dim stateMachineInfo1 = m1.GetStateMachineInfo() Dim stateMachineInfo2 = m2.GetStateMachineInfo() diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/StatementMatchingTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/StatementMatchingTests.vb index 17415a7cf907a..c62f32a06d23b 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/StatementMatchingTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/StatementMatchingTests.vb @@ -72,7 +72,7 @@ End If Dim m2 = MakeMethodBody(src2) Dim knownMatches = {New KeyValuePair(Of SyntaxNode, SyntaxNode)(m1.RootNodes.First(), m2.RootNodes().First())} - Dim match = m1.ComputeMatch(m2, knownMatches) + Dim match = m1.ComputeSingleRootMatch(m2, knownMatches) Dim actual = ToMatchingPairs(match) Dim expected = New MatchingPairs From @@ -1398,7 +1398,7 @@ End Try Dim knownMatches = {New KeyValuePair(Of SyntaxNode, SyntaxNode)(b1.Statements(1), b2.Statements(0))} ' pre-matched: - Dim match = m1.ComputeMatch(m2, knownMatches) + Dim match = m1.ComputeSingleRootMatch(m2, knownMatches) Dim actual = ToMatchingPairs(match) Dim expected = New MatchingPairs From @@ -1412,7 +1412,7 @@ End Try expected.AssertEqual(actual) ' not pre-matched: - match = m1.ComputeMatch(m2, knownMatches:=Nothing) + match = m1.ComputeSingleRootMatch(m2, knownMatches:=Nothing) actual = ToMatchingPairs(match) expected = New MatchingPairs From diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb index 98ee6243066d9..4915d85f8efb5 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb @@ -1311,7 +1311,6 @@ End Class" Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Sub G(Of S)()", FeaturesResources.method), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Property P2", FeaturesResources.property_), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Event E", FeaturesResources.event_), - Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Sub New(x As Integer)", FeaturesResources.constructor), Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "Property P1(i As Integer)", GetResource("property")), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Set(value As Integer)", FeaturesResources.property_accessor) } @@ -1373,12 +1372,15 @@ End Class" edits.VerifySemanticDiagnostics( { + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Property P2", FeaturesResources.auto_property), + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "Property P3", FeaturesResources.auto_property), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F1", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F2", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F3", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F4", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F5(1, 2)", FeaturesResources.field), Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "F6?", FeaturesResources.field), + Diagnostic(RudeEditKind.InsertNotSupportedByRuntime, "WE", VBFeaturesResources.WithEvents_field), Diagnostic(RudeEditKind.InsertVirtual, "WE", VBFeaturesResources.WithEvents_field), Diagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, "Class C(Of T)", GetResource("constructor", "New()")) }, capabilities:=nonGenericCapabilities Or EditAndContinueCapabilities.GenericAddMethodToExistingType) @@ -10686,6 +10688,88 @@ End Class }) End Sub + + Public Sub Event_WithHandlerDeclaration_Insert() + Dim src1 = " +Class C +End Class +" + Dim src2 = " +Class C + Event E(a As Integer) +End Class +" + Dim edits = GetTopEdits(src1, src2) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.E")) + }, + capabilities:=EditAndContinueCapabilities.AddInstanceFieldToExistingType Or EditAndContinueCapabilities.AddMethodToExistingType) + End Sub + + + Public Sub Event_WithHandlerDeclaration_Insert_NoParameters() + Dim src1 = " +Class C +End Class +" + Dim src2 = " +Class C + Event E() +End Class +" + Dim edits = GetTopEdits(src1, src2) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.E")) + }, + capabilities:=EditAndContinueCapabilities.AddInstanceFieldToExistingType Or EditAndContinueCapabilities.AddMethodToExistingType) + End Sub + + + Public Sub Event_WithHandlerDeclaration_Delete() + Dim src1 = " +Class C + Event E(a As Integer) +End Class +" + Dim src2 = " +Class C +End Class +" + Dim edits = GetTopEdits(src1, src2) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.add_E"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.remove_E"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")) + }, + capabilities:=EditAndContinueCapabilities.AddInstanceFieldToExistingType Or EditAndContinueCapabilities.AddMethodToExistingType) + End Sub + + + Public Sub Event_WithHandlerDeclaration_Delete_NoParameters() + Dim src1 = " +Class C + Event E() +End Class +" + Dim src2 = " +Class C +End Class +" + Dim edits = GetTopEdits(src1, src2) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.add_E"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.remove_E"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")) + }, + capabilities:=EditAndContinueCapabilities.AddInstanceFieldToExistingType Or EditAndContinueCapabilities.AddMethodToExistingType) + End Sub + #End Region #Region "Parameters And Return Values" diff --git a/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs b/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs index e81ba79ec126c..361377dd039d7 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/BreakpointSpans.cs @@ -36,7 +36,7 @@ public static bool TryGetBreakpointSpan(SyntaxTree tree, int position, Cancellat } var root = tree.GetRoot(cancellationToken); - return TryGetClosestBreakpointSpan(root, position, out breakpointSpan); + return TryGetClosestBreakpointSpan(root, position, minLength: 0, out breakpointSpan); } private static bool IsBlank(TextLine line) @@ -58,26 +58,52 @@ private static bool IsBlank(TextLine line) /// Given a syntax token determines a text span delimited by the closest applicable sequence points /// encompassing the token. /// + /// + /// In case there are multiple breakpoint spans starting at the given , + /// can be used to disambiguate between them. + /// The inner-most available span whose length is at least is returned. + /// /// /// If the span exists it is possible to place a breakpoint at the given position. /// - public static bool TryGetClosestBreakpointSpan(SyntaxNode root, int position, out TextSpan span) + public static bool TryGetClosestBreakpointSpan(SyntaxNode root, int position, int minLength, out TextSpan span) { var node = root.FindToken(position).Parent; + var candidate = (TextSpan?)null; + while (node != null) { var breakpointSpan = TryCreateSpanForNode(node, position); if (breakpointSpan.HasValue) { span = breakpointSpan.Value; - return span != default; + if (span == default) + { + break; + } + + // the new breakpoint span doesn't alight with the previously found breakpoint span, return the previous one: + if (candidate.HasValue && breakpointSpan.Value.Start != candidate.Value.Start) + { + span = candidate.Value; + return true; + } + + // The span length meets the requirement: + if (breakpointSpan.Value.Length >= minLength) + { + span = breakpointSpan.Value; + return true; + } + + candidate = breakpointSpan; } node = node.Parent; } - span = default; - return false; + span = candidate.GetValueOrDefault(); + return candidate.HasValue; } private static TextSpan CreateSpan(SyntaxToken startToken, SyntaxToken endToken) @@ -172,14 +198,14 @@ private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) // on or after explicit base initializer: // C(...) :$$ [|B(...)|], I // C(...) : [|B(...)|], I where ... $$ - var baseInitializer = typeDeclaration.BaseList?.Types.FirstOrDefault(t => t.IsKind(SyntaxKind.PrimaryConstructorBaseType)); + var baseInitializer = (PrimaryConstructorBaseTypeSyntax?)typeDeclaration.BaseList?.Types.FirstOrDefault(t => t.IsKind(SyntaxKind.PrimaryConstructorBaseType)); if (baseInitializer != null && position > typeDeclaration.BaseList!.ColonToken.SpanStart) { - return baseInitializer.Span; + return CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer); } // record properties and copy constructor - if (position >= typeDeclaration.Identifier.SpanStart && node is RecordDeclarationSyntax) + if (position >= typeDeclaration.Identifier.SpanStart && node is RecordDeclarationSyntax recordDeclaration) { // on identifier: // record $$C(...) : B(...); @@ -187,9 +213,7 @@ private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) if (position <= typeDeclaration.ParameterList.SpanStart) { // copy-constructor: [|C|] - return CreateSpan( - typeDeclaration.Identifier, - LastNotMissing(typeDeclaration.Identifier, typeDeclaration.TypeParameterList?.GreaterThanToken ?? default)); + return CreateSpanForCopyConstructor(recordDeclaration); } // on parameter: @@ -200,7 +224,7 @@ private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) if (parameter != null) { // [A][|int p|] = default - return CreateSpan(parameter.Modifiers, parameter.Type, parameter.Identifier); + return CreateSpanForRecordParameter(parameter); } static ParameterSyntax? GetParameter(int position, SeparatedSyntaxList parameters) @@ -228,8 +252,9 @@ private static int GetEndPosition(SyntaxNodeOrToken nodeOrToken) // C(...) : [|B(...)|] // implicit base initializer // [|C(...)|] - return (baseInitializer != null) ? baseInitializer.Span : - TextSpan.FromBounds(typeDeclaration.Identifier.SpanStart, typeDeclaration.ParameterList.Span.End); + return (baseInitializer != null) + ? CreateSpanForExplicitPrimaryConstructorInitializer(baseInitializer) + : CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); } return null; @@ -303,7 +328,7 @@ node is SwitchExpressionSyntax switchExpression && } else { - return CreateSpan(node); + return CreateSpanForAutoPropertyAccessor(accessor); } case SyntaxKind.PropertyDeclaration: @@ -455,6 +480,73 @@ internal static TextSpan CreateSpanForExplicitConstructorInitializer(Constructor internal static IEnumerable GetActiveTokensForExplicitConstructorInitializer(ConstructorInitializerSyntax constructorInitializer) => SpecializedCollections.SingletonEnumerable(constructorInitializer.ThisOrBaseKeyword).Concat(constructorInitializer.ArgumentList.DescendantTokens()); + internal static TextSpan CreateSpanForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) + { + Debug.Assert(typeDeclaration.ParameterList != null); + return TextSpan.FromBounds(typeDeclaration.Identifier.SpanStart, typeDeclaration.ParameterList.Span.End); + } + + internal static IEnumerable GetActiveTokensForImplicitPrimaryConstructorInitializer(TypeDeclarationSyntax typeDeclaration) + { + Debug.Assert(typeDeclaration.ParameterList != null); + + yield return typeDeclaration.Identifier; + + if (typeDeclaration.TypeParameterList != null) + { + foreach (var token in typeDeclaration.TypeParameterList.DescendantTokens()) + yield return token; + } + + foreach (var token in typeDeclaration.ParameterList.DescendantTokens()) + yield return token; + } + + internal static TextSpan CreateSpanForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) + => baseTypeSyntax.Span; + + internal static IEnumerable GetActiveTokensForExplicitPrimaryConstructorInitializer(PrimaryConstructorBaseTypeSyntax baseTypeSyntax) + => baseTypeSyntax.DescendantTokens(); + + internal static TextSpan CreateSpanForCopyConstructor(RecordDeclarationSyntax recordDeclaration) + => CreateSpan( + recordDeclaration.Identifier, + LastNotMissing(recordDeclaration.Identifier, recordDeclaration.TypeParameterList?.GreaterThanToken ?? default)); + + internal static IEnumerable GetActiveTokensForCopyConstructor(RecordDeclarationSyntax recordDeclaration) + { + yield return recordDeclaration.Identifier; + + if (recordDeclaration.TypeParameterList != null) + { + foreach (var token in recordDeclaration.TypeParameterList.DescendantTokens()) + yield return token; + } + } + + internal static TextSpan CreateSpanForRecordParameter(ParameterSyntax parameter) + => CreateSpan(parameter.Modifiers, parameter.Type, parameter.Identifier); + + internal static IEnumerable GetActiveTokensForRecordParameter(ParameterSyntax parameter) + { + foreach (var modifier in parameter.Modifiers) + yield return modifier; + + if (parameter.Type != null) + { + foreach (var token in parameter.Type.DescendantTokens()) + yield return token; + } + + yield return parameter.Identifier; + } + + internal static TextSpan CreateSpanForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) + => accessor.Span; + + internal static IEnumerable GetActiveTokensForAutoPropertyAccessor(AccessorDeclarationSyntax accessor) + => accessor.DescendantTokens(); + private static TextSpan? TryCreateSpanForFieldDeclaration(BaseFieldDeclarationSyntax fieldDeclaration, int position) => TryCreateSpanForVariableDeclaration(fieldDeclaration.Declaration, fieldDeclaration.Modifiers, fieldDeclaration.SemicolonToken, position); diff --git a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs index e6a4d7bb635ba..2b852efcdac26 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs @@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -73,14 +74,40 @@ private enum SwitchExpressionPart /// for property initializers and expression bodies. /// for indexer expression bodies. /// for getter of an expression-bodied property/indexer. + /// for top-level statements. + /// for record copy-constructors. + /// for primary constructors. + /// for record primary constructor parameters. /// - internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, out OneOrMany declarations) + internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations) { var current = node; while (current != null && current != root) { switch (current.Kind()) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + var typeDeclaration = (TypeDeclarationSyntax)current; + + // type declaration with primary constructor + if (typeDeclaration.ParameterList != null) + { + declarations = new(typeDeclaration.ParameterList); + return true; + } + + break; + + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + var recordDeclaration = (RecordDeclarationSyntax)current; + + declarations = (recordDeclaration.ParameterList != null && activeSpan.OverlapsWith(recordDeclaration.ParameterList.Span)) + ? new(recordDeclaration.ParameterList) : new(recordDeclaration); + + return true; + case SyntaxKind.MethodDeclaration: case SyntaxKind.ConversionOperatorDeclaration: case SyntaxKind.OperatorDeclaration: @@ -96,7 +123,7 @@ internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode nod case SyntaxKind.PropertyDeclaration: // int P { get; } = [|initializer|]; - RoslynDebug.Assert(((PropertyDeclarationSyntax)current).Initializer != null); + Debug.Assert(((PropertyDeclarationSyntax)current).Initializer != null); declarations = new(current); return true; @@ -107,9 +134,19 @@ internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode nod declarations = new(((BaseFieldDeclarationSyntax)current).Declaration.Variables.First()); return true; + case SyntaxKind.Parameter: + + if (current is { Parent.Parent: RecordDeclarationSyntax }) + { + declarations = new(current); + return true; + } + + break; + case SyntaxKind.VariableDeclarator: // public static int F = 1, [|G = 2|]; - RoslynDebug.Assert(current.Parent.IsKind(SyntaxKind.VariableDeclaration)); + Debug.Assert(current.Parent.IsKind(SyntaxKind.VariableDeclaration)); switch (current.Parent.Parent!.Kind()) { @@ -131,6 +168,11 @@ internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode nod } break; + + case SyntaxKind.GlobalStatement: + Debug.Assert(current.Parent.IsKind(SyntaxKind.CompilationUnit)); + declarations = new(current.Parent); + return true; } current = current.Parent; @@ -140,21 +182,15 @@ internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode nod return false; } - internal override MemberBody? TryGetDeclarationBody(SyntaxNode node) - => SyntaxUtilities.TryGetDeclarationBody(node); + internal override MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) + => SyntaxUtilities.TryGetDeclarationBody(node, symbol); - internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration) - => false; + internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member) + => member is IMethodSymbol { AssociatedSymbol: IPropertySymbol property } && property.IsSynthesizedAutoProperty(); protected override bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod) => true; - internal override bool HasParameterClosureScope(ISymbol member) - { - // in instance constructor parameters are lifted to a closure different from method body - return (member as IMethodSymbol)?.MethodKind == MethodKind.Constructor; - } - protected override IEnumerable GetVariableUseSites(IEnumerable roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken) { Debug.Assert(localOrParameter is IParameterSymbol or ILocalSymbol or IRangeVariableSymbol); @@ -171,37 +207,6 @@ where node.IsKind(SyntaxKind.IdentifierName) select node; } - protected override SyntaxNode GetEncompassingAncestorImpl(SyntaxNode bodyOrMatchRoot) - { - // Constructor may contain active nodes outside of its body (constructor initializer), - // but within the body of the member declaration (the parent). - if (bodyOrMatchRoot.Parent.IsKind(SyntaxKind.ConstructorDeclaration)) - { - return bodyOrMatchRoot.Parent; - } - - // Field initializer match root -- an active statement may include the modifiers - // and type specification of the field declaration. - if (bodyOrMatchRoot.IsKind(SyntaxKind.EqualsValueClause) && - bodyOrMatchRoot.Parent.IsKind(SyntaxKind.VariableDeclarator) && - bodyOrMatchRoot.Parent.Parent.IsKind(SyntaxKind.FieldDeclaration)) - { - return bodyOrMatchRoot.Parent.Parent; - } - - // Field initializer body -- an active statement may include the modifiers - // and type specification of the field declaration. - if (bodyOrMatchRoot.Parent.IsKind(SyntaxKind.EqualsValueClause) && - bodyOrMatchRoot.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator) && - bodyOrMatchRoot.Parent.Parent.Parent.IsKind(SyntaxKind.FieldDeclaration)) - { - return bodyOrMatchRoot.Parent.Parent.Parent; - } - - // otherwise all active statements are covered by the body/match root itself: - return bodyOrMatchRoot; - } - internal static SyntaxNode FindStatementAndPartner( TextSpan span, SyntaxNode body, @@ -386,10 +391,10 @@ private static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode left, SyntaxNod internal override bool IsClosureScope(SyntaxNode node) => LambdaUtilities.IsClosureScope(node); - protected override LambdaBody? FindEnclosingLambdaBody(SyntaxNode root, SyntaxNode node) + protected override LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node) { var current = node; - while (current != root && current != null) + while (current != encompassingAncestor && current != null) { if (LambdaUtilities.IsLambdaBodyStatementOrExpression(current, out var body)) { @@ -454,7 +459,9 @@ static SyntaxNode GetMatchingRoot(SyntaxNode body) var oldRoot = GetMatchingRoot(oldBody); var newRoot = GetMatchingRoot(newBody); - return new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true).ComputeMatch(oldRoot, newRoot, knownMatches); + var comparer = new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true); + + return comparer.ComputeMatch(oldRoot, newRoot, knownMatches); } return SyntaxComparer.Statement.ComputeMatch(oldBody, newBody, knownMatches); @@ -579,13 +586,19 @@ internal override bool ExperimentalFeaturesEnabled(SyntaxTree tree) protected override bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2) => SyntaxComparer.Statement.GetLabel(node1) == SyntaxComparer.Statement.GetLabel(node2); - protected override bool TryGetEnclosingBreakpointSpan(SyntaxNode root, int position, out TextSpan span) - => BreakpointSpans.TryGetClosestBreakpointSpan(root, position, out span); + protected override bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span) + => BreakpointSpans.TryGetClosestBreakpointSpan(token.Parent!, token.SpanStart, minLength: token.Span.Length, out span); protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span) { switch (node.Kind()) { + case SyntaxKind.ArrowExpressionClause: + // Member block body is matched with expression body, so we might be called with statement part of open or closed brace. + Debug.Assert(statementPart is DefaultStatementPart or (int)BlockPart.OpenBrace or (int)BlockPart.CloseBrace); + span = ((ArrowExpressionClauseSyntax)node).Expression.Span; + return true; + case SyntaxKind.Block: span = GetActiveSpan((BlockSyntax)node, (BlockPart)statementPart); return true; @@ -605,7 +618,7 @@ protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int Debug.Assert(statementPart == DefaultStatementPart); var doStatement = (DoStatementSyntax)node; - return BreakpointSpans.TryGetClosestBreakpointSpan(node, doStatement.WhileKeyword.SpanStart, out span); + return BreakpointSpans.TryGetClosestBreakpointSpan(node, doStatement.WhileKeyword.SpanStart, minLength, out span); case SyntaxKind.PropertyDeclaration: // The active span corresponding to a property declaration is the span corresponding to its initializer (if any), @@ -616,7 +629,7 @@ protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int var propertyDeclaration = (PropertyDeclarationSyntax)node; if (propertyDeclaration.Initializer != null && - BreakpointSpans.TryGetClosestBreakpointSpan(node, propertyDeclaration.Initializer.SpanStart, out span)) + BreakpointSpans.TryGetClosestBreakpointSpan(node, propertyDeclaration.Initializer.SpanStart, minLength, out span)) { return true; } @@ -639,14 +652,30 @@ protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int span = ((SwitchExpressionArmSyntax)node).Expression.Span; return true; + case SyntaxKind.ParameterList when node.Parent is TypeDeclarationSyntax typeDeclaration: + // The only case when an active statement is a parameter list is an active statement + // for an implicit constructor initializer of a type wiht primary constructor. + // In that case the span of the active statement starts before the parameter list + // (it includes the type name and type parameters). + span = BreakpointSpans.CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration); + return true; + + case SyntaxKind.RecordDeclaration: + case SyntaxKind.RecordStructDeclaration: + span = BreakpointSpans.CreateSpanForCopyConstructor((RecordDeclarationSyntax)node); + return true; + default: // make sure all nodes that use statement parts are handled above: Debug.Assert(statementPart == DefaultStatementPart); - return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, out span); + return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, minLength, out span); } } + public static (SyntaxNode node, int part) GetFirstBodyActiveStatement(SyntaxNode memberBody) + => (memberBody, memberBody.IsKind(SyntaxKind.Block) ? (int)BlockPart.OpenBrace : DefaultStatementPart); + protected override IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement) { var direction = +1; @@ -893,46 +922,6 @@ internal override bool IsRecordDeclaration(SyntaxNode node) internal override SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node) => node is CompilationUnitSyntax ? null : node.Parent!.FirstAncestorOrSelf(); - internal override bool HasBackingField(SyntaxNode propertyOrIndexerDeclaration) - => propertyOrIndexerDeclaration is PropertyDeclarationSyntax propertyDecl && - SyntaxUtilities.HasBackingField(propertyDecl); - - internal override bool TryGetAssociatedMemberDeclaration(SyntaxNode node, EditKind editKind, [NotNullWhen(true)] out SyntaxNode? declaration) - { - if (node is (kind: SyntaxKind.Parameter)) - { - Contract.ThrowIfFalse(node.Parent is (kind: SyntaxKind.ParameterList or SyntaxKind.BracketedParameterList)); - - // ParameterList represents the primary constructor: - declaration = node.Parent.Parent is TypeDeclarationSyntax ? node.Parent : node.Parent.Parent; - Contract.ThrowIfNull(declaration); - - return true; - } - - if (node is (kind: SyntaxKind.TypeParameter)) - { - Contract.ThrowIfFalse(node.Parent is (kind: SyntaxKind.TypeParameterList)); - - declaration = node.Parent.Parent!; - return true; - } - - // For deletes, we don't associate accessors with their parents, as deleting accessors is allowed - if (editKind != EditKind.Delete && - node.Parent?.Parent is (kind: - SyntaxKind.PropertyDeclaration or - SyntaxKind.IndexerDeclaration or - SyntaxKind.EventDeclaration)) - { - declaration = node.Parent.Parent; - return true; - } - - declaration = null; - return false; - } - internal override bool IsDeclarationWithInitializer(SyntaxNode declaration) => declaration is VariableDeclaratorSyntax { Initializer: not null } or PropertyDeclarationSyntax { Initializer: not null }; @@ -1135,6 +1124,50 @@ join newVariable in newVariables on oldVariable.Identifier.Text equals newVariab return OneOrMany.Create(result.ToImmutableArray()); } + // Update to a type declaration with primary constructor may also need to update + // the primary constructor, copy-constructor and/or synthesized record auto-properties. + // Active statements in bodies of these symbols lay within the type declaration node. + if (oldNode is TypeDeclarationSyntax oldTypeDeclaration && + newNode is TypeDeclarationSyntax newTypeDeclaration && + (oldTypeDeclaration.ParameterList != null || newTypeDeclaration.ParameterList != null)) + { + if (oldSymbol is not INamedTypeSymbol oldType || newSymbol is not INamedTypeSymbol newType) + { + throw ExceptionUtilities.Unreachable(); + } + + var result = new TemporaryArray<(ISymbol?, ISymbol?, EditKind)>(); + + // the type kind, attributes, constracints may have changed: + result.Add((oldSymbol, newSymbol, EditKind.Update)); + + var typeNameSpanChanged = + oldTypeDeclaration.Identifier.Span != newTypeDeclaration.Identifier.Span || + oldTypeDeclaration.TypeParameterList?.Span != newTypeDeclaration.TypeParameterList?.Span; + + // primary constructor active span: [|C(...)|] + if (typeNameSpanChanged || + oldTypeDeclaration.ParameterList?.Span != newTypeDeclaration.ParameterList?.Span) + { + var oldPrimaryConstructor = GetPrimaryConstructor(oldType, cancellationToken); + var newPrimaryConstructor = GetPrimaryConstructor(newType, cancellationToken); + + result.Add((oldPrimaryConstructor, newPrimaryConstructor, EditKind.Update)); + } + + // copy constructor active span: [|C|] + if (typeNameSpanChanged && (oldNode.IsKind(SyntaxKind.RecordDeclaration) || newNode.IsKind(SyntaxKind.RecordDeclaration))) + { + var oldCopyConstructor = oldType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); + var newCopyConstructor = newType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor()); + Debug.Assert(oldCopyConstructor != null || newCopyConstructor != null); + + result.Add((oldCopyConstructor, newCopyConstructor, EditKind.Update)); + } + + return (result.Count == 1) ? OneOrMany.Create(result[0]) : OneOrMany.Create(result.ToImmutableAndClear()); + } + break; case EditKind.Delete: @@ -1199,6 +1232,23 @@ join newVariable in newVariables on oldVariable.Identifier.Text equals newVariab : OneOrMany.Create((oldSymbol, newSymbol, editKind)); } + // record primary constructor parameter + if (oldNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } || + newNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax }) + { + var oldSynthesizedAutoProperty = (IPropertySymbol?)oldSymbol?.ContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); + var newSynthesizedAutoProperty = (IPropertySymbol?)newSymbol?.ContainingType.GetMembers(newSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty()); + + if (oldSynthesizedAutoProperty != null || newSynthesizedAutoProperty != null) + { + return OneOrMany.Create(ImmutableArray.Create( + (oldSymbol, newSymbol, editKind), + (oldSynthesizedAutoProperty, newSynthesizedAutoProperty, editKind), + (oldSynthesizedAutoProperty?.GetMethod, newSynthesizedAutoProperty?.GetMethod, editKind), + (oldSynthesizedAutoProperty?.SetMethod, newSynthesizedAutoProperty?.SetMethod, editKind))); + } + } + return (editKind == EditKind.Delete ? oldSymbol : newSymbol) is null ? OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty : new OneOrMany<(ISymbol?, ISymbol?, EditKind)>((oldSymbol, newSymbol, editKind)); } @@ -2356,12 +2406,12 @@ internal override void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder /// Return nodes that represent exception handlers encompassing the given active statement node. /// - protected override List GetExceptionHandlingAncestors(SyntaxNode node, bool isNonLeaf) + protected override List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf) { var result = new List(); var current = node; - while (current != null) + while (current != root) { var kind = current.Kind(); @@ -2400,6 +2450,7 @@ protected override List GetExceptionHandlingAncestors(SyntaxNode nod return result; } + Debug.Assert(current.Parent != null); current = current.Parent; } @@ -2692,13 +2743,15 @@ private static bool IsSimpleAwaitAssignment(SyntaxNode node, SyntaxNode awaitExp internal override void ReportOtherRudeEditsAroundActiveStatement( ArrayBuilder diagnostics, - Match match, + IReadOnlyDictionary reverseMap, SyntaxNode oldActiveStatement, + DeclarationBody oldBody, SyntaxNode newActiveStatement, + DeclarationBody newBody, bool isNonLeaf) { ReportRudeEditsForSwitchWhenClauses(diagnostics, oldActiveStatement, newActiveStatement); - ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, match, oldActiveStatement, newActiveStatement); + ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, reverseMap, oldActiveStatement, oldBody.EncompassingAncestor, newActiveStatement, newBody.EncompassingAncestor); ReportRudeEditsForCheckedStatements(diagnostics, oldActiveStatement, newActiveStatement, isNonLeaf); } @@ -2821,9 +2874,11 @@ private void ReportRudeEditsForCheckedStatements( private void ReportRudeEditsForAncestorsDeclaringInterStatementTemps( ArrayBuilder diagnostics, - Match match, + IReadOnlyDictionary reverseMap, SyntaxNode oldActiveStatement, - SyntaxNode newActiveStatement) + SyntaxNode oldEncompassingAncestor, + SyntaxNode newActiveStatement, + SyntaxNode newEncompassingAncestor) { // Rude Edits for fixed/using/lock/foreach statements that are added/updated around an active statement. // Although such changes are technically possible, they might lead to confusion since @@ -2834,30 +2889,48 @@ private void ReportRudeEditsForAncestorsDeclaringInterStatementTemps( // // Unlike exception regions matching where we use LCS, we allow reordering of the statements. - ReportUnmatchedStatements(diagnostics, match, n => n.IsKind(SyntaxKind.LockStatement), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n.IsKind(SyntaxKind.LockStatement), + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, areEquivalent: AreEquivalentActiveStatements, areSimilar: null); - ReportUnmatchedStatements(diagnostics, match, n => n.IsKind(SyntaxKind.FixedStatement), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements( + diagnostics, + reverseMap, + n => n.IsKind(SyntaxKind.FixedStatement), + oldActiveStatement, + oldEncompassingAncestor, + newActiveStatement, + newEncompassingAncestor, areEquivalent: AreEquivalentActiveStatements, areSimilar: (n1, n2) => DeclareSameIdentifiers(n1.Declaration.Variables, n2.Declaration.Variables)); // Using statements with declaration do not introduce compiler generated temporary. ReportUnmatchedStatements( diagnostics, - match, + reverseMap, n => n is UsingStatementSyntax usingStatement && usingStatement.Declaration is null, oldActiveStatement, + oldEncompassingAncestor, newActiveStatement, + newEncompassingAncestor, areEquivalent: AreEquivalentActiveStatements, areSimilar: null); ReportUnmatchedStatements( diagnostics, - match, + reverseMap, n => n.IsKind(SyntaxKind.ForEachStatement) || n.IsKind(SyntaxKind.ForEachVariableStatement), oldActiveStatement, + oldEncompassingAncestor, newActiveStatement, + newEncompassingAncestor, areEquivalent: AreEquivalentActiveStatements, areSimilar: AreSimilarActiveStatements); } diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CSharpLambdaBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CSharpLambdaBody.cs index 6cb04b1155272..46aafdb7c9444 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CSharpLambdaBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CSharpLambdaBody.cs @@ -22,16 +22,19 @@ public sealed override SyntaxTree SyntaxTree public override OneOrMany RootNodes => OneOrMany.Create(node); + public override SyntaxNode EncompassingAncestor + => node; + public override StateMachineInfo GetStateMachineInfo() => new( IsAsync: SyntaxUtilities.IsAsyncDeclaration(node.Parent!), IsIterator: SyntaxUtilities.IsIterator(node), HasSuspensionPoints: SyntaxUtilities.GetSuspensionPoints(node).Any()); - public override Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + public override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) => CSharpEditAndContinueAnalyzer.ComputeBodyMatch(node, ((CSharpLambdaBody)newBody).Node, knownMatches); - public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) => CSharpEditAndContinueAnalyzer.TryMatchActiveStatement(Node, ((CSharpLambdaBody)newBody).Node, oldStatement, out newStatement); public override LambdaBody? TryGetPartnerLambdaBody(SyntaxNode newLambda) diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CopyConstructorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CopyConstructorDeclarationBody.cs new file mode 100644 index 0000000000000..7e7e588150931 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/CopyConstructorDeclarationBody.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +///

+/// record [|C{T}|](int a, int b); +/// +internal sealed class CopyConstructorDeclarationBody(RecordDeclarationSyntax recordDeclaration) : InstanceConstructorDeclarationBody +{ + public override bool HasExplicitInitializer + => false; + + public override SyntaxNode? ExplicitBody + => null; + + public override SyntaxNode? MatchRoot + => null; + + public override SyntaxNode InitializerActiveStatement + => recordDeclaration; + + public override TextSpan InitializerActiveStatementSpan + => BreakpointSpans.CreateSpanForCopyConstructor(recordDeclaration); + + public override OneOrMany RootNodes + => new(recordDeclaration); + + public override SyntaxNode EncompassingAncestor + => recordDeclaration; + + public override TextSpan Envelope + => InitializerActiveStatementSpan; + + public override ImmutableArray GetCapturedVariables(SemanticModel model) + => ImmutableArray.Empty; + + public override IEnumerable? GetActiveTokens() + => BreakpointSpans.GetActiveTokensForCopyConstructor(recordDeclaration); +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/ExplicitAutoPropertyAccessorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/ExplicitAutoPropertyAccessorDeclarationBody.cs new file mode 100644 index 0000000000000..828397a295c04 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/ExplicitAutoPropertyAccessorDeclarationBody.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Auto-property accessor: +/// T P { [|get;|] } +/// T P { [|set;|] } +/// T P { [|init;|] } +/// +internal sealed class ExplicitAutoPropertyAccessorDeclarationBody(AccessorDeclarationSyntax accessor) : PropertyOrIndexerAccessorDeclarationBody +{ + public override SyntaxNode? ExplicitBody + => null; + + public override SyntaxNode? HeaderActiveStatement + => accessor; + + public override TextSpan HeaderActiveStatementSpan + => BreakpointSpans.CreateSpanForAutoPropertyAccessor(accessor); + + public override IEnumerable? GetActiveTokens() + => BreakpointSpans.GetActiveTokensForAutoPropertyAccessor(accessor); + + public override SyntaxNode? MatchRoot + => null; +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/FieldWithInitializerDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/FieldWithInitializerDeclarationBody.cs index 177cc80ffc0d0..08ab1b2e2ae2e 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/FieldWithInitializerDeclarationBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/FieldWithInitializerDeclarationBody.cs @@ -33,7 +33,7 @@ public override SyntaxTree SyntaxTree public override ImmutableArray GetCapturedVariables(SemanticModel model) => model.AnalyzeDataFlow(InitializerExpression)!.Captured; - public override ActiveStatementEnvelope Envelope + public override TextSpan Envelope { get { @@ -57,10 +57,10 @@ public override StateMachineInfo GetStateMachineInfo() public override OneOrMany RootNodes => OneOrMany.Create(InitializerExpression); - public override Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + public override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) => CSharpEditAndContinueAnalyzer.ComputeBodyMatch(InitializerExpression, ((FieldWithInitializerDeclarationBody)newBody).InitializerExpression, knownMatches); - public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) { if (oldStatement == InitializerExpression) { diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorDeclarationBody.cs index c6216176ebd31..92c215dbaf065 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorDeclarationBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorDeclarationBody.cs @@ -2,91 +2,100 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Extensions; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Differencing; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; -internal abstract class InstanceConstructorDeclarationBody(ConstructorDeclarationSyntax constructor) : MemberBody +internal abstract class InstanceConstructorDeclarationBody : MemberBody { - public ConstructorDeclarationSyntax Constructor - => constructor; - - public SyntaxNode Body - => (SyntaxNode?)constructor.Body ?? constructor.ExpressionBody?.Expression!; + public abstract bool HasExplicitInitializer { get; } + public abstract TextSpan InitializerActiveStatementSpan { get; } + public abstract SyntaxNode InitializerActiveStatement { get; } + public abstract SyntaxNode? MatchRoot { get; } /// - /// Active statement node for the implicit or explicit constructor initializer. - /// Implicit initializer: [|public C()|] { } - /// Explicit initializer: public C() : [|base(expr)|] { } + /// Expression or block body. /// - public abstract SyntaxNode InitializerActiveStatement { get; } - - public sealed override SyntaxNode EncompassingAncestor - => Constructor; + public abstract SyntaxNode? ExplicitBody { get; } public sealed override SyntaxTree SyntaxTree - => constructor.SyntaxTree; + => InitializerActiveStatement.SyntaxTree; - public override OneOrMany RootNodes - => OneOrMany.Create(Constructor); + public sealed override StateMachineInfo GetStateMachineInfo() + => StateMachineInfo.None; public sealed override SyntaxNode FindStatementAndPartner(TextSpan span, MemberBody? partnerDeclarationBody, out SyntaxNode? partnerStatement, out int statementPart) { var partnerCtorBody = (InstanceConstructorDeclarationBody?)partnerDeclarationBody; - if (span.Start == InitializerActiveStatement.SpanStart) + if (span == InitializerActiveStatementSpan) { statementPart = AbstractEditAndContinueAnalyzer.DefaultStatementPart; partnerStatement = partnerCtorBody?.InitializerActiveStatement; return InitializerActiveStatement; } - if (Constructor.Initializer?.Span.Contains(span.Start) == true) + if (HasExplicitInitializer && InitializerActiveStatementSpan.Contains(span)) { - // Partner body does not have any non-trivial changes and thus the initializer is also present. - Debug.Assert(partnerCtorBody == null || partnerCtorBody?.Constructor.Initializer != null); + // If present, partner body does not have any non-trivial changes and thus the initializer is also present. + Debug.Assert(partnerCtorBody == null || partnerCtorBody.HasExplicitInitializer); return CSharpEditAndContinueAnalyzer.FindStatementAndPartner( span, - body: Constructor.Initializer, - partnerBody: partnerCtorBody?.Constructor.Initializer, + body: InitializerActiveStatement, + partnerBody: partnerCtorBody?.InitializerActiveStatement, out partnerStatement, out statementPart); } + Debug.Assert(ExplicitBody != null); + + // If present, partner body does not have any non-trivial changes and thus the explicit body is also present. + Debug.Assert(partnerCtorBody == null || partnerCtorBody.ExplicitBody != null); + return CSharpEditAndContinueAnalyzer.FindStatementAndPartner( span, - body: Body, - partnerBody: partnerCtorBody?.Body, + body: ExplicitBody, + partnerBody: partnerCtorBody?.ExplicitBody, out partnerStatement, out statementPart); } - public sealed override StateMachineInfo GetStateMachineInfo() - => new(IsAsync: false, IsIterator: false, HasSuspensionPoints: false); - - public sealed override Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) - => SyntaxComparer.Statement.ComputeMatch(Constructor, ((InstanceConstructorDeclarationBody)newBody).Constructor, knownMatches); - - public sealed override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + public sealed override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) { var newCtorBody = (InstanceConstructorDeclarationBody)newBody; - if (oldStatement is (kind: SyntaxKind.ThisConstructorInitializer or SyntaxKind.BaseConstructorInitializer or SyntaxKind.ConstructorDeclaration)) + if (oldStatement == InitializerActiveStatement) { - newStatement = newCtorBody.Constructor.Initializer ?? (SyntaxNode)newCtorBody.Constructor; + newStatement = newCtorBody.InitializerActiveStatement; return true; } - return CSharpEditAndContinueAnalyzer.TryMatchActiveStatement(Body, newCtorBody.Body, oldStatement, out newStatement); + if (ExplicitBody != null && newCtorBody.ExplicitBody != null && + CSharpEditAndContinueAnalyzer.TryMatchActiveStatement(ExplicitBody, newCtorBody.ExplicitBody, oldStatement, out newStatement)) + { + return true; + } + + if (MatchRoot == null || newCtorBody.MatchRoot == null) + { + // General body mapping is not available, so we can't do better then + // mapping any active statement in this body to the initializer of the other body. + newStatement = newCtorBody.InitializerActiveStatement; + return true; + } + + newStatement = null; + return false; } + + public sealed override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + => MatchRoot is { } oldRoot && ((InstanceConstructorDeclarationBody)newBody).MatchRoot is { } newRoot + ? SyntaxComparer.Statement.ComputeMatch(oldRoot, newRoot, knownMatches) + : null; } diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorDeclarationBody.cs new file mode 100644 index 0000000000000..49d5e2a16ee62 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorDeclarationBody.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +internal abstract class OrdinaryInstanceConstructorDeclarationBody(ConstructorDeclarationSyntax constructor) + : InstanceConstructorDeclarationBody +{ + public ConstructorDeclarationSyntax Constructor + => constructor; + + public SyntaxNode Body + => (SyntaxNode?)constructor.Body ?? constructor.ExpressionBody?.Expression!; + + public sealed override SyntaxNode? ExplicitBody + => Body; + + public sealed override SyntaxNode EncompassingAncestor + => constructor; + + public sealed override SyntaxNode? MatchRoot + => constructor; + + public override OneOrMany RootNodes + => OneOrMany.Create(constructor); +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithExplicitInitializerDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody.cs similarity index 67% rename from src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithExplicitInitializerDeclarationBody.cs rename to src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody.cs index 42fbfa22a3a33..70ad516b152da 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithExplicitInitializerDeclarationBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody.cs @@ -12,20 +12,26 @@ namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; -internal sealed class InstanceConstructorWithExplicitInitializerDeclarationBody(ConstructorDeclarationSyntax constructor) - : InstanceConstructorDeclarationBody(constructor) +internal sealed class OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody(ConstructorDeclarationSyntax constructor) + : OrdinaryInstanceConstructorDeclarationBody(constructor) { private ConstructorInitializerSyntax Initializer => Constructor.Initializer!; + public override bool HasExplicitInitializer + => true; + public override SyntaxNode InitializerActiveStatement => Initializer; + public override TextSpan InitializerActiveStatementSpan + => BreakpointSpans.CreateSpanForExplicitConstructorInitializer(Initializer); + public override ImmutableArray GetCapturedVariables(SemanticModel model) => model.AnalyzeDataFlow(Initializer)!.Captured.AddRange(model.AnalyzeDataFlow(Body).Captured).Distinct(); - public override ActiveStatementEnvelope Envelope - => TextSpan.FromBounds(BreakpointSpans.CreateSpanForExplicitConstructorInitializer(Initializer).Start, Body.Span.End); + public override TextSpan Envelope + => TextSpan.FromBounds(InitializerActiveStatementSpan.Start, Body.Span.End); public override IEnumerable GetActiveTokens() => BreakpointSpans.GetActiveTokensForExplicitConstructorInitializer(Initializer).Concat(Body.DescendantTokens()); diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithImplicitInitializerDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody.cs similarity index 63% rename from src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithImplicitInitializerDeclarationBody.cs rename to src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody.cs index 8c8c1138c9e71..15d10abc92bd8 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/InstanceConstructorWithImplicitInitializerDeclarationBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody.cs @@ -12,17 +12,23 @@ namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; -internal sealed class InstanceConstructorWithImplicitInitializerDeclarationBody(ConstructorDeclarationSyntax constructor) - : InstanceConstructorDeclarationBody(constructor) +internal sealed class OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody(ConstructorDeclarationSyntax constructor) + : OrdinaryInstanceConstructorDeclarationBody(constructor) { public override SyntaxNode InitializerActiveStatement => Constructor; + public override bool HasExplicitInitializer + => false; + + public override TextSpan InitializerActiveStatementSpan + => BreakpointSpans.CreateSpanForImplicitConstructorInitializer(Constructor); + public override ImmutableArray GetCapturedVariables(SemanticModel model) => model.AnalyzeDataFlow(Body).Captured; - public override ActiveStatementEnvelope Envelope - => TextSpan.FromBounds(BreakpointSpans.CreateSpanForImplicitConstructorInitializer(Constructor).Start, Body.Span.End); + public override TextSpan Envelope + => TextSpan.FromBounds(InitializerActiveStatementSpan.Start, Body.Span.End); public override IEnumerable GetActiveTokens() => BreakpointSpans.GetActiveTokensForImplicitConstructorInitializer(Constructor).Concat(Body.DescendantTokens()); diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorDeclarationBody.cs new file mode 100644 index 0000000000000..9dc6f628c8e53 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorDeclarationBody.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Implicit initializer: class [|C(int a, int b)|] : B; +/// Explicit initializer: class C(int a, int b) : [|B(expr)|]; +/// +internal abstract class PrimaryConstructorDeclarationBody(TypeDeclarationSyntax typeDeclaration) + : InstanceConstructorDeclarationBody +{ + public TypeDeclarationSyntax TypeDeclaration + => typeDeclaration; + + public sealed override SyntaxNode? ExplicitBody + => null; + + public sealed override OneOrMany RootNodes + => OneOrMany.Create(InitializerActiveStatement); + + public sealed override TextSpan Envelope + => InitializerActiveStatementSpan; +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithExplicitInitializerDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithExplicitInitializerDeclarationBody.cs new file mode 100644 index 0000000000000..67b8396061ce8 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithExplicitInitializerDeclarationBody.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Breakpoint spans: +/// +/// class C(int a, int b) : [|B(expr)|]; +/// +internal sealed class PrimaryConstructorWithExplicitInitializerDeclarationBody(TypeDeclarationSyntax typeDeclaration) + : PrimaryConstructorDeclarationBody(typeDeclaration) +{ + public PrimaryConstructorBaseTypeSyntax Initializer + => (PrimaryConstructorBaseTypeSyntax)TypeDeclaration.BaseList!.Types[0]; + + public override bool HasExplicitInitializer + => true; + + public override SyntaxNode InitializerActiveStatement + => Initializer; + + public override TextSpan InitializerActiveStatementSpan + => BreakpointSpans.CreateSpanForExplicitPrimaryConstructorInitializer(Initializer); + + public override SyntaxNode? MatchRoot + => Initializer; + + public override IEnumerable? GetActiveTokens() + => BreakpointSpans.GetActiveTokensForExplicitPrimaryConstructorInitializer(Initializer); + + public sealed override SyntaxNode EncompassingAncestor + => Initializer; + + public override ImmutableArray GetCapturedVariables(SemanticModel model) + => model.AnalyzeDataFlow(Initializer)!.Captured; +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithImplicitInitializerDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithImplicitInitializerDeclarationBody.cs new file mode 100644 index 0000000000000..d37e54d2a0ec5 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PrimaryConstructorWithImplicitInitializerDeclarationBody.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Differencing; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Breakpoint spans: +/// +/// class [|C(int a, int b)|] : B; +/// +internal sealed class PrimaryConstructorWithImplicitInitializerDeclarationBody(TypeDeclarationSyntax typeDeclaration) + : PrimaryConstructorDeclarationBody(typeDeclaration) +{ + public ParameterListSyntax ParameterList + => TypeDeclaration.ParameterList!; + + public override bool HasExplicitInitializer + => false; + + public override SyntaxNode InitializerActiveStatement + => ParameterList; + + public override TextSpan InitializerActiveStatementSpan + => BreakpointSpans.CreateSpanForImplicitPrimaryConstructorInitializer(TypeDeclaration); + + public override SyntaxNode? MatchRoot + => null; + + public override IEnumerable? GetActiveTokens() + => BreakpointSpans.GetActiveTokensForImplicitPrimaryConstructorInitializer(TypeDeclaration); + + public sealed override SyntaxNode EncompassingAncestor + => TypeDeclaration; + + public override ImmutableArray GetCapturedVariables(SemanticModel model) + => ImmutableArray.Empty; + + // Active spans of copy-constructor and primary record properties overlap with the primary constructor initializer span, + // but do not belong to the primary constructor body. The only active span that belongs is the initializer span itself. + public override bool IsExcludedActiveStatementSpanWithinEnvelope(TextSpan span) + => span != InitializerActiveStatementSpan; +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorDeclarationBody.cs new file mode 100644 index 0000000000000..9cca540e26d58 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorDeclarationBody.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Differencing; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Property or indexer accessor with explicit body: +/// T P { get => [|expr;|] } +/// T P { set => [|expr;|] } +/// T P { init => [|expr;|] } +/// T P { get { ... } } +/// T P { set { ... } } +/// T P { init { ... } } +/// T this[...] { get => [|expr;|] } +/// T this[...] { set => [|expr;|] } +/// T this[...] { get { ... } } +/// T this[...] { set { ... } } +/// +/// Property or indexer with explicit body: +/// T P => [|expr;|] +/// T this[...] => [|expr;|] +/// +/// Auto-property accessor: +/// T P { [|get;|] } +/// T P { [|set;|] } +/// T P { [|init;|] } +/// +/// Primary record auto-property: +/// record R([|T P|]) +/// +internal abstract class PropertyOrIndexerAccessorDeclarationBody : MemberBody +{ + /// + /// or or . + /// + public abstract SyntaxNode? ExplicitBody { get; } + + /// + /// Active statement outside of . + /// + public abstract SyntaxNode? HeaderActiveStatement { get; } + + public abstract TextSpan HeaderActiveStatementSpan { get; } + + public abstract SyntaxNode? MatchRoot { get; } + + public SyntaxNode RootNode + => ExplicitBody ?? HeaderActiveStatement!; + + public sealed override SyntaxTree SyntaxTree + => RootNode.SyntaxTree; + + public sealed override TextSpan Envelope + => ExplicitBody?.Span ?? HeaderActiveStatementSpan; + + public sealed override OneOrMany RootNodes + => new(RootNode); + + public sealed override SyntaxNode EncompassingAncestor + => RootNode; + + public sealed override StateMachineInfo GetStateMachineInfo() + => StateMachineInfo.None; + + public sealed override ImmutableArray GetCapturedVariables(SemanticModel model) + => (ExplicitBody != null) ? model.AnalyzeDataFlow(ExplicitBody).Captured : ImmutableArray.Empty; + + public sealed override SyntaxNode FindStatementAndPartner(TextSpan span, MemberBody? partnerDeclarationBody, out SyntaxNode? partnerStatement, out int statementPart) + { + if (HeaderActiveStatement != null) + { + Debug.Assert(partnerDeclarationBody is null or PropertyOrIndexerAccessorDeclarationBody { HeaderActiveStatement: not null }); + + statementPart = AbstractEditAndContinueAnalyzer.DefaultStatementPart; + partnerStatement = ((PropertyOrIndexerAccessorDeclarationBody?)partnerDeclarationBody)?.HeaderActiveStatement; + return HeaderActiveStatement; + } + + Debug.Assert(ExplicitBody != null); + Debug.Assert(partnerDeclarationBody is null or PropertyOrIndexerAccessorDeclarationBody { ExplicitBody: not null }); + + return CSharpEditAndContinueAnalyzer.FindStatementAndPartner( + span, + body: ExplicitBody, + partnerBody: ((PropertyOrIndexerAccessorDeclarationBody?)partnerDeclarationBody)?.ExplicitBody, + out partnerStatement, + out statementPart); + } + + public sealed override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + { + var newPropertyBody = (PropertyOrIndexerAccessorDeclarationBody)newBody; + + if (HeaderActiveStatement != null) + { + if (oldStatement != HeaderActiveStatement) + { + newStatement = null; + return false; + } + + if (newPropertyBody.HeaderActiveStatement != null) + { + newStatement = newPropertyBody.HeaderActiveStatement; + return true; + } + + Contract.ThrowIfNull(newPropertyBody.ExplicitBody); + (newStatement, statementPart) = CSharpEditAndContinueAnalyzer.GetFirstBodyActiveStatement(newPropertyBody.ExplicitBody); + return true; + } + + if (newPropertyBody.HeaderActiveStatement != null) + { + newStatement = newPropertyBody.HeaderActiveStatement; + statementPart = AbstractEditAndContinueAnalyzer.DefaultStatementPart; + return true; + } + + Contract.ThrowIfNull(ExplicitBody); + Contract.ThrowIfNull(newPropertyBody.ExplicitBody); + Contract.ThrowIfNull(MatchRoot); + Contract.ThrowIfNull(newPropertyBody.MatchRoot); + + // Statements in explicit bodies will be mapped using a match. + // We need to special case active statements in root nodes though because the root kinds differ (block vs arrow expr) + if (oldStatement == ExplicitBody) + { + newStatement = newPropertyBody.ExplicitBody; + statementPart = AbstractEditAndContinueAnalyzer.DefaultStatementPart; + return true; + } + + newStatement = null; + return false; + } + + public sealed override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + => MatchRoot is { } oldRoot && ((PropertyOrIndexerAccessorDeclarationBody)newBody).MatchRoot is { } newRoot + ? SyntaxComparer.Statement.ComputeMatch(oldRoot, newRoot, knownMatches) + : null; +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody.cs new file mode 100644 index 0000000000000..12eb6a1964905 --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Property accessor with explicit body: +/// T P { get => [|expr;|] } +/// T P { set => [|expr;|] } +/// T P { init => [|expr;|] } +/// T P { get { ... } } +/// T P { set { ... } } +/// T P { init { ... } } +/// T this[...] { get => [|expr;|] } +/// T this[...] { set => [|expr;|] } +/// T this[...] { get { ... } } +/// T this[...] { set { ... } } +/// +internal sealed class PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody(AccessorDeclarationSyntax accessor) : PropertyOrIndexerAccessorDeclarationBody +{ + public SyntaxNode Body + => (SyntaxNode?)accessor.Body ?? accessor.ExpressionBody!.Expression; + + public override SyntaxNode? ExplicitBody + => Body; + + public override SyntaxNode? HeaderActiveStatement + => null; + + public override TextSpan HeaderActiveStatementSpan + => default; + + public override SyntaxNode? MatchRoot + => (SyntaxNode?)accessor.Body ?? accessor.ExpressionBody!; + + public sealed override IEnumerable? GetActiveTokens() + => Body.DescendantTokens(); +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerWithExplicitBodyDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerWithExplicitBodyDeclarationBody.cs new file mode 100644 index 0000000000000..7cd25f007d85f --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/PropertyOrIndexerWithExplicitBodyDeclarationBody.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// Property or with explicit body: +/// T P => [|expr;|] +/// T this[...] => [|expr;|] +/// +internal sealed class PropertyOrIndexerWithExplicitBodyDeclarationBody(BasePropertyDeclarationSyntax propertyOrIndexer) : PropertyOrIndexerAccessorDeclarationBody +{ + public ArrowExpressionClauseSyntax Body + => (propertyOrIndexer is PropertyDeclarationSyntax property) ? property.ExpressionBody! : ((IndexerDeclarationSyntax)propertyOrIndexer).ExpressionBody!; + + public ExpressionSyntax BodyExpression + => Body.Expression; + + public override SyntaxNode? ExplicitBody + => BodyExpression; + + public override SyntaxNode? HeaderActiveStatement + => null; + + public override TextSpan HeaderActiveStatementSpan + => default; + + public override SyntaxNode? MatchRoot + => Body; + + public sealed override IEnumerable? GetActiveTokens() + => BodyExpression.DescendantTokens(); +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/RecordParameterDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/RecordParameterDeclarationBody.cs new file mode 100644 index 0000000000000..4b7491dff06cb --- /dev/null +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/RecordParameterDeclarationBody.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; + +/// +/// record C([Attr] [|in int a|] = 1); +/// +internal sealed class RecordParameterDeclarationBody(ParameterSyntax parameter) : PropertyOrIndexerAccessorDeclarationBody +{ + public override SyntaxNode? ExplicitBody + => null; + + public override SyntaxNode? HeaderActiveStatement + => parameter; + + public override TextSpan HeaderActiveStatementSpan + => BreakpointSpans.CreateSpanForRecordParameter(parameter); + + public override SyntaxNode? MatchRoot + => null; + + public override IEnumerable? GetActiveTokens() + => BreakpointSpans.GetActiveTokensForRecordParameter(parameter); +} diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/SimpleMemberBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/SimpleMemberBody.cs index c8d0a3d7609bb..fb63c08d6c937 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/SimpleMemberBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/SimpleMemberBody.cs @@ -27,9 +27,9 @@ public override StateMachineInfo GetStateMachineInfo() IsIterator: SyntaxUtilities.IsIterator(Node), HasSuspensionPoints: SyntaxUtilities.GetSuspensionPoints(Node).Any()); - public override Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + public override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) => CSharpEditAndContinueAnalyzer.ComputeBodyMatch(Node, ((SimpleMemberBody)newBody).Node, knownMatches); - public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) => CSharpEditAndContinueAnalyzer.TryMatchActiveStatement(Node, ((SimpleMemberBody)newBody).Node, oldStatement, out newStatement); } diff --git a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/TopLevelCodeDeclarationBody.cs b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/TopLevelCodeDeclarationBody.cs index cc2b1534d6fe4..6b7785c8dbae0 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/TopLevelCodeDeclarationBody.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/DeclarationBody/TopLevelCodeDeclarationBody.cs @@ -29,7 +29,7 @@ public override SyntaxTree SyntaxTree public override ImmutableArray GetCapturedVariables(SemanticModel model) => model.AnalyzeDataFlow(((GlobalStatementSyntax)unit.Members[0]).Statement, GlobalStatements.Last().Statement)!.Captured; - public override ActiveStatementEnvelope Envelope + public override TextSpan Envelope => TextSpan.FromBounds(unit.Members[0].SpanStart, GlobalStatements.Last().Span.End); public override SyntaxNode EncompassingAncestor @@ -47,7 +47,7 @@ public override StateMachineInfo GetStateMachineInfo() public override OneOrMany RootNodes => OneOrMany.Create(GlobalStatements.ToImmutableArray()); - public override Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + public override Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches) => CSharpEditAndContinueAnalyzer.ComputeBodyMatch(Unit, ((TopLevelCodeDeclarationBody)newBody).Unit, knownMatches); public override SyntaxNode FindStatementAndPartner(TextSpan span, MemberBody? partnerDeclarationBody, out SyntaxNode? partnerStatement, out int statementPart) @@ -58,7 +58,7 @@ public override SyntaxNode FindStatementAndPartner(TextSpan span, MemberBody? pa out partnerStatement, out statementPart); - public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) + public override bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement) { newStatement = null; return false; diff --git a/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs b/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs index 6a24536983085..00d92ee24c1d2 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/SyntaxUtilities.cs @@ -18,27 +18,36 @@ internal static partial class SyntaxUtilities public static LambdaBody CreateLambdaBody(SyntaxNode node) => new CSharpLambdaBody(node); - public static MemberBody? TryGetDeclarationBody(SyntaxNode node) + public static MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol) => node switch { MethodDeclarationSyntax methodDeclaration => CreateSimpleBody(BlockOrExpression(methodDeclaration.Body, methodDeclaration.ExpressionBody)), ConversionOperatorDeclarationSyntax conversionDeclaration => CreateSimpleBody(BlockOrExpression(conversionDeclaration.Body, conversionDeclaration.ExpressionBody)), OperatorDeclarationSyntax operatorDeclaration => CreateSimpleBody(BlockOrExpression(operatorDeclaration.Body, operatorDeclaration.ExpressionBody)), - AccessorDeclarationSyntax accessorDeclaration => CreateSimpleBody(BlockOrExpression(accessorDeclaration.Body, accessorDeclaration.ExpressionBody)), DestructorDeclarationSyntax destructorDeclaration => CreateSimpleBody(BlockOrExpression(destructorDeclaration.Body, destructorDeclaration.ExpressionBody)), + AccessorDeclarationSyntax accessorDeclaration + => BlockOrExpression(accessorDeclaration.Body, accessorDeclaration.ExpressionBody) != null + ? new PropertyOrIndexerAccessorWithExplicitBodyDeclarationBody(accessorDeclaration) + : new ExplicitAutoPropertyAccessorDeclarationBody(accessorDeclaration), + // We associate the body of expression-bodied property/indexer with the ArrowExpressionClause // since that's the syntax node associated with the getter symbol. + // This approach makes it possible to change the expression body to an explicit getter and vice versa (both are method symbols). + // // The property/indexer itself is considered to not have a body unless the property has an initializer. - ArrowExpressionClauseSyntax { Parent: (kind: SyntaxKind.PropertyDeclaration) or (kind: SyntaxKind.IndexerDeclaration) } arrowClause => CreateSimpleBody(arrowClause.Expression), - PropertyDeclarationSyntax propertyDeclaration => CreateSimpleBody(propertyDeclaration.Initializer?.Value), + ArrowExpressionClauseSyntax { Parent: (kind: SyntaxKind.PropertyDeclaration) or (kind: SyntaxKind.IndexerDeclaration) } arrowExpression + => new PropertyOrIndexerWithExplicitBodyDeclarationBody((BasePropertyDeclarationSyntax)arrowExpression.Parent!), + + PropertyDeclarationSyntax { Initializer: { } propertyInitializer } + => CreateSimpleBody(propertyInitializer.Value), ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.Body != null || constructorDeclaration.ExpressionBody != null => constructorDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) ? CreateSimpleBody(BlockOrExpression(constructorDeclaration.Body, constructorDeclaration.ExpressionBody)) : (constructorDeclaration.Initializer != null) - ? new InstanceConstructorWithExplicitInitializerDeclarationBody(constructorDeclaration) - : new InstanceConstructorWithImplicitInitializerDeclarationBody(constructorDeclaration), + ? new OrdinaryInstanceConstructorWithExplicitInitializerDeclarationBody(constructorDeclaration) + : new OrdinaryInstanceConstructorWithImplicitInitializerDeclarationBody(constructorDeclaration), CompilationUnitSyntax unit when unit.ContainsGlobalStatements() => new TopLevelCodeDeclarationBody(unit), @@ -47,6 +56,19 @@ CompilationUnitSyntax unit when unit.ContainsGlobalStatements() when !fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword) => new FieldWithInitializerDeclarationBody(variableDeclarator), + ParameterListSyntax { Parent: TypeDeclarationSyntax typeDeclaration } + => typeDeclaration is { BaseList.Types: [PrimaryConstructorBaseTypeSyntax { }, ..] } + ? new PrimaryConstructorWithExplicitInitializerDeclarationBody(typeDeclaration) + : new PrimaryConstructorWithImplicitInitializerDeclarationBody(typeDeclaration), + + // Record type itself does not have a body, create body only when the declaration represents copy constructor: + RecordDeclarationSyntax recordDeclarationSyntax when symbol is not INamedTypeSymbol + => new CopyConstructorDeclarationBody(recordDeclarationSyntax), + + // Parameters themselves do not have a body, the synthesized property accessors do: + ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } parameterSyntax when symbol is not IParameterSymbol + => new RecordParameterDeclarationBody(parameterSyntax), + _ => null }; diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs index e6815140c30a2..b0cc1f06e7f7a 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs @@ -80,80 +80,63 @@ private static TraceLog Log internal abstract bool ExperimentalFeaturesEnabled(SyntaxTree tree); /// - /// Finds member declaration node(s) containing given . - /// Specified may be either a node of the declaration body or an active node that belongs to the declaration. + /// Finds member declaration node(s) containing given , if any. /// + /// Span used to disambiguate member declarations if there are multiple applicable ones based on . /// /// The implementation has to decide what kinds of nodes in top-level match relationship represent a declaration. /// Every member declaration must be represented by exactly one node, but not all nodes have to represent a declaration. /// - /// Note that in some cases the set of nodes of the declaration body may differ from the set of active nodes that - /// belong to the declaration. For example, in Dim a, b As New T the sets for member a are - /// { New, T } and { a }, respectively. - /// - /// May return multiple declarations if the specified belongs to multiple declarations, + /// May return multiple declarations if the specified belongs to bodies of multiple declarations, /// such as in VB Dim a, b As New T case when is e.g. T. /// - internal abstract bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, out OneOrMany declarations); + internal abstract bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany declarations); /// - /// If the specified node represents a member declaration returns an object that represents its body. + /// If the specified represents a member declaration returns an object that represents its body. /// - /// A node representing a declaration or a top-level edit node. - /// + /// + /// If specified then the returned body must belong to this symbol. + /// + /// node itself may represent a that doesn't belong to the . + /// E.g. a record copy-constructor declaration is represented by the record type declaration node, + /// but this node also represents the record symbol itself. + /// /// /// Null for nodes that don't represent declarations. /// - internal abstract MemberBody? TryGetDeclarationBody(SyntaxNode node); + internal abstract MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol); /// /// True if the specified node shares body with another declaration. /// - internal abstract bool IsDeclarationWithSharedBody(SyntaxNode declaration); - - /// - /// Returns an envelope that contains all possible breakpoint spans of the body of the - /// and no breakpoint spans that do not belong to the body. - /// - /// Returns default if the declaration does not have any breakpoint spans. - /// - internal ActiveStatementEnvelope GetActiveSpanEnvelope(SyntaxNode declaration) - => TryGetDeclarationBody(declaration)?.Envelope ?? default; - - /// - /// Returns an ancestor that encompasses all active and statement level - /// nodes that belong to the member represented by . - /// - protected SyntaxNode? GetEncompassingAncestor(SyntaxNode? bodyOrMatchRoot) - { - if (bodyOrMatchRoot == null) - { - return null; - } - - var root = GetEncompassingAncestorImpl(bodyOrMatchRoot); - Debug.Assert(root.Span.Contains(bodyOrMatchRoot.Span)); - return root; - } - - protected abstract SyntaxNode GetEncompassingAncestorImpl(SyntaxNode bodyOrMatchRoot); + internal abstract bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member); /// /// Returns a node that represents a body of a lambda containing specified , /// or null if the node isn't contained in a lambda. If a node is returned it must uniquely represent the lambda, /// i.e. be no two distinct nodes may represent the same lambda. /// - protected abstract LambdaBody? FindEnclosingLambdaBody(SyntaxNode root, SyntaxNode node); + protected abstract LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node); protected abstract Match ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit); protected abstract BidirectionalMap? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration); protected abstract IEnumerable GetSyntaxSequenceEdits(ImmutableArray oldNodes, ImmutableArray newNodes); - protected abstract bool TryGetEnclosingBreakpointSpan(SyntaxNode root, int position, out TextSpan span); + protected abstract bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span); /// /// Get the active span that corresponds to specified node (or its part). /// + /// + /// In case there are multiple breakpoint spans starting at the of the , + /// can be used to disambiguate between them. + /// The inner-most available span whose length is at least is returned. + /// + /// + /// might have multiple active statement span. is used to identify the + /// specific part. + /// /// /// True if the node has an active span associated with it, false otherwise. /// @@ -342,13 +325,22 @@ protected virtual string GetSuspensionPointDisplayName(SyntaxNode node, EditKind protected abstract string LineDirectiveKeyword { get; } protected abstract ushort LineDirectiveSyntaxKind { get; } protected abstract SymbolDisplayFormat ErrorDisplayFormat { get; } - protected abstract List GetExceptionHandlingAncestors(SyntaxNode node, bool isNonLeaf); + protected abstract List GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf); protected abstract TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren); internal abstract void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, Match match, Edit edit, Dictionary editMap); internal abstract void ReportEnclosingExceptionHandlingRudeEdits(ArrayBuilder diagnostics, IEnumerable> exceptionHandlingEdits, SyntaxNode oldStatement, TextSpan newStatementSpan); - internal abstract void ReportOtherRudeEditsAroundActiveStatement(ArrayBuilder diagnostics, Match match, SyntaxNode oldStatement, SyntaxNode newStatement, bool isNonLeaf); internal abstract void ReportMemberOrLambdaBodyUpdateRudeEditsImpl(ArrayBuilder diagnostics, SyntaxNode newDeclaration, DeclarationBody newBody); + + internal abstract void ReportOtherRudeEditsAroundActiveStatement( + ArrayBuilder diagnostics, + IReadOnlyDictionary forwardMap, + SyntaxNode oldActiveStatement, + DeclarationBody oldBody, + SyntaxNode newActiveStatement, + DeclarationBody newBody, + bool isNonLeaf); + internal abstract void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType); internal abstract void ReportStateMachineSuspensionPointRudeEdits(ArrayBuilder diagnostics, SyntaxNode oldNode, SyntaxNode newNode); @@ -374,11 +366,6 @@ internal bool ContainsLambda(MemberBody body) return body.RootNodes.Any(static (root, isLambda) => root.DescendantNodesAndSelf().Any(isLambda), isLambda); } - /// - /// Returns true if the parameters of the symbol are lifted into a scope that is different from the symbol's body. - /// - internal abstract bool HasParameterClosureScope(ISymbol member); - /// /// Returns all lambda bodies of a node representing a lambda, /// or false if the node doesn't represent a lambda. @@ -399,16 +386,6 @@ internal bool ContainsLambda(MemberBody body) /// internal abstract SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node); - /// - /// Returns the declaration of - /// - a property, indexer or event declaration whose accessor is the specified , - /// - a method, an indexer, a type (delegate), or primary constructor parameter list if the is a parameter, - /// - a method or an type if the is a type parameter. - /// - internal abstract bool TryGetAssociatedMemberDeclaration(SyntaxNode node, EditKind editKind, [NotNullWhen(true)] out SyntaxNode? declaration); - - internal abstract bool HasBackingField(SyntaxNode propertyDeclaration); - /// /// Return true if the declaration is a field/property declaration with an initializer. /// Shall return false for enum members. @@ -733,15 +710,15 @@ private void AnalyzeUnchangedActiveMemberBodies( var node = TryGetNode(topMatch.OldRoot, oldStatementSpan.Start); // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). - if (node != null && TryFindMemberDeclaration(topMatch.OldRoot, node, out var oldMemberDeclarations)) + if (node != null && TryFindMemberDeclaration(topMatch.OldRoot, node, oldStatementSpan, out var oldMemberDeclarations)) { foreach (var oldMember in oldMemberDeclarations) { var hasPartner = topMatch.TryGetNewNode(oldMember, out var newMember); Contract.ThrowIfFalse(hasPartner); - var oldBody = TryGetDeclarationBody(oldMember); - var newBody = TryGetDeclarationBody(newMember); + var oldBody = TryGetDeclarationBody(oldMember, symbol: null); + var newBody = TryGetDeclarationBody(newMember, symbol: null); // Guard against invalid active statement spans (in case PDB was somehow out of sync with the source). if (oldBody == null || newBody == null) @@ -755,12 +732,12 @@ private void AnalyzeUnchangedActiveMemberBodies( // We seed the method body matching algorithm with tracking spans (unless they were deleted) // to get precise matching. - if (TryGetTrackedStatement(newActiveStatementSpans, i, newText, newMember, newBody, out var trackedStatement, out var trackedStatementPart)) + if (TryGetTrackedStatement(newActiveStatementSpans, i, newText, newBody, out var trackedStatement, out var trackedStatementPart)) { // Adjust for active statements that cover more than the old member span. // For example, C# variable declarators that represent field initializers: // [|public int <>;|] - var adjustedOldStatementStart = oldMember.FullSpan.Contains(oldStatementSpan.Start) ? oldStatementSpan.Start : oldMember.SpanStart; + var adjustedOldStatementStart = oldBody.ContainsActiveStatementSpan(oldStatementSpan) ? oldStatementSpan.Start : oldBody.Envelope.Start; // The tracking span might have been moved outside of lambda. // It is not an error to move the statement - we just ignore it. @@ -782,7 +759,7 @@ private void AnalyzeUnchangedActiveMemberBodies( if (diagnostics.Count == 0) { - var ancestors = GetExceptionHandlingAncestors(newStatement, oldActiveStatements[i].Statement.IsNonLeaf); + var ancestors = GetExceptionHandlingAncestors(newStatement, newBody.EncompassingAncestor, oldActiveStatements[i].Statement.IsNonLeaf); newExceptionRegions[i] = GetExceptionRegions(ancestors, newStatement.SyntaxTree, cancellationToken).Spans; } @@ -827,7 +804,7 @@ internal readonly struct LambdaInfo public readonly List? ActiveNodeIndices; // both fields are non-null for a matching lambda (lambda that exists in both old and new document): - public readonly Match? Match; + public readonly BidirectionalMap? Match; public readonly LambdaBody? NewBody; public LambdaInfo(List activeNodeIndices) @@ -835,7 +812,7 @@ public LambdaInfo(List activeNodeIndices) { } - private LambdaInfo(List? activeNodeIndices, Match? match, LambdaBody? newLambdaBody) + private LambdaInfo(List? activeNodeIndices, BidirectionalMap? match, LambdaBody? newLambdaBody) { ActiveNodeIndices = activeNodeIndices; Match = match; @@ -845,15 +822,15 @@ private LambdaInfo(List? activeNodeIndices, Match? match, Lambd public bool HasActiveStatement => ActiveNodeIndices != null; - public LambdaInfo WithMatch(Match match, LambdaBody newLambdaBody) + public LambdaInfo WithMatch(BidirectionalMap match, LambdaBody newLambdaBody) => new(ActiveNodeIndices, match, newLambdaBody); } private void AnalyzeChangedMemberBody( SyntaxNode oldDeclaration, SyntaxNode newDeclaration, - MemberBody oldBody, - MemberBody? newBody, + MemberBody oldMemberBody, + MemberBody? newMemberBody, SemanticModel oldModel, SemanticModel newModel, ISymbol oldMember, @@ -875,9 +852,9 @@ private void AnalyzeChangedMemberBody( syntaxMap = null; - var activeStatementIndices = GetOverlappingActiveStatements(oldBody, oldActiveStatements); + var activeStatementIndices = oldMemberBody.GetOverlappingActiveStatements(oldActiveStatements); - if (newBody == null) + if (newMemberBody == null) { // The body has been deleted. var newSpan = FindClosestActiveSpan(newDeclaration, DefaultStatementPart); @@ -889,7 +866,7 @@ private void AnalyzeChangedMemberBody( // This may only happen when two or more member declarations share the same body (VB AsNew clause). if (newActiveStatements[activeStatementIndex] != null) { - Debug.Assert(IsDeclarationWithSharedBody(newDeclaration)); + Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); continue; } @@ -902,7 +879,7 @@ private void AnalyzeChangedMemberBody( try { - _testFaultInjector?.Invoke(newBody.RootNodes.First()); + _testFaultInjector?.Invoke(newMemberBody.RootNodes.First()); // Populated with active lambdas and matched lambdas. // Unmatched non-active lambdas are not included. @@ -915,10 +892,10 @@ private void AnalyzeChangedMemberBody( { var oldStatementSpan = oldActiveStatements[activeStatementIndex].UnmappedSpan; - var oldStatementSyntax = oldBody.FindStatement(oldStatementSpan, out var statementPart); + var oldStatementSyntax = oldMemberBody.FindStatement(oldStatementSpan, out var statementPart); Contract.ThrowIfNull(oldStatementSyntax); - var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldBody.EncompassingAncestor, oldStatementSyntax); + var oldEnclosingLambdaBody = FindEnclosingLambdaBody(oldMemberBody.EncompassingAncestor, oldStatementSyntax); if (oldEnclosingLambdaBody != null) { lazyActiveOrMatchedLambdas ??= new Dictionary(); @@ -934,9 +911,9 @@ private void AnalyzeChangedMemberBody( SyntaxNode? trackedNode = null; - if (TryGetTrackedStatement(newActiveStatementSpans, activeStatementIndex, newText, newDeclaration, newBody, out var newStatementSyntax, out var _)) + if (TryGetTrackedStatement(newActiveStatementSpans, activeStatementIndex, newText, newMemberBody, out var newStatementSyntax, out var _)) { - var newEnclosingLambdaBody = FindEnclosingLambdaBody(newBody.EncompassingAncestor, newStatementSyntax); + var newEnclosingLambdaBody = FindEnclosingLambdaBody(newMemberBody.EncompassingAncestor, newStatementSyntax); // The tracking span might have been moved outside of the lambda span. // It is not an error to move the statement - we just ignore it. @@ -952,22 +929,23 @@ private void AnalyzeChangedMemberBody( var activeNodesInBody = activeNodes.Where(n => n.EnclosingLambdaBody == null).ToArray(); - var bodyMatch = ComputeMatch(oldBody, newBody, activeNodesInBody); - var map = ComputeMap(bodyMatch, activeNodes, ref lazyActiveOrMatchedLambdas); + var bodyMap = ComputeMatch(oldMemberBody, newMemberBody, activeNodesInBody); + var map = IncludeLambdaBodyMaps(bodyMap, activeNodes, ref lazyActiveOrMatchedLambdas); - var oldStateMachineInfo = oldBody.GetStateMachineInfo(); - var newStateMachineInfo = newBody.GetStateMachineInfo(); - ReportStateMachineBodyUpdateRudeEdits(bodyMatch, oldStateMachineInfo, newDeclaration, newStateMachineInfo, hasActiveStatement: activeNodesInBody.Length != 0, diagnostics); + var oldStateMachineInfo = oldMemberBody.GetStateMachineInfo(); + var newStateMachineInfo = newMemberBody.GetStateMachineInfo(); + ReportStateMachineBodyUpdateRudeEdits(bodyMap, oldStateMachineInfo, newDeclaration, newStateMachineInfo, hasActiveStatement: activeNodesInBody.Length != 0, diagnostics); ReportMemberOrLambdaBodyUpdateRudeEdits( diagnostics, oldModel, oldDeclaration, oldMember, + oldMemberBody, newDeclaration, - newBody, + newMemberBody, newMember, - bodyMatch, + newMemberBody, capabilities, oldStateMachineInfo, newStateMachineInfo); @@ -975,13 +953,12 @@ private void AnalyzeChangedMemberBody( ReportLambdaAndClosureRudeEdits( oldModel, oldMember, - oldBody, + oldMemberBody, oldDeclaration, newModel, newMember, - newBody, + newMemberBody, newDeclaration, - bodyMatch, lazyActiveOrMatchedLambdas, map, capabilities, @@ -1013,7 +990,6 @@ private void AnalyzeChangedMemberBody( foreach (var activeNode in activeNodes) { var activeStatementIndex = activeNode.ActiveStatementIndex; - var hasMatching = false; var isNonLeaf = oldActiveStatements[activeStatementIndex].Statement.IsNonLeaf; var isPartiallyExecuted = (oldActiveStatements[activeStatementIndex].Statement.Flags & ActiveStatementFlags.PartiallyExecuted) != 0; var statementPart = activeNode.StatementPart; @@ -1022,45 +998,54 @@ private void AnalyzeChangedMemberBody( newExceptionRegions[activeStatementIndex] = ImmutableArray.Empty; - TextSpan newSpan; - SyntaxNode? newStatementSyntax; - Match? match; - + BidirectionalMap? match; + DeclarationBody oldBody; + DeclarationBody? newBody; if (oldEnclosingLambdaBody == null) { - match = bodyMatch; - - hasMatching = oldBody.TryMatchActiveStatement(newBody, oldStatementSyntax, statementPart, out newStatementSyntax) || - match.TryGetNewNode(oldStatementSyntax, out newStatementSyntax); + match = bodyMap; + oldBody = oldMemberBody; + newBody = newMemberBody; } else { - RoslynDebug.Assert(lazyActiveOrMatchedLambdas != null); + Debug.Assert(lazyActiveOrMatchedLambdas != null); - var oldLambdaInfo = lazyActiveOrMatchedLambdas[oldEnclosingLambdaBody]; - var newEnclosingLambdaBody = oldLambdaInfo.NewBody; - match = oldLambdaInfo.Match; + var matchingLambdaInfo = lazyActiveOrMatchedLambdas[oldEnclosingLambdaBody]; + match = matchingLambdaInfo.Match; + oldBody = oldEnclosingLambdaBody; + newBody = matchingLambdaInfo.NewBody; + } - if (match != null) - { - RoslynDebug.Assert(newEnclosingLambdaBody != null); // matching lambda has body + bool hasMatching; + SyntaxNode? newStatementSyntax; + if (match != null) + { + Debug.Assert(newBody != null); - hasMatching = oldEnclosingLambdaBody.TryMatchActiveStatement(newEnclosingLambdaBody, oldStatementSyntax, statementPart, out newStatementSyntax) || - match.TryGetNewNode(oldStatementSyntax, out newStatementSyntax); - } - else + hasMatching = oldBody.TryMatchActiveStatement(newBody, oldStatementSyntax, ref statementPart, out newStatementSyntax); + if (!hasMatching) { - // Lambda match is null if lambdas can't be matched, - // in such case we won't have active statement matched either. - hasMatching = false; - newStatementSyntax = null; + // If the body has an empty mapping then all active statements in the body must be mapped by TryMatchActiveStatement. + Debug.Assert(!match.Value.Forward.IsEmpty()); + + hasMatching = match.Value.Forward.TryGetValue(oldStatementSyntax, out newStatementSyntax); } } + else + { + // Lambda match is null if lambdas can't be matched, + // in such case we won't have active statement matched either. + hasMatching = false; + newStatementSyntax = null; + } + TextSpan newSpan; if (hasMatching) { - RoslynDebug.Assert(newStatementSyntax != null); - RoslynDebug.Assert(match != null); + Debug.Assert(newStatementSyntax != null); + Debug.Assert(match != null); + Debug.Assert(newBody != null); // The matching node doesn't produce sequence points. // E.g. "const" keyword is inserted into a local variable declaration with an initializer. @@ -1073,14 +1058,21 @@ private void AnalyzeChangedMemberBody( } // other statements around active statement: - ReportOtherRudeEditsAroundActiveStatement(diagnostics, match, oldStatementSyntax, newStatementSyntax, isNonLeaf); + ReportOtherRudeEditsAroundActiveStatement( + diagnostics, + match.Value.Reverse, + oldStatementSyntax, + oldBody, + newStatementSyntax, + newBody, + isNonLeaf); } else if (match == null) { - RoslynDebug.Assert(oldEnclosingLambdaBody != null); - RoslynDebug.Assert(lazyActiveOrMatchedLambdas != null); + Debug.Assert(oldEnclosingLambdaBody != null); + Debug.Assert(lazyActiveOrMatchedLambdas != null); - newSpan = GetDeletedNodeDiagnosticSpan(oldEnclosingLambdaBody, bodyMatch, lazyActiveOrMatchedLambdas); + newSpan = GetDeletedNodeDiagnosticSpan(oldEnclosingLambdaBody, oldMemberBody.EncompassingAncestor, bodyMap.Forward, lazyActiveOrMatchedLambdas); // Lambda containing the active statement can't be found in the new source. var oldLambda = oldEnclosingLambdaBody.GetLambda(); @@ -1089,14 +1081,14 @@ private void AnalyzeChangedMemberBody( } else { - newSpan = GetDeletedNodeActiveSpan(match.Matches, oldStatementSyntax); + newSpan = GetDeletedNodeActiveSpan(match.Value.Forward, oldStatementSyntax); if (isNonLeaf || isPartiallyExecuted) { // rude edit: internal active statement deleted diagnostics.Add( new RudeEditDiagnostic(isNonLeaf ? RudeEditKind.DeleteActiveStatement : RudeEditKind.PartiallyExecutedActiveStatementDelete, - GetDeletedNodeDiagnosticSpan(match.Matches, oldStatementSyntax), + GetDeletedNodeDiagnosticSpan(match.Value.Forward, oldStatementSyntax), arguments: new[] { FeaturesResources.code })); } } @@ -1104,13 +1096,17 @@ private void AnalyzeChangedMemberBody( // If there was a lambda, but we couldn't match its body to the new tree, then the lambda was // removed, so we don't need to check it for active statements. If there wasn't a lambda then // match here will be the same as bodyMatch. - if (match is not null) + if (match != null) { + Debug.Assert(newBody != null); + // exception handling around the statement: CalculateExceptionRegionsAroundActiveStatement( - match, + match.Value.Forward, oldStatementSyntax, + oldBody.EncompassingAncestor, newStatementSyntax, + newBody.EncompassingAncestor, newSpan, activeStatementIndex, isNonLeaf, @@ -1121,10 +1117,16 @@ private void AnalyzeChangedMemberBody( // We have already calculated the new location of this active statement when analyzing another member declaration. // This may only happen when two or more member declarations share the same body (VB AsNew clause). - Debug.Assert(IsDeclarationWithSharedBody(newDeclaration) || newActiveStatements[activeStatementIndex] == null); Debug.Assert(newSpan != default); - newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], newDeclaration.SyntaxTree, newSpan, diagnostics, cancellationToken); + if (newActiveStatements[activeStatementIndex] == null) + { + newActiveStatements[activeStatementIndex] = GetActiveStatementWithSpan(oldActiveStatements[activeStatementIndex], newDeclaration.SyntaxTree, newSpan, diagnostics, cancellationToken); + } + else + { + Debug.Assert(IsDeclarationWithSharedBody(oldDeclaration, oldMember)); + } } } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) @@ -1172,7 +1174,7 @@ private void AnalyzeChangedMemberBody( } } - private bool TryGetTrackedStatement(ImmutableArray activeStatementSpans, int index, SourceText text, SyntaxNode declaration, MemberBody body, [NotNullWhen(true)] out SyntaxNode? trackedStatement, out int trackedStatementPart) + private static bool TryGetTrackedStatement(ImmutableArray activeStatementSpans, int index, SourceText text, MemberBody body, [NotNullWhen(true)] out SyntaxNode? trackedStatement, out int trackedStatementPart) { trackedStatement = null; trackedStatementPart = -1; @@ -1195,8 +1197,7 @@ private bool TryGetTrackedStatement(ImmutableArray activeState // It is not an error to move the statement - we just ignore it. // Consider: Instead of checking here, explicitly handle all cases when active statements can be outside of the body in FindStatement and // return false if the requested span is outside of the active envelope. - var (envelope, hole) = GetActiveSpanEnvelope(declaration); - if (!envelope.Contains(trackedSpan) || hole.Contains(trackedSpan)) + if (!body.Envelope.Contains(trackedSpan)) { return false; } @@ -1222,9 +1223,11 @@ private ActiveStatement GetActiveStatementWithSpan(UnmappedActiveStatement oldSt } private void CalculateExceptionRegionsAroundActiveStatement( - Match bodyMatch, + IReadOnlyDictionary forwardMap, SyntaxNode oldStatementSyntax, + SyntaxNode oldEncompassingAncestor, SyntaxNode? newStatementSyntax, + SyntaxNode newEncompassingAncestor, TextSpan newStatementSyntaxSpan, int ordinal, bool isNonLeaf, @@ -1234,22 +1237,22 @@ private void CalculateExceptionRegionsAroundActiveStatement( { if (newStatementSyntax == null) { - if (!bodyMatch.NewRoot.Span.Contains(newStatementSyntaxSpan.Start)) + if (!newEncompassingAncestor.Span.Contains(newStatementSyntaxSpan.Start)) { return; } - newStatementSyntax = bodyMatch.NewRoot.FindToken(newStatementSyntaxSpan.Start).Parent; + newStatementSyntax = newEncompassingAncestor.FindToken(newStatementSyntaxSpan.Start).Parent; Contract.ThrowIfNull(newStatementSyntax); } - var oldAncestors = GetExceptionHandlingAncestors(oldStatementSyntax, isNonLeaf); - var newAncestors = GetExceptionHandlingAncestors(newStatementSyntax, isNonLeaf); + var oldAncestors = GetExceptionHandlingAncestors(oldStatementSyntax, oldEncompassingAncestor, isNonLeaf); + var newAncestors = GetExceptionHandlingAncestors(newStatementSyntax, newEncompassingAncestor, isNonLeaf); if (oldAncestors.Count > 0 || newAncestors.Count > 0) { - var edits = bodyMatch.GetSequenceEdits(oldAncestors, newAncestors); + var edits = new MapBasedLongestCommonSubsequence(forwardMap).GetEdits(oldAncestors, newAncestors); ReportEnclosingExceptionHandlingRudeEdits(diagnostics, edits, oldStatementSyntax, newStatementSyntaxSpan); // Exception regions are not needed in presence of errors. @@ -1264,35 +1267,35 @@ private void CalculateExceptionRegionsAroundActiveStatement( /// /// Calculates a syntax map of the entire method body including all lambda bodies it contains. /// - private BidirectionalMap ComputeMap( - Match memberBodyMatch, + private BidirectionalMap IncludeLambdaBodyMaps( + BidirectionalMap memberBodyMap, ArrayBuilder memberBodyActiveNodes, ref Dictionary? lazyActiveOrMatchedLambdas) { - ArrayBuilder>? lambdaBodyMatches = null; + ArrayBuilder<(BidirectionalMap map, SyntaxNode? oldLambda)>? lambdaBodyMatches = null; + SyntaxNode? currentOldLambda = null; var currentLambdaBodyMatch = -1; - var currentBodyMatch = memberBodyMatch; + var currentBodyMatch = memberBodyMap; while (true) { - foreach (var (oldNode, newNode) in currentBodyMatch.Matches) + foreach (var (oldNode, newNode) in currentBodyMatch.Forward) { - // Skip root, only enumerate body matches. - if (oldNode == currentBodyMatch.OldRoot) + // the node is a declaration of the current lambda (we already processed it): + if (oldNode == currentOldLambda) { - Debug.Assert(newNode == currentBodyMatch.NewRoot); continue; } if (TryGetLambdaBodies(oldNode, out var oldLambdaBody1, out var oldLambdaBody2)) { - lambdaBodyMatches ??= ArrayBuilder>.GetInstance(); + lambdaBodyMatches ??= ArrayBuilder<(BidirectionalMap, SyntaxNode?)>.GetInstance(); lazyActiveOrMatchedLambdas ??= new Dictionary(); var newLambdaBody1 = oldLambdaBody1.TryGetPartnerLambdaBody(newNode); if (newLambdaBody1 != null) { - lambdaBodyMatches.Add(ComputeMatch(oldLambdaBody1, newLambdaBody1, memberBodyActiveNodes, lazyActiveOrMatchedLambdas)); + lambdaBodyMatches.Add((ComputeMatch(oldLambdaBody1, newLambdaBody1, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); } if (oldLambdaBody2 != null) @@ -1300,7 +1303,7 @@ private BidirectionalMap ComputeMap( var newLambdaBody2 = oldLambdaBody2.TryGetPartnerLambdaBody(newNode); if (newLambdaBody2 != null) { - lambdaBodyMatches.Add(ComputeMatch(oldLambdaBody2, newLambdaBody2, memberBodyActiveNodes, lazyActiveOrMatchedLambdas)); + lambdaBodyMatches.Add((ComputeMatch(oldLambdaBody2, newLambdaBody2, memberBodyActiveNodes, lazyActiveOrMatchedLambdas), oldNode)); } } } @@ -1312,24 +1315,24 @@ private BidirectionalMap ComputeMap( break; } - currentBodyMatch = lambdaBodyMatches[currentLambdaBodyMatch]; + (currentBodyMatch, currentOldLambda) = lambdaBodyMatches[currentLambdaBodyMatch]; } if (lambdaBodyMatches == null) { - return BidirectionalMap.FromMatch(memberBodyMatch); + return memberBodyMap; } var map = new Dictionary(); var reverseMap = new Dictionary(); // include all matches, including the root: - map.AddRange(memberBodyMatch.Matches); - reverseMap.AddRange(memberBodyMatch.ReverseMatches); + map.AddRange(memberBodyMap.Forward); + reverseMap.AddRange(memberBodyMap.Reverse); - foreach (var lambdaBodyMatch in lambdaBodyMatches) + foreach (var (lambdaBodyMatch, _) in lambdaBodyMatches) { - foreach (var (oldNode, newNode) in lambdaBodyMatch.Matches) + foreach (var (oldNode, newNode) in lambdaBodyMatch.Forward) { if (!map.ContainsKey(oldNode)) { @@ -1344,7 +1347,7 @@ private BidirectionalMap ComputeMap( return new BidirectionalMap(map, reverseMap); } - private static Match ComputeMatch( + private static BidirectionalMap ComputeMatch( LambdaBody oldLambdaBody, LambdaBody newLambdaBody, IReadOnlyList memberBodyActiveNodes, @@ -1373,11 +1376,11 @@ private static Match ComputeMatch( /// /// Called for a member body and for bodies of all lambdas and local functions (recursively) found in the member body. /// - private static Match ComputeMatch(DeclarationBody oldBody, DeclarationBody newBody, IEnumerable activeNodes) + private static BidirectionalMap ComputeMatch(DeclarationBody oldBody, DeclarationBody newBody, IEnumerable activeNodes) => oldBody.ComputeMatch(newBody, knownMatches: GetMatchingActiveNodes(activeNodes)); private void ReportStateMachineBodyUpdateRudeEdits( - Match match, + BidirectionalMap match, StateMachineInfo oldStateMachineInfo, SyntaxNode newDeclaration, StateMachineInfo newStateMachineInfo, @@ -1393,7 +1396,7 @@ private void ReportStateMachineBodyUpdateRudeEdits( if (oldStateMachineInfo.HasSuspensionPoints) { - foreach (var (oldNode, newNode) in match.Matches) + foreach (var (oldNode, newNode) in match.Forward) { ReportStateMachineSuspensionPointRudeEdits(diagnostics, oldNode, newNode); } @@ -1465,11 +1468,13 @@ internal virtual void ReportStateMachineSuspensionPointInsertedRudeEdit(ArrayBui return lazyKnownMatches; } - public ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode syntaxRoot, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken) + public ActiveStatementExceptionRegions GetExceptionRegions(SyntaxNode root, TextSpan unmappedActiveStatementSpan, bool isNonLeaf, CancellationToken cancellationToken) { - var token = syntaxRoot.FindToken(unmappedActiveStatementSpan.Start); - var ancestors = GetExceptionHandlingAncestors(token.Parent!, isNonLeaf); - return GetExceptionRegions(ancestors, syntaxRoot.SyntaxTree, cancellationToken); + var node = root.FindToken(unmappedActiveStatementSpan.Start).Parent; + Debug.Assert(node != null); + + var ancestors = GetExceptionHandlingAncestors(node, root, isNonLeaf); + return GetExceptionRegions(ancestors, root.SyntaxTree, cancellationToken); } private ActiveStatementExceptionRegions GetExceptionRegions(List exceptionHandlingAncestors, SyntaxTree tree, CancellationToken cancellationToken) @@ -1503,21 +1508,25 @@ private ActiveStatementExceptionRegions GetExceptionRegions(List exc return new ActiveStatementExceptionRegions(result.ToImmutable(), isCovered); } - private TextSpan GetDeletedNodeDiagnosticSpan(LambdaBody deletedLambdaBody, Match match, Dictionary lambdaInfos) + private TextSpan GetDeletedNodeDiagnosticSpan( + LambdaBody deletedLambdaBody, + SyntaxNode oldEncompassingAncestor, + IReadOnlyDictionary forwardMap, + Dictionary lambdaInfos) { var oldLambdaBody = deletedLambdaBody; while (true) { var oldLambda = oldLambdaBody.GetLambda(); - var oldParentLambdaBody = FindEnclosingLambdaBody(match.OldRoot, oldLambda); + var oldParentLambdaBody = FindEnclosingLambdaBody(oldEncompassingAncestor, oldLambda); if (oldParentLambdaBody == null) { - return GetDeletedNodeDiagnosticSpan(match.Matches, oldLambda); + return GetDeletedNodeDiagnosticSpan(forwardMap, oldLambda); } if (lambdaInfos.TryGetValue(oldParentLambdaBody, out var lambdaInfo) && lambdaInfo.Match != null) { - return GetDeletedNodeDiagnosticSpan(lambdaInfo.Match.Matches, oldLambda); + return GetDeletedNodeDiagnosticSpan(lambdaInfo.Match.Value.Forward, oldLambda); } oldLambdaBody = oldParentLambdaBody; @@ -1619,25 +1628,6 @@ private static bool TryGetMatchingAncestor(IReadOnlyDictionary GetOverlappingActiveStatements(MemberBody body, ImmutableArray statements) - { - var (span, hole) = body.Envelope; - - var range = ActiveStatementsMap.GetSpansStartingInSpan( - span.Start, - span.End, - statements, - startPositionComparer: (x, y) => x.UnmappedSpan.Start.CompareTo(y)); - - for (var i = range.Start.Value; i < range.End.Value; i++) - { - if (!hole.Contains(statements[i].UnmappedSpan.Start)) - { - yield return i; - } - } - } - protected static bool HasParentEdit(IReadOnlyDictionary editMap, Edit edit) { SyntaxNode node; @@ -1717,31 +1707,33 @@ protected void AddRudeDeleteAroundActiveStatement(ArrayBuilder( ArrayBuilder diagnostics, - Match match, + IReadOnlyDictionary reverseMap, Func nodeSelector, SyntaxNode oldActiveStatement, + SyntaxNode oldEncompassingAncestor, SyntaxNode newActiveStatement, + SyntaxNode newEncompassingAncestor, Func areEquivalent, Func? areSimilar) where TSyntaxNode : SyntaxNode { - var newNodes = GetAncestors(GetEncompassingAncestor(match.NewRoot), newActiveStatement, nodeSelector); + var newNodes = GetAncestors(newEncompassingAncestor, newActiveStatement, nodeSelector); if (newNodes == null) { return; } - var oldNodes = GetAncestors(GetEncompassingAncestor(match.OldRoot), oldActiveStatement, nodeSelector); + var oldNodes = GetAncestors(oldEncompassingAncestor, oldActiveStatement, nodeSelector); int matchCount; if (oldNodes != null) { - matchCount = MatchNodes(oldNodes, newNodes, diagnostics: null, match: match, comparer: areEquivalent); + matchCount = MatchNodes(oldNodes, newNodes, diagnostics: null, reverseMap, comparer: areEquivalent); // Do another pass over the nodes to improve error messages. if (areSimilar != null && matchCount < Math.Min(oldNodes.Count, newNodes.Count)) { - matchCount += MatchNodes(oldNodes, newNodes, diagnostics: diagnostics, match: null, comparer: areSimilar); + matchCount += MatchNodes(oldNodes, newNodes, diagnostics, reverseMap: null, comparer: areSimilar); } } else @@ -1784,7 +1776,7 @@ private int MatchNodes( List oldNodes, List newNodes, ArrayBuilder? diagnostics, - Match? match, + IReadOnlyDictionary? reverseMap, Func comparer) where TSyntaxNode : SyntaxNode { @@ -1818,13 +1810,13 @@ private int MatchNodes( } var i = -1; - if (match == null) + if (reverseMap == null) { i = IndexOfEquivalent(newNode, oldNodes, oldIndex, comparer); } - else if (match.TryGetOldNode(newNode, out var partner) && comparer((TSyntaxNode)partner, (TSyntaxNode)newNode)) + else if (reverseMap.TryGetValue(newNode, out var oldPartner) && comparer((TSyntaxNode)oldPartner, (TSyntaxNode)newNode)) { - i = oldNodes.IndexOf(partner, oldIndex); + i = oldNodes.IndexOf(oldPartner, oldIndex); } if (i >= 0) @@ -1923,7 +1915,7 @@ private void AnalyzeTrivia( continue; } - var newTokens = TryGetDeclarationBody(newNode)?.GetActiveTokens(); + var newTokens = TryGetDeclarationBody(newNode, symbol: null)?.GetActiveTokens(); if (newTokens == null) { continue; @@ -1932,7 +1924,7 @@ private void AnalyzeTrivia( // A (rude) edit could have been made that changes whether the node may contain active statements, // so although the nodes match they might not have the same active tokens. // E.g. field declaration changed to const field declaration. - var oldTokens = TryGetDeclarationBody(oldNode)?.GetActiveTokens(); + var oldTokens = TryGetDeclarationBody(oldNode, symbol: null)?.GetActiveTokens(); if (oldTokens == null) { continue; @@ -1961,8 +1953,8 @@ bool IsCurrentSegmentBreakpointSpanMappable() Contract.ThrowIfNull(newToken.Parent); // Some nodes (e.g. const local declaration) may not be covered by a breakpoint span. - if (!TryGetEnclosingBreakpointSpan(oldToken.Parent, oldToken.SpanStart, out var oldBreakpointSpan) || - !TryGetEnclosingBreakpointSpan(newToken.Parent, newToken.SpanStart, out var newBreakpointSpan)) + if (!TryGetEnclosingBreakpointSpan(oldToken, out var oldBreakpointSpan) || + !TryGetEnclosingBreakpointSpan(newToken, out var newBreakpointSpan)) { return true; } @@ -2375,23 +2367,18 @@ private async Task> AnalyzeSemanticsAsync( foreach (var symbolEdit in symbolEdits) { - Func? syntaxMap; - SemanticEditKind editKind; - var (oldSymbol, newSymbol, syntacticEditKind) = symbolEdit; - var symbol = newSymbol ?? oldSymbol; - Contract.ThrowIfNull(symbol); - - if (!processedSymbols.Add(symbol)) - { - continue; - } if (syntacticEditKind == EditKind.Move) { Debug.Assert(oldSymbol is INamedTypeSymbol); Debug.Assert(newSymbol is INamedTypeSymbol); + if (!processedSymbols.Add(newSymbol)) + { + continue; + } + var oldSymbolInNewCompilation = SymbolKey.Create(oldSymbol, cancellationToken).Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; var newSymbolInOldCompilation = SymbolKey.Create(newSymbol, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; @@ -2418,12 +2405,23 @@ private async Task> AnalyzeSemanticsAsync( continue; } - var symbolKey = SymbolKey.Create(symbol, cancellationToken); + var symbolKey = SymbolKey.Create(newSymbol ?? oldSymbol, cancellationToken); // Ignore ambiguous resolution result - it may happen if there are semantic errors in the compilation. oldSymbol ??= symbolKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; newSymbol ??= symbolKey.Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + var symbol = newSymbol ?? oldSymbol; + Contract.ThrowIfNull(symbol); + + if (!processedSymbols.Add(symbol)) + { + continue; + } + + Func? syntaxMap; + SemanticEditKind editKind; + var (oldDeclaration, newDeclaration) = GetSymbolDeclarationNodes(oldSymbol, newSymbol, edit.OldNode, edit.NewNode); // The syntax change implies an update of the associated symbol but the old/new symbol does not actually exist. @@ -2463,7 +2461,7 @@ private async Task> AnalyzeSemanticsAsync( if (capabilities.Grant(EditAndContinueCapabilities.NewTypeDefinition)) { semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Replace, containingTypeSymbolKey, syntaxMap: null, syntaxMapTree: null, - IsPartialEdit(oldContainingType, newContainingType, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? containingTypeSymbolKey : null)); + IsPartialEdit(oldContainingType, newContainingType, editScript.Match) ? containingTypeSymbolKey : null)); } else { @@ -2503,7 +2501,7 @@ private async Task> AnalyzeSemanticsAsync( else { semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Replace, symbolKey, syntaxMap: null, syntaxMapTree: null, - IsPartialEdit(oldType, newType, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? symbolKey : null)); + IsPartialEdit(oldType, newType, editScript.Match) ? symbolKey : null)); } } @@ -2531,7 +2529,7 @@ private async Task> AnalyzeSemanticsAsync( // or replaced with an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) // Ignore the delete if there is going to be an insert corresponding to the new symbol that will create an update edit. - if (HasInsertMatchingDelete(newSymbol, oldCompilation, cancellationToken)) + if (DeleteEditImpliesInsertEdit(oldSymbol, newSymbol, oldCompilation, cancellationToken)) { continue; } @@ -2546,8 +2544,6 @@ private async Task> AnalyzeSemanticsAsync( { // If a constructor is deleted and replaced by an implicit one the update needs to aggregate updates (syntax maps) of all data member initializers. DeferConstructorEdit(oldSymbol.ContainingType, newSymbol.ContainingType, newDeclaration: null, syntaxMap, oldSymbol.IsStatic, isMemberWithDeletedInitializer: false); - - processedSymbols.Remove(oldSymbol); continue; } @@ -2557,8 +2553,6 @@ newSymbol is IPropertySymbol newProperty && IsPrimaryConstructorParameterMatchingSymbol(newSymbol, cancellationToken)) { AnalyzeRecordPropertyReplacement((IPropertySymbol)oldSymbol, newProperty, isDeleteEdit: true); - editKind = SemanticEditKind.Update; - break; } // there is no insert edit for an implicit declaration, therefore we need to issue an update: @@ -2585,7 +2579,7 @@ newSymbol is IPropertySymbol newProperty && // Associated member declarations must be in the same document as the symbol, so we don't need to resolve their symbol. // In some cases the symbol even can't be resolved unambiguously. Consider e.g. resolving a method with its parameter deleted - // we wouldn't know which overload to resolve to. - if (TryGetAssociatedMemberDeclaration(oldDeclaration, EditKind.Delete, out var oldAssociatedMemberDeclaration)) + if (TryGetAssociatedMemberDeclaration(oldSymbol, EditKind.Delete, cancellationToken, out var oldAssociatedMemberDeclaration)) { if (HasEdit(editMap, oldAssociatedMemberDeclaration, EditKind.Delete)) { @@ -2666,7 +2660,7 @@ newSymbol is IPropertySymbol newProperty && var newMatchingSymbol = newContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m is IPropertySymbol or IFieldSymbol); if (newMatchingSymbol is null) { - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, partialType: null, cancellationToken); } } @@ -2682,7 +2676,7 @@ newSymbol is IPropertySymbol newProperty && AddDeconstructorEdits(semanticEdits, oldPrimaryConstructor, otherConstructor: null, containingTypeKey, oldCompilation, newCompilation, syntaxMap: null, processedSymbols, isParameterDelete: true, cancellationToken); // Synthesized method updates: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newContainingType, partialType: null, cancellationToken); } continue; @@ -2700,13 +2694,12 @@ newSymbol is IPropertySymbol newProperty && INamedTypeSymbol? oldContainingType; var newContainingType = newSymbol.ContainingType; - // Check if the declaration has been moved from one document to another. if (oldSymbol != null) { - editKind = SemanticEditKind.Update; - // Symbol has actually not been inserted but rather moved between documents or partial type declarations, // or is replacing an implicitly generated one (e.g. parameterless constructor, auto-generated record methods, etc.) + editKind = SemanticEditKind.Update; + oldContainingType = oldSymbol.ContainingType; if (oldSymbol is IPropertySymbol { ContainingType.IsRecord: true, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true } oldRecordProperty && @@ -2743,17 +2736,15 @@ newSymbol is IPropertySymbol newProperty && // We can therefore ignore any symbols that have more than one declaration. ReportTypeLayoutUpdateRudeEdits(diagnostics, newSymbol, newDeclaration, newModel, ref lazyLayoutAttribute); - var oldBody = TryGetDeclarationBody(oldDeclaration); + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); if (oldBody != null) { // The old symbol's declaration syntax may be located in a different document than the old version of the current document. var oldSyntaxDocument = oldProject.Solution.GetRequiredDocument(oldDeclaration.SyntaxTree); var oldSyntaxModel = await oldSyntaxDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var oldSyntaxText = await oldSyntaxDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var newBody = TryGetDeclarationBody(newDeclaration); + var newBody = TryGetDeclarationBody(newDeclaration, newSymbol); - // Skip analysis of active statements. We already report rude edit for removal of code containing - // active statements in the old declaration and don't currently support moving active statements. AnalyzeChangedMemberBody( oldDeclaration, newDeclaration, @@ -2764,9 +2755,9 @@ newSymbol is IPropertySymbol newProperty && oldSymbol, newSymbol, newText, - oldActiveStatements: ImmutableArray.Empty, - newActiveStatementSpans: ImmutableArray.Empty, - capabilities: capabilities, + oldActiveStatements, + newActiveStatementSpans, + capabilities, newActiveStatements, newExceptionRegions, diagnostics, @@ -2781,11 +2772,6 @@ newSymbol is IPropertySymbol newProperty && if (isNewConstructorWithMemberInitializers || isDeclarationWithInitializer) { - if (isNewConstructorWithMemberInitializers) - { - processedSymbols.Remove(newSymbol); - } - if (isDeclarationWithInitializer) { AnalyzeSymbolUpdate(oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match, capabilities, diagnostics, semanticEdits, syntaxMap, processedSymbols, cancellationToken); @@ -2800,7 +2786,7 @@ newSymbol is IPropertySymbol newProperty && } } } - else if (TryGetAssociatedMemberDeclaration(newDeclaration, EditKind.Insert, out var newAssociatedMemberDeclaration) && + else if (TryGetAssociatedMemberDeclaration(newSymbol, EditKind.Insert, cancellationToken, out var newAssociatedMemberDeclaration) && HasEdit(editMap, newAssociatedMemberDeclaration, EditKind.Insert)) { // If the symbol is an accessor and the containing property/indexer/event declaration has also been inserted @@ -2838,7 +2824,7 @@ newSymbol is IPropertySymbol newProperty && var containingSymbolKey = SymbolKey.Create(newContainingType, cancellationToken); oldContainingType = containingSymbolKey.Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol as INamedTypeSymbol; - if (oldContainingType != null && !CanAddNewMemberToExistingType(newSymbol, capabilities, cancellationToken)) + if (oldContainingType != null && !CanAddNewMemberToExistingType(newSymbol, capabilities)) { diagnostics.Add(new RudeEditDiagnostic( RudeEditKind.InsertNotSupportedByRuntime, @@ -2866,7 +2852,7 @@ newSymbol is IPropertySymbol newProperty && // (PrintMembers print all properties and fields, Equals and GHC compare all data members, etc.) if (SymbolPresenceAffectsSynthesizedRecordMembers(newSymbol)) { - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingType, partialType: null, cancellationToken); } } else @@ -2907,11 +2893,6 @@ newSymbol is IPropertySymbol newProperty && DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMap, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); - if (isConstructorWithMemberInitializers) - { - processedSymbols.Remove(newSymbol); - } - if (isConstructorWithMemberInitializers || editKind == SemanticEditKind.Update) { // Don't add a separate semantic edit. @@ -2945,10 +2926,10 @@ newSymbol is IPropertySymbol newProperty && Contract.ThrowIfNull(oldDeclaration); Contract.ThrowIfNull(newDeclaration); - var oldBody = TryGetDeclarationBody(oldDeclaration); + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); if (oldBody != null) { - var newBody = TryGetDeclarationBody(newDeclaration); + var newBody = TryGetDeclarationBody(newDeclaration, newSymbol); AnalyzeChangedMemberBody( oldDeclaration, @@ -2977,11 +2958,6 @@ newSymbol is IPropertySymbol newProperty && if (isConstructorWithMemberInitializers || isDeclarationWithInitializer) { - if (isConstructorWithMemberInitializers) - { - processedSymbols.Remove(newSymbol); - } - if (isDeclarationWithInitializer) { AnalyzeSymbolUpdate(oldSymbol, newSymbol, edit.NewNode, newModel, editScript.Match, capabilities, diagnostics, semanticEdits, syntaxMap, processedSymbols, cancellationToken); @@ -3008,16 +2984,23 @@ void AnalyzeRecordPropertyReplacement(IPropertySymbol oldProperty, IPropertySymb Debug.Assert(oldProperty.ContainingType.IsRecord); Debug.Assert(newProperty.ContainingType.IsRecord); + var (customProperty, synthesizedProperty) = isDeleteEdit ? (oldProperty, newProperty) : (newProperty, oldProperty); + + Debug.Assert(synthesizedProperty.IsSynthesizedAutoProperty()); + Debug.Assert(synthesizedProperty.SetMethod != null); + + // No update is needed if both properties are synthesized. The property has been updated indirectly, + // e.g. when a primary constructor parameter is deleted from one partial declaration and inserted into another one. + if (customProperty.IsSynthesizedAutoProperty()) + { + return; + } + // The synthesized auto-property is `T P { get; init; } = P`. // If the initializer is different from `P` the primary constructor needs to be updated. // Note: we update the constructor regardless of the initializer exact shape, but we could check for it. DeferConstructorEdit(oldProperty.ContainingType, newProperty.ContainingType, newDeclaration: null, syntaxMap, oldProperty.IsStatic, isMemberWithDeletedInitializer: true); - var (customProperty, synthesizedProperty) = isDeleteEdit ? (oldProperty, newProperty) : (newProperty, oldProperty); - - // new property is synthesized - Debug.Assert(synthesizedProperty.SetMethod is { IsInitOnly: true }); - if (customProperty.SetMethod == null) { // Custom read-only property replaced with synthesized auto-property @@ -3033,7 +3016,7 @@ void AnalyzeRecordPropertyReplacement(IPropertySymbol oldProperty, IPropertySymb // The synthesized property replacing the deleted one will be an auto-property. // If the accessor had body or the property changed accessibility then synthesized record members might be affected. - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newProperty.ContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newProperty.ContainingType, partialType: null, cancellationToken); } void ReportDeletedMemberActiveStatementsRudeEdits() @@ -3041,13 +3024,13 @@ void ReportDeletedMemberActiveStatementsRudeEdits() Contract.ThrowIfNull(oldDeclaration); Contract.ThrowIfNull(oldSymbol); - var oldBody = TryGetDeclarationBody(oldDeclaration); + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); if (oldBody == null) { return; } - var activeStatementIndices = GetOverlappingActiveStatements(oldBody, oldActiveStatements); + var activeStatementIndices = oldBody.GetOverlappingActiveStatements(oldActiveStatements); if (!activeStatementIndices.Any()) { return; @@ -3070,17 +3053,26 @@ void ReportDeletedMemberActiveStatementsRudeEdits() return; } - var newSpan = GetDeletedDeclarationActiveSpan(editScript.Match.Matches, oldDeclaration); - + TextSpan? newActiveStatementSpan = null; foreach (var index in activeStatementIndices) { - Debug.Assert(newActiveStatements[index] == null); - - newActiveStatements[index] = GetActiveStatementWithSpan(oldActiveStatements[index], editScript.Match.NewRoot.SyntaxTree, newSpan, diagnostics, cancellationToken); - newExceptionRegions[index] = ImmutableArray.Empty; + if (newActiveStatements[index] == null) + { + newActiveStatementSpan ??= GetDeletedDeclarationActiveSpan(editScript.Match.Matches, oldDeclaration); + newActiveStatements[index] = GetActiveStatementWithSpan(oldActiveStatements[index], editScript.Match.NewRoot.SyntaxTree, newActiveStatementSpan.Value, diagnostics, cancellationToken); + newExceptionRegions[index] = ImmutableArray.Empty; + } + else + { + // active statements were mapped from a deleted declaration to another one: + Debug.Assert(newSymbol != null); + } } - ReportDeletedMemberRudeEdit(diagnostics, oldSymbol, newModel, RudeEditKind.DeleteActiveStatement, cancellationToken); + if (newActiveStatementSpan.HasValue) + { + ReportDeletedMemberRudeEdit(diagnostics, oldSymbol, newModel, RudeEditKind.DeleteActiveStatement, cancellationToken); + } } Contract.ThrowIfFalse(editKind is SemanticEditKind.Update or SemanticEditKind.Insert); @@ -3137,13 +3129,13 @@ void ReportDeletedMemberActiveStatementsRudeEdits() // so we also check that the old symbol can't be resolved in the new compilation if (createDeleteAndInsertEdits && AllowsDeletion(oldSymbol) && - CanAddNewMemberToExistingType(oldSymbol, capabilities, cancellationToken) && + CanAddNewMemberToExistingType(oldSymbol, capabilities) && SymbolKey.Create(oldSymbol, cancellationToken).Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol is null) { Contract.ThrowIfNull(oldDeclaration); - var oldBody = TryGetDeclarationBody(oldDeclaration); - if (oldBody != null && GetOverlappingActiveStatements(oldBody, oldActiveStatements).Any()) + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); + if (oldBody != null && oldBody.GetOverlappingActiveStatements(oldActiveStatements).Any()) { Contract.ThrowIfNull(newDeclaration); AddRudeUpdateAroundActiveStatement(diagnostics, newDeclaration); @@ -3154,7 +3146,7 @@ void ReportDeletedMemberActiveStatementsRudeEdits() AddDeleteEditsForMemberAndAccessors(semanticEdits, oldSymbol, containingSymbolKey, syntaxMap, partialType: null, cancellationToken); AddInsertEditsForMemberAndAccessors(semanticEdits, newSymbol, syntaxMap, - partialType: IsPartialEdit(oldSymbol, newSymbol, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? symbolKey : null, processedSymbols, + partialType: IsPartialEdit(oldSymbol, newSymbol, editScript.Match) ? symbolKey : null, processedSymbols, cancellationToken); } @@ -3168,10 +3160,11 @@ void ReportDeletedMemberActiveStatementsRudeEdits() } semanticEdits.Add(new SemanticEditInfo(editKind, symbolKey, syntaxMap, syntaxMapTree: null, - IsPartialEdit(oldSymbol, newSymbol, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? symbolKey : null)); + IsPartialEdit(oldSymbol, newSymbol, editScript.Match) ? symbolKey : null)); } } + // Trivia edits are generated for trivia that affect active statement positions. foreach (var (oldEditNode, newEditNode, diagnosticSpan) in triviaEdits) { Contract.ThrowIfNull(oldModel); @@ -3193,8 +3186,17 @@ void ReportDeletedMemberActiveStatementsRudeEdits() Contract.ThrowIfNull(oldDeclaration); Contract.ThrowIfNull(newDeclaration); + // if the member doesn't have a body triva changes have no effect: + var oldBody = TryGetDeclarationBody(oldDeclaration, oldSymbol); + if (oldBody == null) + { + continue; + } + var oldContainingType = oldSymbol.ContainingType; var newContainingType = newSymbol.ContainingType; + + // types do not have bodies: Contract.ThrowIfNull(oldContainingType); Contract.ThrowIfNull(newContainingType); @@ -3206,7 +3208,7 @@ void ReportDeletedMemberActiveStatementsRudeEdits() { var containingTypeSymbolKey = SymbolKey.Create(oldContainingType, cancellationToken); semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Replace, containingTypeSymbolKey, syntaxMap: null, syntaxMapTree: null, - IsPartialEdit(oldContainingType, newContainingType, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? containingTypeSymbolKey : null)); + IsPartialEdit(oldContainingType, newContainingType, editScript.Match) ? containingTypeSymbolKey : null)); } else { @@ -3217,49 +3219,38 @@ void ReportDeletedMemberActiveStatementsRudeEdits() continue; } - // We need to provide syntax map to the compiler if the member is active (see member update above): - var oldBody = TryGetDeclarationBody(oldDeclaration); - var newBody = TryGetDeclarationBody(newDeclaration); + var newBody = TryGetDeclarationBody(newDeclaration, newSymbol); // only trivia changed: - Debug.Assert(oldBody is null == newBody is null); + Contract.ThrowIfNull(newBody); Debug.Assert(IsConstructorWithMemberInitializers(oldSymbol, cancellationToken) == IsConstructorWithMemberInitializers(newSymbol, cancellationToken)); Debug.Assert(IsDeclarationWithInitializer(oldDeclaration) == IsDeclarationWithInitializer(newDeclaration)); Func? syntaxMap = null; - if (oldBody != null) - { - Debug.Assert(newBody != null); + // We need to provide syntax map to the compiler if the member is active (see member update above): + var isActiveMember = + oldBody.GetOverlappingActiveStatements(oldActiveStatements).Any() || + IsStateMachineMethod(oldDeclaration) || + ContainsLambda(oldBody); - var isActiveMember = - GetOverlappingActiveStatements(oldBody, oldActiveStatements).Any() || - IsStateMachineMethod(oldDeclaration) || - ContainsLambda(oldBody); + syntaxMap = isActiveMember ? CreateSyntaxMapForEquivalentNodes(oldBody, newBody) : null; - syntaxMap = isActiveMember ? CreateSyntaxMapForEquivalentNodes(oldBody, newBody) : null; + ReportMemberOrLambdaBodyUpdateRudeEditsImpl(diagnostics, newDeclaration, newBody); - ReportMemberOrLambdaBodyUpdateRudeEditsImpl(diagnostics, newDeclaration, newBody); + var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); + var isDeclarationWithInitializer = IsDeclarationWithInitializer(newDeclaration); - var isConstructorWithMemberInitializers = IsConstructorWithMemberInitializers(newSymbol, cancellationToken); - var isDeclarationWithInitializer = IsDeclarationWithInitializer(newDeclaration); - - if (isConstructorWithMemberInitializers || isDeclarationWithInitializer) - { - // TODO: only create syntax map if any field initializers are active/contain lambdas or this is a partial type - syntaxMap ??= CreateSyntaxMapForEquivalentNodes(oldBody, newBody); - - if (isConstructorWithMemberInitializers) - { - processedSymbols.Remove(newSymbol); - } + if (isConstructorWithMemberInitializers || isDeclarationWithInitializer) + { + // TODO: only create syntax map if any field initializers are active/contain lambdas or this is a partial type + syntaxMap ??= CreateSyntaxMapForEquivalentNodes(oldBody, newBody); - DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMap, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); + DeferConstructorEdit(oldContainingType, newContainingType, newDeclaration, syntaxMap, newSymbol.IsStatic, isMemberWithDeletedInitializer: false); - // Don't add a separate semantic edit. - // Updates of data members with initializers and constructors that emit initializers will be aggregated and added later. - continue; - } + // Don't add a separate semantic edit. + // Updates of data members with initializers and constructors that emit initializers will be aggregated and added later. + continue; } // updating generic methods and types @@ -3271,7 +3262,7 @@ void ReportDeletedMemberActiveStatementsRudeEdits() var symbolKey = SymbolKey.Create(newSymbol, cancellationToken); semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Update, symbolKey, syntaxMap, syntaxMapTree: null, - IsPartialEdit(oldSymbol, newSymbol, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? symbolKey : null)); + IsPartialEdit(oldSymbol, newSymbol, editScript.Match) ? symbolKey : null)); } } @@ -3447,7 +3438,7 @@ private bool TryAddParameterInsertOrDeleteEdits( // the match in var (oldContainingSymbol, newContainingSymbol) = isParameterDelete ? (member, otherMember) : (otherMember, member); - if (!CanRenameOrChangeSignature(oldContainingSymbol, newContainingSymbol, capabilities, cancellationToken)) + if (!CanRenameOrChangeSignature(oldContainingSymbol, newContainingSymbol, capabilities)) { notSupportedByRuntime = true; return false; @@ -3528,7 +3519,7 @@ private void AddSynthesizedMemberEditsForParameterChange( AddDeconstructorEdits(semanticEdits, primaryConstructor, otherPrimaryConstructor, containingTypeKey, model.Compilation, otherModel.Compilation, syntaxMap, processedSymbols, isParameterDelete, cancellationToken); // Synthesized method updates - we can add edits for each changed parameter, they will get deduplicated. - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, otherModel.Compilation, primaryConstructor.ContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, otherModel.Compilation, primaryConstructor.ContainingType, partialType: null, cancellationToken); } /// @@ -3903,10 +3894,11 @@ private void ReportMemberOrLambdaBodyUpdateRudeEdits( SemanticModel? oldModel, SyntaxNode oldDeclaration, ISymbol oldMember, + MemberBody oldMemberBody, SyntaxNode newDeclaration, DeclarationBody newBody, ISymbol newMember, - Match memberBodyMatch, + MemberBody newMemberBody, EditAndContinueCapabilitiesGrantor capabilities, StateMachineInfo oldStateMachineInfo, StateMachineInfo newStateMachineInfo) @@ -3938,8 +3930,8 @@ private void ReportMemberOrLambdaBodyUpdateRudeEdits( if ((InGenericContext(oldMember) || InGenericContext(newMember) || - IsLambda(oldDeclaration) && InGenericLocalContext(oldDeclaration, OneOrMany.Create(memberBodyMatch.OldRoot)) || - IsLambda(newDeclaration) && InGenericLocalContext(newDeclaration, OneOrMany.Create(memberBodyMatch.NewRoot))) && + IsLambda(oldDeclaration) && InGenericLocalContext(oldDeclaration, oldMemberBody.RootNodes) || + IsLambda(newDeclaration) && InGenericLocalContext(newDeclaration, newMemberBody.RootNodes)) && !capabilities.Grant(EditAndContinueCapabilities.GenericAddFieldToExistingType)) { diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.UpdatingGenericNotSupportedByRuntime, GetDiagnosticSpan(newDeclaration, EditKind.Update), newDeclaration, new[] { GetDisplayName(newDeclaration) })); @@ -4007,7 +3999,7 @@ private void ReportUpdatedSymbolDeclarationRudeEdits( { rudeEdit = RudeEditKind.Renamed; } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities, cancellationToken)) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } @@ -4023,7 +4015,7 @@ private void ReportUpdatedSymbolDeclarationRudeEdits( { rudeEdit = RudeEditKind.Renamed; } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities, cancellationToken)) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } @@ -4039,7 +4031,7 @@ private void ReportUpdatedSymbolDeclarationRudeEdits( { rudeEdit = RudeEditKind.Renamed; } - else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities, cancellationToken)) + else if (!CanRenameOrChangeSignature(oldSymbol, newSymbol, capabilities)) { rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } @@ -4104,6 +4096,14 @@ private void ReportUpdatedSymbolDeclarationRudeEdits( rudeEdit = RudeEditKind.AccessorKindUpdate; } + // Changing property accessor to auto-property accessor adds a field: + if (oldMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol oldProperty } && !oldProperty.IsAutoProperty() && + newMethod is { MethodKind: MethodKind.PropertyGet, AssociatedSymbol: IPropertySymbol newProperty } && newProperty.IsAutoProperty() && + !capabilities.Grant(GetRequiredAddFieldCapabilities(newMethod))) + { + rudeEdit = RudeEditKind.InsertNotSupportedByRuntime; + } + // Consider: Generalize to compare P/Invokes regardless of how they are defined (using attribute or Declare) if (oldMethod.MethodKind == MethodKind.DeclareMethod || newMethod.MethodKind == MethodKind.DeclareMethod) { @@ -4322,7 +4322,7 @@ private void AnalyzeParameterType( } else if (AllowsDeletion(newParameter.ContainingSymbol)) { - if (CanRenameOrChangeSignature(oldParameter.ContainingSymbol, newParameter.ContainingSymbol, capabilities, cancellationToken)) + if (CanRenameOrChangeSignature(oldParameter.ContainingSymbol, newParameter.ContainingSymbol, capabilities)) { hasParameterTypeChange = true; } @@ -4371,7 +4371,7 @@ private void AnalyzeReturnType(IMethodSymbol oldMethod, IMethodSymbol newMethod, } else if (AllowsDeletion(newMethod)) { - if (CanRenameOrChangeSignature(oldMethod, newMethod, capabilities, cancellationToken)) + if (CanRenameOrChangeSignature(oldMethod, newMethod, capabilities)) { hasReturnTypeChange = true; } @@ -4401,7 +4401,7 @@ private void AnalyzeReturnType(IEventSymbol oldEvent, IEventSymbol newEvent, Edi } else if (AllowsDeletion(newEvent)) { - if (CanRenameOrChangeSignature(oldEvent, newEvent, capabilities, cancellationToken)) + if (CanRenameOrChangeSignature(oldEvent, newEvent, capabilities)) { hasReturnTypeChange = true; } @@ -4431,7 +4431,7 @@ private void AnalyzeReturnType(IPropertySymbol oldProperty, IPropertySymbol newP } else if (AllowsDeletion(newProperty)) { - if (CanRenameOrChangeSignature(oldProperty, newProperty, capabilities, cancellationToken)) + if (CanRenameOrChangeSignature(oldProperty, newProperty, capabilities)) { hasReturnTypeChange = true; } @@ -4528,13 +4528,13 @@ private void AddCustomAttributeSemanticEdits( { var symbolKey = SymbolKey.Create(newSymbol, cancellationToken); semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Update, symbolKey, syntaxMap, syntaxMapTree: null, - IsPartialEdit(oldSymbol, newSymbol, topMatch.OldRoot.SyntaxTree, topMatch.NewRoot.SyntaxTree) ? symbolKey : null)); + IsPartialEdit(oldSymbol, newSymbol, topMatch) ? symbolKey : null)); } else if (newSymbol is ITypeParameterSymbol) { var containingTypeSymbolKey = SymbolKey.Create(newSymbol.ContainingSymbol, cancellationToken); semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Update, containingTypeSymbolKey, syntaxMap, syntaxMapTree: null, - IsPartialEdit(oldSymbol.ContainingSymbol, newSymbol.ContainingSymbol, topMatch.OldRoot.SyntaxTree, topMatch.NewRoot.SyntaxTree) ? containingTypeSymbolKey : null)); + IsPartialEdit(oldSymbol.ContainingSymbol, newSymbol.ContainingSymbol, topMatch) ? containingTypeSymbolKey : null)); } else if (newSymbol is IFieldSymbol or IPropertySymbol or IEventSymbol) { @@ -4585,7 +4585,7 @@ private void AddParameterUpdateSemanticEdit( AddDeleteAndInsertEditsForMemberAndAccessors(semanticEdits, oldSynthesizedDeconstructor, newSynthesizedDeconstructor, containingSymbolKey, syntaxMap, processedSymbols, cancellationToken); // add updates of synthesized methods: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingMember.ContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newCompilation, newContainingMember.ContainingType, partialType: null, cancellationToken); } } else @@ -4813,33 +4813,35 @@ static bool IsSecurityAttribute(INamedTypeSymbol namedTypeSymbol) /// Check if the allow us to rename or change signature of a member. /// Such edit translates to an addition of a new member, an update of any method bodies associated with the old one and marking the member as "deleted". /// - private bool CanRenameOrChangeSignature(ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities, CancellationToken cancellationToken) - => CanAddNewMemberToExistingType(newSymbol, capabilities, cancellationToken) && + private static bool CanRenameOrChangeSignature(ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) + => CanAddNewMemberToExistingType(newSymbol, capabilities) && CanUpdateMemberBody(oldSymbol, newSymbol, capabilities); - private bool CanAddNewMemberToExistingType(ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities, CancellationToken cancellationToken) + private static bool CanAddNewMemberToExistingType(ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) { var requiredCapabilities = EditAndContinueCapabilities.None; if (newSymbol is IMethodSymbol or IEventSymbol or IPropertySymbol) { - requiredCapabilities |= EditAndContinueCapabilities.AddMethodToExistingType; + requiredCapabilities |= GetRequiredAddMethodCapabilities(newSymbol); } - if (newSymbol is IFieldSymbol || newSymbol is IPropertySymbol { DeclaringSyntaxReferences: [_] } && HasBackingField(GetSymbolDeclarationSyntax(newSymbol, cancellationToken))) + if (newSymbol is IFieldSymbol || newSymbol.IsAutoProperty()) { - requiredCapabilities |= newSymbol.IsStatic ? EditAndContinueCapabilities.AddStaticFieldToExistingType : EditAndContinueCapabilities.AddInstanceFieldToExistingType; - } - - // Inserting a member into an existing generic type, or a generic method into a type is only allowed if the runtime supports it - if (newSymbol is not INamedTypeSymbol && InGenericContext(newSymbol)) - { - requiredCapabilities |= newSymbol is IFieldSymbol ? EditAndContinueCapabilities.GenericAddFieldToExistingType : EditAndContinueCapabilities.GenericAddMethodToExistingType; + requiredCapabilities |= GetRequiredAddFieldCapabilities(newSymbol); } return capabilities.Grant(requiredCapabilities); } + private static EditAndContinueCapabilities GetRequiredAddMethodCapabilities(ISymbol symbol) + => EditAndContinueCapabilities.AddMethodToExistingType | + (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddMethodToExistingType : 0); + + private static EditAndContinueCapabilities GetRequiredAddFieldCapabilities(ISymbol symbol) + => (symbol.IsStatic ? EditAndContinueCapabilities.AddStaticFieldToExistingType : EditAndContinueCapabilities.AddInstanceFieldToExistingType) | + (InGenericContext(symbol) ? EditAndContinueCapabilities.GenericAddFieldToExistingType : 0); + private static bool CanUpdateMemberBody(ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) { if (InGenericContext(oldSymbol) || InGenericContext(newSymbol)) @@ -4857,14 +4859,18 @@ private static void AddSynthesizedRecordMethodUpdatesForPropertyChange( ArrayBuilder semanticEdits, Compilation compilation, INamedTypeSymbol recordType, + SymbolKey? partialType, CancellationToken cancellationToken) { Debug.Assert(recordType.IsRecord); foreach (var member in GetRecordUpdatedSynthesizedMethods(compilation, recordType)) { + // We update all synthesized members regardless of whether the original change in the property actually changed them. + // We could avoid these updates if we check the details (e.g. name & type matching, etc.) + var symbolKey = SymbolKey.Create(member, cancellationToken); - semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Update, symbolKey, syntaxMap: null, syntaxMapTree: null, partialType: null)); + semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Update, symbolKey, syntaxMap: null, syntaxMapTree: null, partialType)); } } @@ -5032,8 +5038,8 @@ internal void ReportTypeLayoutUpdateRudeEdits( break; case SymbolKind.Property: - if (HasBackingField(newSyntax) && - HasExplicitOrSequentialLayout(newSymbol.ContainingType, newModel, ref lazyLayoutAttribute)) + if (HasExplicitOrSequentialLayout(newSymbol.ContainingType, newModel, ref lazyLayoutAttribute) && + newSymbol.IsAutoProperty()) { ReportTypeLayoutUpdateRudeEdits(diagnostics, newSymbol, newSyntax); } @@ -5137,7 +5143,7 @@ private static bool HasExplicitOrSequentialLayout( return newNode => { // containing declaration - if (!TryFindMemberDeclaration(root: null, newNode, out var newDeclarations)) + if (!TryFindMemberDeclaration(root: null, newNode, newNode.Span, out var newDeclarations)) { return null; } @@ -5156,8 +5162,8 @@ private static bool HasExplicitOrSequentialLayout( // The node is in a declaration that hasn't been changed: if (reverseTopMatches.TryGetValue(newDeclaration, out var oldDeclaration)) { - var oldBody = TryGetDeclarationBody(oldDeclaration); - var newBody = TryGetDeclarationBody(newDeclaration); + var oldBody = TryGetDeclarationBody(oldDeclaration, symbol: null); + var newBody = TryGetDeclarationBody(newDeclaration, symbol: null); // The declarations must have bodies since we found newNode in the newDeclaration's body // and the new body can only differ from the old one in trivia. @@ -5214,12 +5220,6 @@ private void AddConstructorEdits( foreach (var newCtor in isStatic ? newType.StaticConstructors : newType.InstanceConstructors) { - if (processedSymbols.Contains(newCtor)) - { - // we already have an edit for the new constructor - continue; - } - if (newType.TypeKind != oldType.TypeKind || oldType.IsRecord != newType.IsRecord) { // rude edit has been reported when changing type kinds @@ -5292,7 +5292,7 @@ private void AddConstructorEdits( if (oldCtor != null && newDeclaration.SyntaxTree == newSyntaxTree && anyInitializerUpdatesInCurrentDocument && - TryGetDeclarationBody(newDeclaration) is { } newBody) + TryGetDeclarationBody(newDeclaration, newCtor) is { } newBody) { ReportMemberOrLambdaBodyUpdateRudeEditsImpl(diagnostics, newDeclaration, newBody); } @@ -5346,7 +5346,7 @@ private void AddConstructorEdits( AddDeconstructorEdits(semanticEdits, oldCtor, newCtor, typeKey, oldCompilation, newModel.Compilation, syntaxMap: null, processedSymbols, isParameterDelete: newCtorIsPrimary, cancellationToken); // Synthesized method updates: - AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newCtor.ContainingType, cancellationToken); + AddSynthesizedRecordMethodUpdatesForPropertyChange(semanticEdits, newModel.Compilation, newCtor.ContainingType, partialType: null, cancellationToken); } } } @@ -5391,7 +5391,7 @@ private bool HasMemberInitializerContainingLambda(INamedTypeSymbol type, bool is member.DeclaringSyntaxReferences.Length > 0) // skip generated fields (e.g. VB auto-property backing fields) { var syntax = GetSymbolDeclarationSyntax(member, cancellationToken); - if (IsDeclarationWithInitializer(syntax) && TryGetDeclarationBody(syntax) is { } body && ContainsLambda(body)) + if (IsDeclarationWithInitializer(syntax) && TryGetDeclarationBody(syntax, member) is { } body && ContainsLambda(body)) { return true; } @@ -5414,6 +5414,9 @@ private bool HasMemberInitializerContainingLambda(INamedTypeSymbol type, bool is } } + private static bool IsPartialEdit(ISymbol? oldSymbol, ISymbol? newSymbol, Match topMatch) + => IsPartialEdit(oldSymbol, newSymbol, topMatch.OldRoot.SyntaxTree, topMatch.NewRoot.SyntaxTree); + private static bool IsPartialEdit(ISymbol? oldSymbol, ISymbol? newSymbol, SyntaxTree oldSyntaxTree, SyntaxTree newSyntaxTree) { // If any of the partial declarations of the new or the old type are in another document @@ -5438,7 +5441,6 @@ private void ReportLambdaAndClosureRudeEdits( ISymbol newMember, MemberBody newMemberBody, SyntaxNode newDeclaration, - Match memberBodyMatch, IReadOnlyDictionary? matchedLambdas, BidirectionalMap map, EditAndContinueCapabilitiesGrantor capabilities, @@ -5466,7 +5468,7 @@ private void ReportLambdaAndClosureRudeEdits( var oldStateMachineInfo = oldLambdaBody.GetStateMachineInfo(); var newStateMachineInfo = newLambdaBody.GetStateMachineInfo(); - ReportStateMachineBodyUpdateRudeEdits(lambdaBodyMatch, oldStateMachineInfo, newLambda, newStateMachineInfo, newLambdaInfo.HasActiveStatement, diagnostics); + ReportStateMachineBodyUpdateRudeEdits(lambdaBodyMatch.Value, oldStateMachineInfo, newLambda, newStateMachineInfo, newLambdaInfo.HasActiveStatement, diagnostics); // When the delta IL of the containing method is emitted lambdas declared in it are also emitted. // If the runtime does not support changing IL of the method (e.g. method containing stackalloc) @@ -5481,10 +5483,11 @@ private void ReportLambdaAndClosureRudeEdits( oldModel, oldLambda, oldMember, + oldMemberBody, newLambda, newLambdaBody, newMember, - memberBodyMatch, + newMemberBody, capabilities, oldStateMachineInfo, newStateMachineInfo); @@ -5918,6 +5921,31 @@ private static void BuildIndex(Dictionary index, ImmutableArray } } + /// + /// Returns the declaration of + /// - a property, indexer or event declaration whose accessor is the specified , + /// - a method, an indexer, a type (delegate), or primary constructor parameter list if the is a parameter, + /// - a method or an type if the is a type parameter. + /// + internal bool TryGetAssociatedMemberDeclaration(ISymbol symbol, EditKind editKind, CancellationToken cancellationToken, [NotNullWhen(true)] out SyntaxNode? declaration) + { + var associatedSymbol = symbol switch + { + IParameterSymbol or ITypeParameterSymbol => symbol.ContainingSymbol, + IMethodSymbol method when editKind != EditKind.Delete => method.AssociatedSymbol, + _ => null + }; + + if (associatedSymbol == null) + { + declaration = null; + return false; + } + + declaration = GetSymbolDeclarationSyntax(associatedSymbol, cancellationToken); + return true; + } + /// /// Returns node that represents a declaration of the symbol. /// @@ -6729,15 +6757,22 @@ public bool IsPrimaryConstructorParameterMatchingSymbol(ISymbol symbol, Cancella => (IPropertySymbol?)parameter.ContainingType.GetMembers(parameter.Name) .FirstOrDefault(static m => m is IPropertySymbol { IsImplicitlyDeclared: false, GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }); + /// + /// True if being inserted or deleted affects the bodies of synthesized record members. + /// private static bool SymbolPresenceAffectsSynthesizedRecordMembers(ISymbol symbol) - => symbol is (IPropertySymbol { GetMethod: not null } or IFieldSymbol) and { IsStatic: false, ContainingType.IsRecord: true }; + => symbol is { IsStatic: false, ContainingType.IsRecord: true } and + (IPropertySymbol { GetMethod.IsImplicitlyDeclared: false, SetMethod: null or { IsImplicitlyDeclared: false } } or IFieldSymbol); /// - /// True if a syntactic delete edit of an old symbol has a matching syntactic insert edit. + /// True if a syntactic delete edit of an in + /// that has a corresponding in the new compilation implies an existance + /// of a matching syntactic insert edit (either in the currently analyzed document or another one). /// /// The old symbol has to be explicitly declared, otherwise it couldn't have been deleted via syntactic delete edit. + /// Only detects scenarios where an insert must have occurred. False doesn't mean an insert does not exist. /// - private bool HasInsertMatchingDelete(ISymbol newSymbol, Compilation oldCompilation, CancellationToken cancellationToken) + private bool DeleteEditImpliesInsertEdit(ISymbol oldSymbol, ISymbol newSymbol, Compilation oldCompilation, CancellationToken cancellationToken) { if (!newSymbol.IsSynthesized()) { @@ -6765,18 +6800,35 @@ private bool HasInsertMatchingDelete(ISymbol newSymbol, Compilation oldCompilati static (parameter, name) => parameter.Name == name, newSymbol.Name) is { } newPrimaryParameter) { var oldParameter = SymbolKey.Create(newPrimaryParameter, cancellationToken).Resolve(oldCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + var oldProperty = (IPropertySymbol)oldSymbol; // An insert exists if the new primary parameter is explicitly declared and // the old one doesn't exist, is synthesized, or is not a primary constructor parameter. + // A syntax change is causing the old synthesized property to be deleted, + // so there has to be an insert inserting the new one. + // + // old: + // + // record R() { int P { get; init; } } // insert exists if oldParameter == null + // record R(int P) { int P { get; init; } } // no insert + // record R(int P); // insert exists if oldProperty is synthesized auto-prop + // + // new: + // + // record R(int P); + return !newPrimaryParameter.IsSynthesized() && - (oldParameter == null || oldParameter.IsSynthesized() || !IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken)); + (oldParameter == null || oldParameter.IsSynthesized() || !IsPrimaryConstructor(oldParameter.ContainingSymbol, cancellationToken) || oldProperty.IsSynthesizedAutoProperty()); } // Accessor of a property is synthesized based on presence of the property: if (newSymbol is IMethodSymbol { AssociatedSymbol: IPropertySymbol { } newProperty }) { + var oldProperty = ((IMethodSymbol)oldSymbol).AssociatedSymbol; + Contract.ThrowIfNull(oldProperty); + // An insert exists if an insert exists for the new property - return HasInsertMatchingDelete(newProperty, oldCompilation, cancellationToken); + return DeleteEditImpliesInsertEdit(oldProperty, newProperty, oldCompilation, cancellationToken); } return false; @@ -6805,12 +6857,12 @@ internal readonly struct TestAccessor(AbstractEditAndContinueAnalyzer abstractEd internal void ReportTopLevelSyntacticRudeEdits(ArrayBuilder diagnostics, EditScript syntacticEdits, Dictionary editMap) => _abstractEditAndContinueAnalyzer.ReportTopLevelSyntacticRudeEdits(diagnostics, syntacticEdits, editMap); - internal BidirectionalMap ComputeMap( - Match bodyMatch, + internal BidirectionalMap IncludeLambdaBodyMaps( + BidirectionalMap bodyMatch, ArrayBuilder memberBodyActiveNodes, ref Dictionary? lazyActiveOrMatchedLambdas) { - return _abstractEditAndContinueAnalyzer.ComputeMap(bodyMatch, memberBodyActiveNodes, ref lazyActiveOrMatchedLambdas); + return _abstractEditAndContinueAnalyzer.IncludeLambdaBodyMaps(bodyMatch, memberBodyActiveNodes, ref lazyActiveOrMatchedLambdas); } } diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractSimpleMemberBody.cs b/src/Features/Core/Portable/EditAndContinue/AbstractSimpleMemberBody.cs index ba057d7f40293..ab912035d39a9 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractSimpleMemberBody.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractSimpleMemberBody.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.EditAndContinue; @@ -19,7 +20,7 @@ public sealed override SyntaxTree SyntaxTree public sealed override OneOrMany RootNodes => OneOrMany.Create(node); - public sealed override ActiveStatementEnvelope Envelope + public sealed override TextSpan Envelope => node.Span; public sealed override SyntaxNode EncompassingAncestor diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementEnvelope.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementEnvelope.cs deleted file mode 100644 index 13366695fd2cc..0000000000000 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementEnvelope.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -/// -/// Represents a that covers all active statements of with a possible excluded. -/// -internal readonly record struct ActiveStatementEnvelope(TextSpan Span, TextSpan Hole = default) -{ - public static implicit operator ActiveStatementEnvelope(TextSpan span) - => new(span, default); -} diff --git a/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs b/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs index 5ec846bdd527f..3ea9546ebbc30 100644 --- a/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs +++ b/src/Features/Core/Portable/EditAndContinue/ActiveStatementsMap.cs @@ -316,7 +316,18 @@ internal static Range GetSpansStartingInSpan( length = ~length; } - return new Range(start, start + length); + while (start > 0 && startPositionComparer(spans[start - 1], spanStart) == 0) + { + start--; + } + + var end = start + length; + while (end < spans.Length && startPositionComparer(spans[end], spanStart) == 0) + { + end++; + } + + return new Range(start, end); } } } diff --git a/src/Features/Core/Portable/EditAndContinue/DeclarationBody.cs b/src/Features/Core/Portable/EditAndContinue/DeclarationBody.cs index 8f6355732a8bf..e9018282582a8 100644 --- a/src/Features/Core/Portable/EditAndContinue/DeclarationBody.cs +++ b/src/Features/Core/Portable/EditAndContinue/DeclarationBody.cs @@ -16,24 +16,39 @@ internal abstract class DeclarationBody : IEquatable /// /// Root nodes of the body. Descendant nodes of these roots include all nodes of the body and no nodes that do not belong to the body. - /// Note: descendant nodes may include some tokens that are not part of the body. + /// Note: descendant nodes may include some tokens that are not part of the body and exclude some tokens that are. /// public abstract OneOrMany RootNodes { get; } + /// + /// that includes all active tokens () + /// and its span covers the entire . + /// May include descendant nodes or tokens that do not belong to the body. + /// + public abstract SyntaxNode EncompassingAncestor { get; } + public abstract StateMachineInfo GetStateMachineInfo(); /// /// Computes a statement-level syntax tree match of this body with . - /// TODO: Consider returning that includes matches of all roots of the body. - /// might not be needed if we do so. /// - public abstract Match ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches); + public virtual BidirectionalMap ComputeMatch(DeclarationBody newBody, IEnumerable>? knownMatches) + { + var primaryMatch = ComputeSingleRootMatch(newBody, knownMatches); + return (primaryMatch != null) ? BidirectionalMap.FromMatch(primaryMatch) : BidirectionalMap.Empty; + } + + /// + /// If both this body and have single roots, computes a statement-level syntax tree match rooted in these roots. + /// Otherwise, returns null (e.g. a primary constructor with implicit initializer does not have any body to match). + /// + public abstract Match? ComputeSingleRootMatch(DeclarationBody newBody, IEnumerable>? knownMatches); /// /// Matches old active statement node to new active statement node for nodes that are not accounted for in the body match or do not match but should be mapped /// (e.g. constructor initializers, opening brace of block body to expression body, etc.). /// - public abstract bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement); + public abstract bool TryMatchActiveStatement(DeclarationBody newBody, SyntaxNode oldStatement, ref int statementPart, [NotNullWhen(true)] out SyntaxNode? newStatement); public bool Equals(DeclarationBody? other) => ReferenceEquals(this, other) || diff --git a/src/Features/Core/Portable/EditAndContinue/MemberBody.cs b/src/Features/Core/Portable/EditAndContinue/MemberBody.cs index d67898ae1f45c..fee58164a1df6 100644 --- a/src/Features/Core/Portable/EditAndContinue/MemberBody.cs +++ b/src/Features/Core/Portable/EditAndContinue/MemberBody.cs @@ -11,15 +11,21 @@ namespace Microsoft.CodeAnalysis.EditAndContinue; internal abstract class MemberBody : DeclarationBody { /// - /// A span that contains all possible breakpoint spans of - /// and no breakpoint spans that do not belong to the . + /// A minimal span that contains all possible breakpoint spans of . /// - public abstract ActiveStatementEnvelope Envelope { get; } + public abstract TextSpan Envelope { get; } /// - /// that includes all active tokens () and its span covers the entire . + /// True if belongs to the . /// - public abstract SyntaxNode EncompassingAncestor { get; } + public bool ContainsActiveStatementSpan(TextSpan span) + => Envelope.Contains(span) && !IsExcludedActiveStatementSpanWithinEnvelope(span); + + /// + /// True for within does not belong to the body. + /// + public virtual bool IsExcludedActiveStatementSpanWithinEnvelope(TextSpan span) + => false; /// /// All tokens of the body that may be part of an active statement. @@ -27,7 +33,7 @@ internal abstract class MemberBody : DeclarationBody public abstract IEnumerable? GetActiveTokens(); /// - /// Finds am active statement at given span within this body and the corresponding partner statement in + /// Finds an active statement at given span within this body and the corresponding partner statement in /// , if specified. Only called with when /// the body does not have any non-trivial changes and thus the correpsonding active statement is always found in the partner body. /// @@ -41,4 +47,24 @@ public SyntaxNode FindStatement(TextSpan span, out int statementPart) /// If the body is a field/property initializer analyzes the initializer expression only. /// public abstract ImmutableArray GetCapturedVariables(SemanticModel model); + + public IEnumerable GetOverlappingActiveStatements(ImmutableArray statements) + { + var envelope = Envelope; + + var range = ActiveStatementsMap.GetSpansStartingInSpan( + envelope.Start, + envelope.End, + statements, + startPositionComparer: (x, y) => x.UnmappedSpan.Start.CompareTo(y)); + + for (var i = range.Start.Value; i < range.End.Value; i++) + { + var span = statements[i].UnmappedSpan; + if (envelope.Contains(span) && !IsExcludedActiveStatementSpanWithinEnvelope(span)) + { + yield return i; + } + } + } } diff --git a/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs b/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs index cd57435749889..9a3dee7865ca7 100644 --- a/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs +++ b/src/Features/Core/Portable/EditAndContinue/Utilities/BidirectionalMap.cs @@ -14,6 +14,8 @@ internal readonly struct BidirectionalMap public readonly IReadOnlyDictionary Forward; public readonly IReadOnlyDictionary Reverse; + public static readonly BidirectionalMap Empty = new(SpecializedCollections.EmptyReadOnlyDictionary(), SpecializedCollections.EmptyReadOnlyDictionary()); + public BidirectionalMap(IReadOnlyDictionary forward, IReadOnlyDictionary reverse) { Contract.ThrowIfFalse(forward.Count == reverse.Count); diff --git a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs index 4b0c2bd53c749..90da461e42157 100644 --- a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs +++ b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; @@ -152,8 +153,17 @@ public static ManagedHotReloadDiagnostic ToHotReloadDiagnostic(this DiagnosticDa public static bool IsSynthesized(this ISymbol symbol) => symbol.IsImplicitlyDeclared || symbol.IsSynthesizedAutoProperty(); - public static bool IsSynthesizedAutoProperty(this ISymbol property) - => property is IPropertySymbol { GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }; + public static bool IsSynthesizedAutoProperty(this IPropertySymbol property) + => property is { GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true }; + + public static bool IsSynthesizedAutoProperty(this ISymbol symbol) + => symbol is IPropertySymbol property && property.IsSynthesizedAutoProperty(); + + public static bool IsAutoProperty(this ISymbol symbol) + => symbol is IPropertySymbol property && IsAutoProperty(property); + + public static bool IsAutoProperty(this IPropertySymbol property) + => property.ContainingType.GetMembers().Any(static (member, property) => member is IFieldSymbol field && field.AssociatedSymbol == property, property); public static bool HasSynthesizedDefaultConstructor(this INamedTypeSymbol type) => !type.InstanceConstructors.Any(static c => !(c.Parameters is [] || c.ContainingType.IsRecord && c.IsCopyConstructor())); diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/BreakpointSpans.vb b/src/Features/VisualBasic/Portable/EditAndContinue/BreakpointSpans.vb index a8fb76ddbc6c0..d73e78ade68e4 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/BreakpointSpans.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/BreakpointSpans.vb @@ -28,7 +28,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End If Dim root = tree.GetRoot(cancellationToken) - Return TryGetEnclosingBreakpointSpan(root, position, minLength:=0, breakpointSpan) + Return TryGetClosestBreakpointSpan(root, position, minLength:=0, breakpointSpan) End Function Private Function IsBlank(line As TextLine) As Boolean @@ -55,7 +55,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue ''' can be used to disambiguate between them. ''' The inner-most available span whose length is at least is returned. ''' - Public Function TryGetEnclosingBreakpointSpan(root As SyntaxNode, position As Integer, minLength As Integer, ByRef span As TextSpan) As Boolean + Public Function TryGetClosestBreakpointSpan(root As SyntaxNode, position As Integer, minLength As Integer, ByRef span As TextSpan) As Boolean Dim node = root.FindToken(position).Parent Dim candidate As TextSpan? = Nothing diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldOrPropertyDeclarationBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldOrPropertyDeclarationBody.vb index f565baa7bdc6f..72c10a70d23b4 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldOrPropertyDeclarationBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldOrPropertyDeclarationBody.vb @@ -54,21 +54,22 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End Get End Property - Public NotOverridable Overrides Function ComputeMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Match(Of SyntaxNode) + Public NotOverridable Overrides Function ComputeSingleRootMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Match(Of SyntaxNode) Dim newFieldBody = DirectCast(newBody, FieldOrPropertyDeclarationBody) If TypeOf OtherActiveStatementContainer Is ExpressionSyntax Then ' Dim a = ' Dim a As ' Dim a, b, c As - Return New SyntaxComparer( - OtherActiveStatementContainer.Parent, - newFieldBody.OtherActiveStatementContainer.Parent, - {OtherActiveStatementContainer}, - {newFieldBody.OtherActiveStatementContainer}, - matchingLambdas:=False, - compareStatementSyntax:=True). - ComputeMatch(OtherActiveStatementContainer.Parent, newFieldBody.OtherActiveStatementContainer.Parent, knownMatches) + Dim comparer = New SyntaxComparer( + OtherActiveStatementContainer.Parent, + newFieldBody.OtherActiveStatementContainer.Parent, + {OtherActiveStatementContainer}, + {newFieldBody.OtherActiveStatementContainer}, + matchingLambdas:=False, + compareStatementSyntax:=True) + + Return comparer.ComputeMatch(OtherActiveStatementContainer.Parent, newFieldBody.OtherActiveStatementContainer.Parent, knownMatches) End If ' Method, accessor, operator, etc. bodies are represented by the declaring block, which is also the root. @@ -100,7 +101,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue statementPart) End Function - Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean + Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, ByRef statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean If oldStatement Is InitializerActiveStatement Then newStatement = DirectCast(newBody, FieldOrPropertyDeclarationBody).InitializerActiveStatement Return True @@ -116,7 +117,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End Get End Property - Public Overrides ReadOnly Property Envelope As ActiveStatementEnvelope + Public Overrides ReadOnly Property Envelope As TextSpan Get Return InitializerActiveStatement.Span End Get diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldWithMultipleAsNewClauseDeclarationBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldWithMultipleAsNewClauseDeclarationBody.vb index f7782e679f3e7..7348a26a439d0 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldWithMultipleAsNewClauseDeclarationBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/FieldWithMultipleAsNewClauseDeclarationBody.vb @@ -32,14 +32,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End Get End Property - Public Overrides ReadOnly Property Envelope As ActiveStatementEnvelope + Public Overrides ReadOnly Property Envelope As TextSpan Get - Return New ActiveStatementEnvelope( - Span:=TextSpan.FromBounds(_modifedIdentifier.Span.Start, OtherActiveStatementContainer.Span.End), - Hole:=TextSpan.FromBounds(_modifedIdentifier.Span.End, OtherActiveStatementContainer.Span.Start)) + Return TextSpan.FromBounds(_modifedIdentifier.Span.Start, OtherActiveStatementContainer.Span.End) End Get End Property + Public Overrides Function IsExcludedActiveStatementSpanWithinEnvelope(span As TextSpan) As Boolean + ' exclude spans in between the identifier and AsNew clause + Return TextSpan.FromBounds(_modifedIdentifier.Span.End, OtherActiveStatementContainer.Span.Start).Contains(span) + End Function + Public Overrides ReadOnly Property EncompassingAncestor As SyntaxNode Get Return _modifedIdentifier.Parent diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/MethodBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/MethodBody.vb index 8bf66f3a2ef25..8ba44cef04ad3 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/MethodBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/MethodBody.vb @@ -31,7 +31,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Return VisualBasicEditAndContinueAnalyzer.GetStateMachineInfo(Node) End Function - Public Overrides Function ComputeMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Match(Of SyntaxNode) + Public Overrides Function ComputeSingleRootMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Match(Of SyntaxNode) Return SyntaxComparer.Statement.ComputeMatch(Node, DirectCast(newBody, MethodBody).Node, knownMatches) End Function @@ -44,7 +44,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue statementPart) End Function - Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean + Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, ByRef statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean newStatement = Nothing Return False End Function diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithInitializerDeclarationBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithInitializerDeclarationBody.vb index a1e423779146d..5041026e7e375 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithInitializerDeclarationBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithInitializerDeclarationBody.vb @@ -32,7 +32,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End Get End Property - Public Overrides ReadOnly Property Envelope As ActiveStatementEnvelope + Public Overrides ReadOnly Property Envelope As TextSpan Get Return TextSpan.FromBounds(PropertyStatement.Identifier.Span.Start, PropertyStatement.Initializer.Span.End) End Get diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithNewClauseDeclarationBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithNewClauseDeclarationBody.vb index 1119fef578542..63a8798c4c69a 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithNewClauseDeclarationBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/PropertyWithNewClauseDeclarationBody.vb @@ -32,7 +32,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue End Get End Property - Public Overrides ReadOnly Property Envelope As ActiveStatementEnvelope + Public Overrides ReadOnly Property Envelope As TextSpan Get Return TextSpan.FromBounds(PropertyStatement.Identifier.Span.Start, PropertyStatement.AsClause.Span.End) End Get diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/VisualBasicLambdaBody.vb b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/VisualBasicLambdaBody.vb index 9d157562b8bcd..b73ec11c59e1a 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/VisualBasicLambdaBody.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/DeclarationBody/VisualBasicLambdaBody.vb @@ -4,6 +4,7 @@ Imports System.Collections.Immutable Imports System.Diagnostics.CodeAnalysis +Imports Microsoft.CodeAnalysis.Differencing Imports Microsoft.CodeAnalysis.EditAndContinue Imports Microsoft.CodeAnalysis.VisualBasic.Syntax @@ -26,11 +27,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Public Overrides ReadOnly Property RootNodes As OneOrMany(Of SyntaxNode) Get - If TypeOf _node.Parent Is LambdaExpressionSyntax Then - Return OneOrMany.Create(_node.Parent) - Else - Return OneOrMany.Create(_node) - End If + Return OneOrMany.Create(EncompassingAncestor) + End Get + End Property + + Public Overrides ReadOnly Property EncompassingAncestor As SyntaxNode + Get + Return If(TypeOf _node.Parent Is LambdaExpressionSyntax, _node.Parent, _node) End Get End Property @@ -54,21 +57,21 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Return GetExpressionsAndStatements().SequenceEqual(other.GetExpressionsAndStatements(), AddressOf SyntaxFactory.AreEquivalent) End Function - Public Overrides Function ComputeMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Differencing.Match(Of SyntaxNode) + Public Overrides Function ComputeSingleRootMatch(newBody As DeclarationBody, knownMatches As IEnumerable(Of KeyValuePair(Of SyntaxNode, SyntaxNode))) As Match(Of SyntaxNode) Dim newLambdaBody = DirectCast(newBody, VisualBasicLambdaBody) If TypeOf _node.Parent Is LambdaExpressionSyntax Then ' The root is a single/multi line sub/function lambda. - Return New SyntaxComparer(_node.Parent, newLambdaBody._node.Parent, _node.Parent.ChildNodes(), newLambdaBody._node.Parent.ChildNodes(), matchingLambdas:=True, compareStatementSyntax:=True). - ComputeMatch(_node.Parent, newLambdaBody._node.Parent, knownMatches) + Dim comparer = New SyntaxComparer(_node.Parent, newLambdaBody._node.Parent, _node.Parent.ChildNodes(), newLambdaBody._node.Parent.ChildNodes(), matchingLambdas:=True, compareStatementSyntax:=True) + Return comparer.ComputeMatch(_node.Parent, newLambdaBody._node.Parent, knownMatches) Else ' Queries: The root is a query clause, the body is the expression. - Return New SyntaxComparer(_node.Parent, newLambdaBody._node.Parent, {_node}, {newLambdaBody._node}, matchingLambdas:=False, compareStatementSyntax:=True). - ComputeMatch(_node.Parent, newLambdaBody._node.Parent, knownMatches) + Dim comparer = New SyntaxComparer(_node.Parent, newLambdaBody._node.Parent, {_node}, {newLambdaBody._node}, matchingLambdas:=False, compareStatementSyntax:=True) + Return comparer.ComputeMatch(_node.Parent, newLambdaBody._node.Parent, knownMatches) End If End Function - Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean + Public Overrides Function TryMatchActiveStatement(newBody As DeclarationBody, oldStatement As SyntaxNode, ByRef statementPart As Integer, ByRef newStatement As SyntaxNode) As Boolean Dim newLambdaBody = DirectCast(newBody, VisualBasicLambdaBody) If TypeOf _node.Parent Is LambdaExpressionSyntax Then diff --git a/src/Features/VisualBasic/Portable/EditAndContinue/VisualBasicEditAndContinueAnalyzer.vb b/src/Features/VisualBasic/Portable/EditAndContinue/VisualBasicEditAndContinueAnalyzer.vb index c8ff01a5d92a6..10ed383f07c1d 100644 --- a/src/Features/VisualBasic/Portable/EditAndContinue/VisualBasicEditAndContinueAnalyzer.vb +++ b/src/Features/VisualBasic/Portable/EditAndContinue/VisualBasicEditAndContinueAnalyzer.vb @@ -39,7 +39,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue #Region "Syntax Analysis" - Friend Overrides Function TryFindMemberDeclaration(rootOpt As SyntaxNode, node As SyntaxNode, ByRef declarations As OneOrMany(Of SyntaxNode)) As Boolean + Friend Overrides Function TryFindMemberDeclaration(rootOpt As SyntaxNode, node As SyntaxNode, activeSpan As TextSpan, ByRef declarations As OneOrMany(Of SyntaxNode)) As Boolean Dim current = node While current IsNot rootOpt Select Case current.Kind @@ -97,11 +97,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue ''' - for fields with array initializer, e.g. "Dim a(1) As Integer". ''' A null reference otherwise. ''' - Friend Overrides Function TryGetDeclarationBody(node As SyntaxNode) As MemberBody + Friend Overrides Function TryGetDeclarationBody(node As SyntaxNode, symbol As ISymbol) As MemberBody Return SyntaxUtilities.TryGetDeclarationBody(node) End Function - Friend Overrides Function IsDeclarationWithSharedBody(declaration As SyntaxNode) As Boolean + Friend Overrides Function IsDeclarationWithSharedBody(declaration As SyntaxNode, member As ISymbol) As Boolean If declaration.Kind = SyntaxKind.ModifiedIdentifier AndAlso declaration.Parent.Kind = SyntaxKind.VariableDeclarator Then Dim variableDeclarator = CType(declaration.Parent, VariableDeclaratorSyntax) Return variableDeclarator.Names.Count > 1 AndAlso variableDeclarator.Initializer IsNot Nothing OrElse SyntaxUtilities.HasAsNewClause(variableDeclarator) @@ -110,10 +110,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Return False End Function - Friend Overrides Function HasParameterClosureScope(member As ISymbol) As Boolean - Return False - End Function - Protected Overrides Function GetVariableUseSites(roots As IEnumerable(Of SyntaxNode), localOrParameter As ISymbol, model As SemanticModel, cancellationToken As CancellationToken) As IEnumerable(Of SyntaxNode) Debug.Assert(TypeOf localOrParameter Is IParameterSymbol OrElse TypeOf localOrParameter Is ILocalSymbol OrElse TypeOf localOrParameter Is IRangeVariableSymbol) @@ -129,37 +125,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Select node End Function - Protected Overrides Function GetEncompassingAncestorImpl(bodyOrMatchRoot As SyntaxNode) As SyntaxNode - ' AsNewClause is a match root for field/property As New initializer - ' EqualsClause is a match root for field/property initializer - If bodyOrMatchRoot.IsKind(SyntaxKind.AsNewClause) OrElse bodyOrMatchRoot.IsKind(SyntaxKind.EqualsValue) Then - Debug.Assert(bodyOrMatchRoot.Parent.IsKind(SyntaxKind.VariableDeclarator) OrElse - bodyOrMatchRoot.Parent.IsKind(SyntaxKind.PropertyStatement)) - Return bodyOrMatchRoot.Parent - End If - - ' ArgumentList is a match root for an array initialized field - If bodyOrMatchRoot.IsKind(SyntaxKind.ArgumentList) Then - Debug.Assert(bodyOrMatchRoot.Parent.IsKind(SyntaxKind.ModifiedIdentifier)) - Return bodyOrMatchRoot.Parent - End If - - ' The following active nodes are outside of the initializer body, - ' we need to return a node that encompasses them. - ' Dim [|a = <>|] - ' Dim [|a As Integer = <>|] - ' Dim [|a As <>|] - ' Dim [|a|], [|b|], [|c|] As <> - ' Property [|P As Integer = <>|] - ' Property [|P As <>|] - If bodyOrMatchRoot.Parent.IsKind(SyntaxKind.AsNewClause) OrElse - bodyOrMatchRoot.Parent.IsKind(SyntaxKind.EqualsValue) Then - Return bodyOrMatchRoot.Parent.Parent - End If - - Return bodyOrMatchRoot - End Function - Friend Shared Function FindStatementAndPartner( span As TextSpan, body As SyntaxNode, @@ -221,8 +186,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Return LambdaUtilities.IsClosureScope(node) End Function - Protected Overrides Function FindEnclosingLambdaBody(root As SyntaxNode, node As SyntaxNode) As LambdaBody - While node IsNot root And node IsNot Nothing + Protected Overrides Function FindEnclosingLambdaBody(encompassingAncestor As SyntaxNode, node As SyntaxNode) As LambdaBody + While node IsNot encompassingAncestor And node IsNot Nothing Dim body As SyntaxNode = Nothing If LambdaUtilities.IsLambdaBodyStatementOrExpression(node, body) Then Return SyntaxUtilities.CreateLambdaBody(body) @@ -358,12 +323,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Not node.IsKind(SyntaxKind.SingleLineSubLambdaExpression) End Function - Protected Overrides Function TryGetEnclosingBreakpointSpan(root As SyntaxNode, position As Integer, ByRef span As TextSpan) As Boolean - Return BreakpointSpans.TryGetEnclosingBreakpointSpan(root, position, minLength:=0, span) + Protected Overrides Function TryGetEnclosingBreakpointSpan(token As SyntaxToken, ByRef span As TextSpan) As Boolean + Return BreakpointSpans.TryGetClosestBreakpointSpan(token.Parent, token.SpanStart, minLength:=token.Span.Length, span) End Function Protected Overrides Function TryGetActiveSpan(node As SyntaxNode, statementPart As Integer, minLength As Integer, ByRef span As TextSpan) As Boolean - Return BreakpointSpans.TryGetEnclosingBreakpointSpan(node, node.SpanStart, minLength, span) + Return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, minLength, span) End Function Protected Overrides Iterator Function EnumerateNearStatements(statement As SyntaxNode) As IEnumerable(Of ValueTuple(Of SyntaxNode, Integer)) @@ -508,27 +473,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Return node.Parent.FirstAncestorOrSelf(Of TypeBlockSyntax)() ' TODO: EnbumBlock? End Function - Friend Overrides Function TryGetAssociatedMemberDeclaration(node As SyntaxNode, editKind As EditKind, ByRef declaration As SyntaxNode) As Boolean - If node.IsKind(SyntaxKind.Parameter, SyntaxKind.TypeParameter) Then - Contract.ThrowIfFalse(node.IsParentKind(SyntaxKind.ParameterList, SyntaxKind.TypeParameterList)) - declaration = node.Parent.Parent - Return True - End If - - ' We allow deleting event and property accessors, so don't associate them - If editKind <> EditKind.Delete AndAlso node.IsParentKind(SyntaxKind.PropertyBlock, SyntaxKind.EventBlock) Then - declaration = node.Parent - Return True - End If - - declaration = Nothing - Return False - End Function - - Friend Overrides Function HasBackingField(propertyDeclaration As SyntaxNode) As Boolean - Return SyntaxUtilities.HasBackingField(propertyDeclaration) - End Function - Friend Overrides Function IsDeclarationWithInitializer(declaration As SyntaxNode) As Boolean Select Case declaration.Kind Case SyntaxKind.ModifiedIdentifier @@ -577,7 +521,20 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue ''' is represented by its declaration statement. ''' Protected Overrides Function GetSymbolDeclarationSyntax(symbol As ISymbol, selector As Func(Of ImmutableArray(Of SyntaxReference), SyntaxReference), cancellationToken As CancellationToken) As SyntaxNode - Dim syntax = selector(symbol.DeclaringSyntaxReferences).GetSyntax(cancellationToken) + ' Invoke method of a delegate type doesn't have DeclaringSyntaxReferences + Dim syntaxRefs As ImmutableArray(Of SyntaxReference) + + If symbol Is symbol.ContainingType?.DelegateInvokeMethod Then + syntaxRefs = symbol.ContainingType.DeclaringSyntaxReferences + If syntaxRefs.IsEmpty Then + Dim parameter = DirectCast(symbol, IMethodSymbol).Parameters.First() + Return parameter.DeclaringSyntaxReferences.Single().GetSyntax(cancellationToken).Parent.Parent + End If + Else + syntaxRefs = symbol.DeclaringSyntaxReferences + End If + + Dim syntax = selector(syntaxRefs).GetSyntax(cancellationToken) Dim parent = syntax.Parent Select Case syntax.Kind @@ -2016,10 +1973,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue #Region "Exception Handling Rude Edits" - Protected Overrides Function GetExceptionHandlingAncestors(node As SyntaxNode, isNonLeaf As Boolean) As List(Of SyntaxNode) + Protected Overrides Function GetExceptionHandlingAncestors(node As SyntaxNode, root As SyntaxNode, isNonLeaf As Boolean) As List(Of SyntaxNode) Dim result = New List(Of SyntaxNode)() - While node IsNot Nothing + While node IsNot root Dim kind = node.Kind Select Case kind @@ -2045,6 +2002,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue Exit While End If + Debug.Assert(node.Parent IsNot Nothing) node = node.Parent End While @@ -2244,40 +2202,46 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue #Region "Rude Edits around Active Statement" Friend Overrides Sub ReportOtherRudeEditsAroundActiveStatement(diagnostics As ArrayBuilder(Of RudeEditDiagnostic), - match As Match(Of SyntaxNode), + forwardMap As IReadOnlyDictionary(Of SyntaxNode, SyntaxNode), oldActiveStatement As SyntaxNode, + oldBody As DeclarationBody, newActiveStatement As SyntaxNode, + newBody As DeclarationBody, isNonLeaf As Boolean) - Dim onErrorOrResumeStatement = FindOnErrorOrResumeStatement(match.NewRoot) + Dim onErrorOrResumeStatement = FindOnErrorOrResumeStatement(newBody) If onErrorOrResumeStatement IsNot Nothing Then AddAroundActiveStatementRudeDiagnostic(diagnostics, oldActiveStatement, onErrorOrResumeStatement, newActiveStatement.Span) End If - ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, match, oldActiveStatement, newActiveStatement) + ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, forwardMap, oldActiveStatement, oldBody.EncompassingAncestor, newActiveStatement, newBody.EncompassingAncestor) End Sub - Private Shared Function FindOnErrorOrResumeStatement(newDeclarationOrBody As SyntaxNode) As SyntaxNode - For Each node In newDeclarationOrBody.DescendantNodes(AddressOf ChildrenCompiledInBody) - Select Case node.Kind - Case SyntaxKind.OnErrorGoToLabelStatement, + Private Shared Function FindOnErrorOrResumeStatement(newBody As DeclarationBody) As SyntaxNode + For Each newRoot In newBody.RootNodes + For Each node In newRoot.DescendantNodes(AddressOf ChildrenCompiledInBody) + Select Case node.Kind + Case SyntaxKind.OnErrorGoToLabelStatement, SyntaxKind.OnErrorGoToMinusOneStatement, SyntaxKind.OnErrorGoToZeroStatement, SyntaxKind.OnErrorResumeNextStatement, SyntaxKind.ResumeStatement, SyntaxKind.ResumeNextStatement, SyntaxKind.ResumeLabelStatement - Return node - End Select + Return node + End Select + Next Next Return Nothing End Function Private Sub ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics As ArrayBuilder(Of RudeEditDiagnostic), - match As Match(Of SyntaxNode), + forwardMap As IReadOnlyDictionary(Of SyntaxNode, SyntaxNode), oldActiveStatement As SyntaxNode, - newActiveStatement As SyntaxNode) + oldEncompassingAncestor As SyntaxNode, + newActiveStatement As SyntaxNode, + newEncompassingAncestor As SyntaxNode) ' Rude Edits for Using/SyncLock/With/ForEach statements that are added/updated around an active statement. ' Although such changes are technically possible, they might lead to confusion since @@ -2288,19 +2252,19 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue ' ' Unlike exception regions matching where we use LCS, we allow reordering of the statements. - ReportUnmatchedStatements(Of SyncLockBlockSyntax)(diagnostics, match, Function(node) node.IsKind(SyntaxKind.SyncLockBlock), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements(Of SyncLockBlockSyntax)(diagnostics, forwardMap, Function(node) node.IsKind(SyntaxKind.SyncLockBlock), oldActiveStatement, oldEncompassingAncestor, newActiveStatement, newEncompassingAncestor, areEquivalent:=Function(n1, n2) AreEquivalentIgnoringLambdaBodies(n1.SyncLockStatement.Expression, n2.SyncLockStatement.Expression), areSimilar:=Nothing) - ReportUnmatchedStatements(Of WithBlockSyntax)(diagnostics, match, Function(node) node.IsKind(SyntaxKind.WithBlock), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements(Of WithBlockSyntax)(diagnostics, forwardMap, Function(node) node.IsKind(SyntaxKind.WithBlock), oldActiveStatement, oldEncompassingAncestor, newActiveStatement, newEncompassingAncestor, areEquivalent:=Function(n1, n2) AreEquivalentIgnoringLambdaBodies(n1.WithStatement.Expression, n2.WithStatement.Expression), areSimilar:=Nothing) - ReportUnmatchedStatements(Of UsingBlockSyntax)(diagnostics, match, Function(node) node.IsKind(SyntaxKind.UsingBlock), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements(Of UsingBlockSyntax)(diagnostics, forwardMap, Function(node) node.IsKind(SyntaxKind.UsingBlock), oldActiveStatement, oldEncompassingAncestor, newActiveStatement, newEncompassingAncestor, areEquivalent:=Function(n1, n2) AreEquivalentIgnoringLambdaBodies(n1.UsingStatement.Expression, n2.UsingStatement.Expression), areSimilar:=Nothing) - ReportUnmatchedStatements(Of ForOrForEachBlockSyntax)(diagnostics, match, Function(node) node.IsKind(SyntaxKind.ForEachBlock), oldActiveStatement, newActiveStatement, + ReportUnmatchedStatements(Of ForOrForEachBlockSyntax)(diagnostics, forwardMap, Function(node) node.IsKind(SyntaxKind.ForEachBlock), oldActiveStatement, oldEncompassingAncestor, newActiveStatement, newEncompassingAncestor, areEquivalent:=Function(n1, n2) AreEquivalentIgnoringLambdaBodies(n1.ForOrForEachStatement, n2.ForOrForEachStatement), areSimilar:=Function(n1, n2) AreEquivalentIgnoringLambdaBodies(DirectCast(n1.ForOrForEachStatement, ForEachStatementSyntax).ControlVariable, DirectCast(n2.ForOrForEachStatement, ForEachStatementSyntax).ControlVariable)) diff --git a/src/Workspaces/Core/Portable/Differencing/Edit.cs b/src/Workspaces/Core/Portable/Differencing/Edit.cs index b8fb5caef62f9..bcdc07bc2e73a 100644 --- a/src/Workspaces/Core/Portable/Differencing/Edit.cs +++ b/src/Workspaces/Core/Portable/Differencing/Edit.cs @@ -27,8 +27,9 @@ internal Edit(EditKind kind, TreeComparer comparer, TNode oldNode, TNode Debug.Assert((oldNode == null || oldNode.Equals(null)) == (kind == EditKind.Insert)); Debug.Assert((newNode == null || newNode.Equals(null)) == (kind == EditKind.Delete)); - Debug.Assert((oldNode == null || oldNode.Equals(null)) || - (newNode == null || newNode.Equals(null)) || + Debug.Assert(comparer == null || + oldNode == null || oldNode.Equals(null) || + newNode == null || newNode.Equals(null) || !comparer.TreesEqual(oldNode, newNode)); _comparer = comparer; @@ -113,6 +114,6 @@ internal string GetDebuggerDisplay() } private string DisplayPosition(TNode node) - => "@" + _comparer.GetSpan(node).Start; + => (_comparer != null) ? "@" + _comparer.GetSpan(node).Start : ""; } } diff --git a/src/Workspaces/Core/Portable/Differencing/MapBasedLongestCommonSubsequence.cs b/src/Workspaces/Core/Portable/Differencing/MapBasedLongestCommonSubsequence.cs new file mode 100644 index 0000000000000..e8c454aa83f9b --- /dev/null +++ b/src/Workspaces/Core/Portable/Differencing/MapBasedLongestCommonSubsequence.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Differencing; + +internal sealed class MapBasedLongestCommonSubsequence(IReadOnlyDictionary map) : LongestCommonSubsequence> + where TNode : notnull +{ + protected override bool ItemsEqual(IReadOnlyList oldSequence, int oldIndex, IReadOnlyList newSequence, int newIndex) + => map.TryGetValue(oldSequence[oldIndex], out var newNode) && newNode.Equals(newSequence[newIndex]); + + internal IEnumerable> GetEdits(IReadOnlyList oldNodes, IReadOnlyList newNodes, TreeComparer? treeComparer = null) + { + foreach (var edit in GetEdits(oldNodes, oldNodes.Count, newNodes, newNodes.Count)) + { + yield return new Edit(edit.Kind, treeComparer, + edit.OldIndex >= 0 ? oldNodes[edit.OldIndex] : default!, + edit.NewIndex >= 0 ? newNodes[edit.NewIndex] : default!); + } + } +}