diff --git a/src/Compilers/CSharp/Portable/Emitter/EditAndContinue/EmitHelpers.cs b/src/Compilers/CSharp/Portable/Emitter/EditAndContinue/EmitHelpers.cs index 7e214685964fa..dac5f61d8977c 100644 --- a/src/Compilers/CSharp/Portable/Emitter/EditAndContinue/EmitHelpers.cs +++ b/src/Compilers/CSharp/Portable/Emitter/EditAndContinue/EmitHelpers.cs @@ -198,10 +198,10 @@ internal static class EmitHelpers currentSynthesizedMembers, currentDeletedMembers); - var mappedSynthesizedMembers = matcher.MapSynthesizedMembers(previousGeneration.SynthesizedMembers, currentSynthesizedMembers); + var mappedSynthesizedMembers = matcher.MapSynthesizedOrDeletedMembers(previousGeneration.SynthesizedMembers, currentSynthesizedMembers, isDeletedMemberMapping: false); // Deleted members are mapped the same way as synthesized members, so we can just call the same method. - var mappedDeletedMembers = matcher.MapSynthesizedMembers(previousGeneration.DeletedMembers, currentDeletedMembers); + var mappedDeletedMembers = matcher.MapSynthesizedOrDeletedMembers(previousGeneration.DeletedMembers, currentDeletedMembers, isDeletedMemberMapping: true); // TODO: can we reuse some data from the previous matcher? var matcherWithAllSynthesizedMembers = new CSharpSymbolMatcher( diff --git a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.GenerationVerifier.cs b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.GenerationVerifier.cs index 8572f285c8403..edb5586b3e34d 100644 --- a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.GenerationVerifier.cs +++ b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.GenerationVerifier.cs @@ -65,6 +65,12 @@ internal void VerifyPropertyDefNames(params string[] expected) AssertEx.Equal(expected, actual, message: GetAssertMessage("PropertyDefs don't match")); } + internal void VerifyDeletedMembers(params string[] expected) + { + var actual = _generationInfo.Baseline.DeletedMembers.Select(e => e.Key.ToString() + ": {" + string.Join(", ", e.Value.Select(v => v.Name)) + "}"); + AssertEx.SetEqual(expected, actual, itemSeparator: ",\r\n", itemInspector: s => $"\"{s}\""); + } + internal void VerifyTableSize(TableIndex table, int expected) { AssertEx.AreEqual(expected, _metadataReader.GetTableRowCount(table), message: GetAssertMessage($"{table} table size doesnt't match")); diff --git a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.cs b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.cs index 04447d026e1da..45f43f7eace84 100644 --- a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.cs +++ b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTest.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection.Metadata; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Test.Utilities; @@ -112,7 +113,11 @@ private ImmutableArray GetSemanticEdits(SemanticEditDescription[] public void Dispose() { - Assert.True(_hasVerified, "No Verify call since the last AddGeneration call."); + // If the test has thrown an exception, or the test host has crashed, we don't want to assert here + // or we'll hide it, so we need to do this dodgy looking thing. + var isInException = Marshal.GetExceptionPointers() != IntPtr.Zero; + + Assert.True(isInException || _hasVerified, "No Verify call since the last AddGeneration call."); foreach (var disposable in _disposables) { disposable.Dispose(); diff --git a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.cs b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.cs index e4833685d7269..a8bde70403ff6 100644 --- a/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.cs +++ b/src/Compilers/CSharp/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.cs @@ -13624,7 +13624,7 @@ class C } """, edits: new[] { - Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M2"), newSymbolProvider: c=>c.GetMember("C")), + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M2"), newSymbolProvider: c => c.GetMember("C")), }, validator: g => { @@ -13679,7 +13679,7 @@ class C } """, edits: new[] { - Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c=>c.GetMember("C")), + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c => c.GetMember("C")), }, validator: g => { @@ -13781,7 +13781,7 @@ class C } """, edits: new[] { - Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c=>c.GetMember("C")), + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c => c.GetMember("C")), }, validator: g => { @@ -13958,7 +13958,7 @@ class C } """, edits: new[] { - Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c=>c.GetMember("C")), + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c => c.GetMember("C")), }, validator: g => { @@ -14039,6 +14039,63 @@ .maxstack 1 .Verify(); } + [Fact] + public void Method_Rename_Multiple() + { + using var test = new EditAndContinueTest(options: TestOptions.DebugDll, targetFramework: TargetFramework.NetStandard20) + .AddGeneration( + source: $$""" + class C + { + int M1() { return 0; } + } + """, + validator: g => + { + g.VerifyTypeDefNames("", "C"); + g.VerifyMethodDefNames("M1", ".ctor"); + g.VerifyMemberRefNames(/*CompilationRelaxationsAttribute.*/".ctor", /*RuntimeCompatibilityAttribute.*/".ctor", /*Object.*/".ctor", /*DebuggableAttribute*/".ctor"); + }); + + for (int i = 0; i < 10; i++) + { + test.AddGeneration( + source: @$" +class C +{{ + int M2() {{ return {i}; }} +}}", + edits: new[] { + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M1"), newSymbolProvider: c => c.GetMember("C")), + Edit(SemanticEditKind.Insert, symbolProvider: c => c.GetMember("C.M2")), + }, + validator: g => + { + g.VerifyTypeDefNames(); + g.VerifyMethodDefNames("M1", "M2"); + g.VerifyDeletedMembers("C: {M1}"); + }) + .AddGeneration( + source: @$" +class C +{{ + int M1() {{ return {i}; }} +}}", + edits: new[] { + Edit(SemanticEditKind.Delete, symbolProvider: c => c.GetMember("C.M2"), newSymbolProvider: c => c.GetMember("C")), + Edit(SemanticEditKind.Insert, symbolProvider: c => c.GetMember("C.M1")), + }, + validator: g => + { + g.VerifyTypeDefNames(); + g.VerifyMethodDefNames("M1", "M2"); + g.VerifyDeletedMembers("C: {M2}"); + }); + } + + test.Verify(); + } + [Fact] public void FileTypes_01() { diff --git a/src/Compilers/Core/Portable/Emit/EditAndContinue/DeltaMetadataWriter.cs b/src/Compilers/Core/Portable/Emit/EditAndContinue/DeltaMetadataWriter.cs index 3fe391f9f2513..e810afc05a460 100644 --- a/src/Compilers/Core/Portable/Emit/EditAndContinue/DeltaMetadataWriter.cs +++ b/src/Compilers/Core/Portable/Emit/EditAndContinue/DeltaMetadataWriter.cs @@ -190,6 +190,9 @@ internal EmitBaseline GetDelta(Compilation compilation, Guid encId, MetadataSize // otherwise members from the current compilation have already been merged into the baseline. var synthesizedMembers = (_previousGeneration.Ordinal == 0) ? module.GetAllSynthesizedMembers() : _previousGeneration.SynthesizedMembers; + Debug.Assert(module.EncSymbolChanges is not null); + var deletedMembers = (_previousGeneration.Ordinal == 0) ? module.EncSymbolChanges.GetAllDeletedMethods() : _previousGeneration.DeletedMembers; + var currentGenerationOrdinal = _previousGeneration.Ordinal + 1; var addedTypes = _typeDefs.GetAdded(); @@ -231,7 +234,7 @@ internal EmitBaseline GetDelta(Compilation compilation, Guid encId, MetadataSize anonymousDelegates: ((IPEDeltaAssemblyBuilder)module).GetAnonymousDelegates(), anonymousDelegatesWithFixedTypes: ((IPEDeltaAssemblyBuilder)module).GetAnonymousDelegatesWithFixedTypes(), synthesizedMembers: synthesizedMembers, - deletedMembers: _previousGeneration.DeletedMembers, + deletedMembers: deletedMembers, addedOrChangedMethods: AddRange(_previousGeneration.AddedOrChangedMethods, addedOrChangedMethodsByIndex), debugInformationProvider: _previousGeneration.DebugInformationProvider, localSignatureProvider: _previousGeneration.LocalSignatureProvider); diff --git a/src/Compilers/Core/Portable/Emit/EditAndContinue/SymbolMatcher.cs b/src/Compilers/Core/Portable/Emit/EditAndContinue/SymbolMatcher.cs index b497c0c700e83..3321441dfeff1 100644 --- a/src/Compilers/Core/Portable/Emit/EditAndContinue/SymbolMatcher.cs +++ b/src/Compilers/Core/Portable/Emit/EditAndContinue/SymbolMatcher.cs @@ -146,8 +146,8 @@ internal abstract class SymbolMatcher } /// - /// Merges synthesized members generated during lowering of the current compilation with aggregate synthesized members - /// from all previous source generations (gen >= 1). + /// Merges synthesized or deleted members generated during lowering, or emit, of the current compilation with aggregate + /// synthesized or deleted members from all previous source generations (gen >= 1). /// /// /// Suppose {S -> {A, B, D}, T -> {E, F}} are all synthesized members in previous generations, @@ -159,9 +159,10 @@ internal abstract class SymbolMatcher /// Then the resulting collection shall have the following entries: /// {S' -> {A', B', C, D}, U -> {G, H}, T -> {E, F}} /// - internal ImmutableDictionary> MapSynthesizedMembers( + internal ImmutableDictionary> MapSynthesizedOrDeletedMembers( ImmutableDictionary> previousMembers, - ImmutableDictionary> newMembers) + ImmutableDictionary> newMembers, + bool isDeletedMemberMapping) { // Note: we can't just return previous members if there are no new members, since we still need to map the symbols to the new compilation. @@ -174,11 +175,8 @@ internal abstract class SymbolMatcher synthesizedMembersBuilder.AddRange(newMembers); - foreach (var pair in previousMembers) + foreach (var (previousContainer, members) in previousMembers) { - var previousContainer = pair.Key; - var members = pair.Value; - var mappedContainer = MapDefinitionOrNamespace(previousContainer); if (mappedContainer == null) { @@ -207,7 +205,11 @@ internal abstract class SymbolMatcher // If the matcher found a member in the current compilation corresponding to previous memberDef, // then the member has to be synthesized and produced as a result of a method update // and thus already contained in newSynthesizedMembers. - Debug.Assert(newSynthesizedMembers.Contains(mappedMember)); + // However, because this method is also used to map deleted members, it's possible that a method + // could be renamed in the previous generation, and renamed back in this generation, which would + // mean it could be mapped, but isn't in the newSynthesizedMembers list, so we allow the flag to + // override this behaviour for deleted methods. + Debug.Assert(isDeletedMemberMapping || newSynthesizedMembers.Contains(mappedMember)); } else { diff --git a/src/Compilers/VisualBasic/Portable/Emit/EditAndContinue/EmitHelpers.vb b/src/Compilers/VisualBasic/Portable/Emit/EditAndContinue/EmitHelpers.vb index 314f80a0c169f..6fb3c556862c8 100644 --- a/src/Compilers/VisualBasic/Portable/Emit/EditAndContinue/EmitHelpers.vb +++ b/src/Compilers/VisualBasic/Portable/Emit/EditAndContinue/EmitHelpers.vb @@ -135,8 +135,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Emit currentSynthesizedMembers, currentDeletedMembers) - Dim mappedSynthesizedMembers = matcher.MapSynthesizedMembers(previousGeneration.SynthesizedMembers, currentSynthesizedMembers) - Dim mappedDeletedMembers = matcher.MapSynthesizedMembers(previousGeneration.DeletedMembers, currentDeletedMembers) + Dim mappedSynthesizedMembers = matcher.MapSynthesizedOrDeletedMembers(previousGeneration.SynthesizedMembers, currentSynthesizedMembers, isDeletedMemberMapping:=False) + Dim mappedDeletedMembers = matcher.MapSynthesizedOrDeletedMembers(previousGeneration.DeletedMembers, currentDeletedMembers, isDeletedMemberMapping:=True) ' TODO can we reuse some data from the previous matcher? Dim matcherWithAllSynthesizedMembers = New VisualBasicSymbolMatcher( diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs index e1745a4fcd352..a39d40d978bc6 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/ActiveStatementTests.Methods.cs @@ -52,8 +52,43 @@ static void Main(string[] args) new[] { edits }, new[] { - DocumentResults(active, - new[] { SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.Goo"), deletedSymbolContainerProvider: c => c.GetMember("C")) }) + DocumentResults( + active, + diagnostics: new[] { Diagnostic(RudeEditKind.Delete, "class C", DeletedSymbolDisplay(FeaturesResources.method, "Goo(int a)")) }) + }); + } + + [Fact] + public void Method_Rename_Leaf1() + { + var src1 = @" +class C +{ + static void Goo(int a) + { + Console.WriteLine(a); + } +}"; + var src2 = @" +class C +{ + static void Boo(int a) + { + Console.WriteLine(a); + } +} +"; + + var edits = GetTopEdits(src1, src2); + var active = GetActiveStatements(src1, src2); + + EditAndContinueValidation.VerifySemantics( + new[] { edits }, + new[] + { + DocumentResults( + active, + diagnostics: new[] {Diagnostic(RudeEditKind.UpdateAroundActiveStatement, "static void Boo(int a)", FeaturesResources.method) }) }); } diff --git a/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs index 3273276a5599f..eb907fcbb89ba 100644 --- a/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs +++ b/src/EditorFeatures/CSharpTest/EditAndContinue/TopLevelEditingTests.cs @@ -6424,34 +6424,27 @@ public void PartialNestedType_InsertDeleteAndChange() [Fact, WorkItem(51011, "https://github.com/dotnet/roslyn/issues/51011")] public void PartialMember_RenameInsertDelete() { - // The syntactic analysis for A and B produce rename edits since it doesn't see that the member was in fact moved. - // TODO: Currently, we don't even pass rename edits to semantic analysis where we could handle them as updates. - var srcA1 = "partial class C { void F1() {} }"; var srcB1 = "partial class C { void F2() {} }"; var srcA2 = "partial class C { void F2() {} }"; var srcB2 = "partial class C { void F1() {} }"; - // current outcome: - GetTopEdits(srcA1, srcA2).VerifySemanticDiagnostics(Diagnostic(RudeEditKind.Renamed, "void F2()", FeaturesResources.method)); - GetTopEdits(srcB1, srcB2).VerifySemanticDiagnostics(Diagnostic(RudeEditKind.Renamed, "void F1()", FeaturesResources.method)); + EditAndContinueValidation.VerifySemantics( + new[] { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, + new[] + { + DocumentResults(semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").GetMember("F2")), + }), - // correct outcome: - //EditAndContinueValidation.VerifySemantics( - // new[] { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, - // new[] - // { - // DocumentResults(semanticEdits: new[] - // { - // SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").GetMember("F2")), - // }), - - // DocumentResults( - // semanticEdits: new[] - // { - // SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").GetMember("F1")), - // }) - // }); + DocumentResults( + semanticEdits: new[] + { + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C").GetMember("F1")), + }) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } [Fact] @@ -7698,7 +7691,7 @@ static void Main(string[] b) } [Fact] - public void Method_Name_Update() + public void Method_Rename() { var src1 = @" class C @@ -7728,8 +7721,55 @@ static void EntryPoint(string[] args) edits.VerifyEdits(expectedEdit); + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.Main"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.EntryPoint")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + + edits.VerifySemanticDiagnostics( + new[] { Diagnostic(RudeEditKind.Renamed, "static void EntryPoint(string[] args)", FeaturesResources.method)}, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Theory] + [InlineData("virtual")] + [InlineData("abstract")] + [InlineData("override")] + public void Method_Rename_Modifiers(string modifier) + { + /* TODO: https://github.com/dotnet/roslyn/issues/59264 + + This should be a supported edit. Consider the following inheritance chain: + + public class C { public virtual void M() => Console.WriteLine("C"); } + public class D : C { public override void M() { base.M(); Console.WriteLine("D"); } } + public class E : D { public override void M() { base.M(); Console.WriteLine("E"); } } + + If D.M is deleted we expect E.M to print "C E" and not throw. + + */ + var src1 = $$""" + class C + { + {{modifier}} void goo() { } + } + """; + var src2 = $$""" + class C + { + {{modifier}} void boo() { } + } + """; + var edits = GetTopEdits(src1, src2); + + edits.VerifyEdits( + $"Update [{modifier} void goo() {{ }}]@16 -> [{modifier} void boo() {{ }}]@16"); + edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "static void EntryPoint(string[] args)", FeaturesResources.method)); + Diagnostic(RudeEditKind.Renamed, $"{modifier} void boo()", FeaturesResources.method)); } [Fact] @@ -9189,6 +9229,30 @@ public void OperatorWithBlockBody_ToExpressionBody() }); } + [Fact] + public void Operator_Rename() + { + var src1 = @" +class C +{ + public static C operator +(C c, C d) { return c; } +} +"; + var src2 = @" +class C +{ + public static C operator -(C c, C d) { return d; } +}"; + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics(ActiveStatementsDescription.Empty, new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.op_Addition"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.op_Subtraction")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + [Fact] public void OperatorReorder1() { @@ -13295,6 +13359,48 @@ public void Field_Type_Update_ReorderRemoveAdd() Diagnostic(RudeEditKind.Delete, "string G, F", DeletedSymbolDisplay(FeaturesResources.field, "H"))); } + [Fact] + public void Event_Rename1() + { + var src1 = "class C { event int E { remove { } add { } } }"; + var src2 = "class C { event int F { remove { } add { } } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.add_E"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.remove_E"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.add_F")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.remove_F")), + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + + edits.VerifySemanticDiagnostics( + new[] { Diagnostic(RudeEditKind.Renamed, "event int F", FeaturesResources.event_) }, + capabilities: EditAndContinueCapabilities.Baseline); + } + + [Fact] + public void Event_Rename2() + { + var src1 = "class C { event int E; }"; + var src2 = "class C { event int F; }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.add_E"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.remove_E"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.add_F")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.remove_F")), + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + [Fact] public void Field_Event_Reorder() { @@ -13374,8 +13480,13 @@ public void Property_ExpressionBody_Rename() var edits = GetTopEdits(src1, src2); - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "int Q", FeaturesResources.property_)); + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.get_Q")), + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } [Fact] @@ -13640,15 +13751,35 @@ public void Property_Rename1() var edits = GetTopEdits(src1, src2); + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.get_Q")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "int Q", FeaturesResources.property_)); + new[] { Diagnostic(RudeEditKind.Renamed, "int Q", FeaturesResources.property_)}, + capabilities: EditAndContinueCapabilities.Baseline); } [Fact] public void Property_Rename2() { - var src1 = "class C { int I.P { get { return 1; } } }"; - var src2 = "class C { int J.P { get { return 1; } } }"; + var interfaces = """ + interface I + { + int P { get; } + } + + interface J + { + int P { get; } + } + """; + var src1 = "class C { int I.P { get { return 1; } } } " + interfaces; + var src2 = "class C { int J.P { get { return 1; } } } " + interfaces; var edits = GetTopEdits(src1, src2); @@ -13656,6 +13787,44 @@ public void Property_Rename2() Diagnostic(RudeEditKind.Renamed, "int J.P", FeaturesResources.property_)); } + [Fact] + public void Property_Rename3() + { + var src1 = "class C { int P { get { return 1; } set { } } }"; + var src2 = "class C { int Q { get { return 1; } set { } } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + 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")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.get_Q")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.set_Q")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + + [Fact] + public void Property_Rename4() + { + var src1 = "class C { int P { get; set; } }"; + var src2 = "class C { int Q { get; set; } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + 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")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.get_Q")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.set_Q")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); + } + [Fact] public void Property_RenameAndUpdate() { @@ -13664,8 +13833,13 @@ public void Property_RenameAndUpdate() var edits = GetTopEdits(src1, src2); - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "int Q", FeaturesResources.property_)); + edits.VerifySemantics( + new[] + { + SemanticEdit(SemanticEditKind.Delete, c => c.GetMember("C.get_P"), deletedSymbolContainerProvider: c => c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.get_Q")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } [Fact] @@ -14926,8 +15100,19 @@ public void Indexer_ExpressionBodyToGetterAndSetterBlockBodies() [Fact] public void Indexer_Rename() { - var src1 = "class C { int I.this[int a] { get { return 1; } } }"; - var src2 = "class C { int J.this[int a] { get { return 1; } } }"; + var interfaces = """ + interface I + { + int this[int a] { get; } + } + + interface J + { + int this[int a] { get; } + } + """; + var src1 = "class C { int I.this[int a] { get { return 1; } } } " + interfaces; + var src2 = "class C { int J.this[int a] { get { return 1; } } } " + interfaces; var edits = GetTopEdits(src1, src2); @@ -15551,7 +15736,11 @@ public void Event_Insert() var edits = GetTopEdits(src1, src2); edits.VerifySemantics( - SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C").GetMember("E"))); + new[] + { + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C").GetMember("E")) + }, + capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } [Fact] @@ -15613,7 +15802,7 @@ class C var edits = GetTopEdits(src1, src2); - edits.VerifySemanticDiagnostics(); + edits.VerifySemanticDiagnostics(capabilities: EditAndContinueCapabilities.AddMethodToExistingType); } [Fact, WorkItem(17681, "https://github.com/dotnet/roslyn/issues/17681")] diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index 7dcca58964e7f..4d09fd38f4d62 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -1288,7 +1288,7 @@ public async Task Encodings() public async Task RudeEdits(bool breakMode) { var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; - var source2 = "class C1 { void M1() { System.Console.WriteLine(1); } }"; + var source2 = "class C1 { void M() { System.Console.WriteLine(1); } }"; var moduleId = Guid.NewGuid(); @@ -1310,7 +1310,7 @@ public async Task RudeEdits(bool breakMode) var document2 = solution.GetDocument(document1.Id); var diagnostics1 = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); - AssertEx.Equal(new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, + AssertEx.Equal(new[] { "ENC0021: " + string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.type_parameter) }, diagnostics1.Select(d => $"{d.Id}: {d.GetMessage()}")); // validate solution update status and emit: @@ -1337,7 +1337,7 @@ public async Task RudeEdits(bool breakMode) { "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=1|EmptySessionCount=0|HotReloadSessionCount=0|EmptyHotReloadSessionCount=2", "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=True|Capabilities=31|ProjectIdsWithAppliedChanges=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=20|RudeEditSyntaxKind=8875|RudeEditBlocking=True" + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=21|RudeEditSyntaxKind=8910|RudeEditBlocking=True" }, _telemetryLog); } else @@ -1346,7 +1346,7 @@ public async Task RudeEdits(bool breakMode) { "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=20|RudeEditSyntaxKind=8875|RudeEditBlocking=True" + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=21|RudeEditSyntaxKind=8910|RudeEditBlocking=True" }, _telemetryLog); } } @@ -1423,12 +1423,12 @@ public async Task DeferredApplyChangeWithActiveStatementRudeEdits() public async Task RudeEdits_SourceGenerators() { var sourceV1 = @" -/* GENERATE: class G { int X1 => 1; } */ +/* GENERATE: class G { int X1() => 1; } */ class C { int Y => 1; } "; var sourceV2 = @" -/* GENERATE: class G { int X2 => 1; } */ +/* GENERATE: class G { int X1() => 1; } */ class C { int Y => 2; } "; @@ -1448,7 +1448,7 @@ class C { int Y => 2; } var generatedDocument = (await solution.Projects.Single().GetSourceGeneratedDocumentsAsync()).Single(); var diagnostics1 = await service.GetDocumentDiagnosticsAsync(generatedDocument, s_noActiveSpans, CancellationToken.None); - AssertEx.Equal(new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.property_) }, + AssertEx.Equal(new[] { "ENC0021: " + string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.type_parameter) }, diagnostics1.Select(d => $"{d.Id}: {d.GetMessage()}")); var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); @@ -1465,7 +1465,7 @@ public async Task RudeEdits_DocumentOutOfSync(bool breakMode) { var source0 = "class C1 { void M() { System.Console.WriteLine(0); } }"; var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; - var source2 = "class C1 { void RenamedMethod() { System.Console.WriteLine(1); } }"; + var source2 = "class C1 { void M() { System.Console.WriteLine(1); } }"; var dir = Temp.CreateDirectory(); var sourceFile = dir.CreateFile("a.cs"); @@ -1524,8 +1524,12 @@ public async Task RudeEdits_DocumentOutOfSync(bool breakMode) // now we see the rude edit: diagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); - AssertEx.Equal(new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, - diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + AssertEx.Equal(new[] + { + "ENC0038: " + FeaturesResources.Modifying_a_method_inside_the_context_of_a_generic_type_requires_restarting_the_application, + "ENC0021: " + string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.type_parameter) + }, + diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ManagedModuleUpdateStatus.RestartRequired, updates.Status); @@ -1549,8 +1553,9 @@ public async Task RudeEdits_DocumentOutOfSync(bool breakMode) AssertEx.Equal(new[] { "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=1|EmptySessionCount=0|HotReloadSessionCount=0|EmptyHotReloadSessionCount=2", - "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=True|Capabilities=31|ProjectIdsWithAppliedChanges=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=20|RudeEditSyntaxKind=8875|RudeEditBlocking=True" + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=2|EmitDeltaErrorIdCount=0|InBreakState=True|Capabilities=31|ProjectIdsWithAppliedChanges=", + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=38|RudeEditSyntaxKind=8875|RudeEditBlocking=True", + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=21|RudeEditSyntaxKind=8910|RudeEditBlocking=True" }, _telemetryLog); } else @@ -1558,8 +1563,9 @@ public async Task RudeEdits_DocumentOutOfSync(bool breakMode) AssertEx.Equal(new[] { "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", - "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=20|RudeEditSyntaxKind=8875|RudeEditBlocking=True" + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=2|EmitDeltaErrorIdCount=0|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=", + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=38|RudeEditSyntaxKind=8875|RudeEditBlocking=True", + "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=21|RudeEditSyntaxKind=8910|RudeEditBlocking=True" }, _telemetryLog); } } @@ -1634,13 +1640,13 @@ public async Task RudeEdits_DelayLoadedModule() EnterBreakState(debuggingSession); // change the source (rude edit) before the library is loaded: - solution = solution.WithDocumentText(document1.Id, SourceText.From("class C { public void Renamed() { } }")); + solution = solution.WithDocumentText(document1.Id, SourceText.From("class C { public void M() { } }")); var document2 = solution.Projects.Single().Documents.Single(); // Rude Edits reported: var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); AssertEx.Equal( - new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, + new[] { "ENC0021: " + string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.type_parameter) }, diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); @@ -1654,7 +1660,7 @@ public async Task RudeEdits_DelayLoadedModule() // Rude Edits still reported: diagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); AssertEx.Equal( - new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, + new[] { "ENC0021: " + string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.type_parameter) }, diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); @@ -4317,7 +4323,7 @@ public async Task WatchHotReloadServiceTest() var source1 = "class C { void M() { System.Console.WriteLine(1); } }"; var source2 = "class C { void M() { System.Console.WriteLine(2); } }"; - var source3 = "class C { void X() { System.Console.WriteLine(2); } }"; + var source3 = "class C { int M() { System.Console.WriteLine(2); } }"; var source4 = "class C { void M() { System.Console.WriteLine(2)/* missing semicolon */ }"; var dir = Temp.CreateDirectory(); @@ -4364,7 +4370,7 @@ public async Task WatchHotReloadServiceTest() result = await hotReload.EmitSolutionUpdateAsync(solution, CancellationToken.None); AssertEx.Equal( - new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, + new[] { "ENC0009: " + string.Format(FeaturesResources.Updating_the_type_of_0_requires_restarting_the_application, FeaturesResources.method) }, result.diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); Assert.Empty(result.updates); @@ -4384,7 +4390,7 @@ public async Task UnitTestingHotReloadServiceTest() { var source1 = "class C { void M() { System.Console.WriteLine(1); } }"; var source2 = "class C { void M() { System.Console.WriteLine(2); } }"; - var source3 = "class C { void X() { System.Console.WriteLine(2); } }"; + var source3 = "class C { int M() { System.Console.WriteLine(2); } }"; var source4 = "class C { void M() { System.Console.WriteLine(2)/* missing semicolon */ }"; var dir = Temp.CreateDirectory(); @@ -4430,7 +4436,7 @@ public async Task UnitTestingHotReloadServiceTest() // Rude edit result = await hotReload.EmitSolutionUpdateAsync(solution, commitUpdates: true, CancellationToken.None); AssertEx.Equal( - new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) }, + new[] { "ENC0009: " + string.Format(FeaturesResources.Updating_the_type_of_0_requires_restarting_the_application, FeaturesResources.method) }, result.diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); Assert.Empty(result.updates); diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb index f5aee999fa0a1..9853fc1001351 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb @@ -344,11 +344,8 @@ End Class Dim edits = GetTopEdits(src1, src2) Dim active = GetActiveStatements(src1, src2) - edits.VerifySemantics(active, - semanticEdits:= - { - SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.Goo"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")) - }) + edits.VerifySemanticDiagnostics(active, + Diagnostic(RudeEditKind.Delete, "Class C", DeletedSymbolDisplay(FeaturesResources.method, "Goo(a As Integer)"))) End Sub diff --git a/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb b/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb index cf164c973708e..d2f6bf9921c5b 100644 --- a/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb +++ b/src/EditorFeatures/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb @@ -3925,34 +3925,25 @@ End Class Public Sub PartialMember_RenameInsertDelete() - ' The syntactic analysis for A And B produce rename edits since it doesn't see that the member was in fact moved. - ' TODO: Currently, we don't even pass rename edits to semantic analysis where we could handle them as updates. - Dim srcA1 = "Partial Class C" + vbCrLf + "Sub F1() : End Sub : End Class" Dim srcB1 = "Partial Class C" + vbCrLf + "Sub F2() : End Sub : End Class" Dim srcA2 = "Partial Class C" + vbCrLf + "Sub F2() : End Sub : End Class" Dim srcB2 = "Partial Class C" + vbCrLf + "Sub F1() : End Sub : End Class" - ' current outcome - GetTopEdits(srcA1, srcA2).VerifySemanticDiagnostics(Diagnostic(RudeEditKind.Renamed, "Sub F2()", FeaturesResources.method)) - GetTopEdits(srcB1, srcB2).VerifySemanticDiagnostics(Diagnostic(RudeEditKind.Renamed, "Sub F1()", FeaturesResources.method)) - - ' correct outcome - 'EditAndContinueValidation.VerifySemantics( - ' { GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2) }, - ' - ' { - ' DocumentResults(semanticEdits:= - ' { - ' SemanticEdit(SemanticEditKind.Update, c => c.GetMember(Of NamedTypeSymbol)("C").GetMember("F2")), - ' }), - ' - ' DocumentResults( - ' semanticEdits:= - ' { - ' SemanticEdit(SemanticEditKind.Update, c => c.GetMember(Of NamedTypeSymbol)("C").GetMember("F1")), - ' }) - ' }) + EditAndContinueValidation.VerifySemantics( + {GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2)}, + { + DocumentResults( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Update, Function(c) c.GetMember("C.F2")) + }), + DocumentResults( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Update, Function(c) c.GetMember("C.F1")) + }) + }) End Sub @@ -4687,8 +4678,12 @@ Imports System.Runtime.InteropServices edits.VerifyEdits( "Update [Sub Goo]@11 -> [Sub Bar]@11") - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "Sub Bar", FeaturesResources.method)) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.Goo"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.Bar")) + }) End Sub @@ -7957,8 +7952,12 @@ End Class" edits.VerifyEdits( "Update [ReadOnly Property P As Integer]@10 -> [ReadOnly Property Q As Integer]@10") - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "ReadOnly Property Q", FeaturesResources.property_)) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.get_P"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.get_Q")) + }) End Sub @@ -7970,8 +7969,12 @@ End Class" edits.VerifyEdits( "Update [ReadOnly Property P As Integer]@10 -> [ReadOnly Property Q As Integer]@10") - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "ReadOnly Property Q", FeaturesResources.property_)) + edits.VerifySemantics( + semanticEdits:= + { + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.get_P"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.get_Q")) + }) End Sub @@ -10184,8 +10187,16 @@ End Class edits.VerifyEdits( "Update [Custom Event E As Action]@10 -> [Custom Event F As Action]@10") - edits.VerifySemanticDiagnostics( - Diagnostic(RudeEditKind.Renamed, "Event F", FeaturesResources.event_)) + 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")), + SemanticEdit(SemanticEditKind.Delete, Function(c) c.GetMember("C.raise_E"), deletedSymbolContainerProvider:=Function(c) c.GetMember("C")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.add_F")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.remove_F")), + SemanticEdit(SemanticEditKind.Insert, Function(c) c.GetMember("C.raise_F")) + }) End Sub diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs index cd2da7f619f6c..074fda0603593 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs @@ -2678,8 +2678,9 @@ public ConstructorEdit(INamedTypeSymbol oldType) continue; } - if (TryAddMemberDeleteSemanticEdits(semanticEdits, oldSymbol, containingSymbolKey, syntaxMap, cancellationToken)) + if (!hasActiveStatement && AllowsDeletion(oldSymbol, willInsertNewMember: false, capabilities)) { + AddMemberOrAssociatedMemberSemanticEdits(semanticEdits, SemanticEditKind.Delete, oldSymbol, containingSymbolKey, syntaxMap, partialType: null, cancellationToken); continue; } } @@ -3024,7 +3025,42 @@ public ConstructorEdit(INamedTypeSymbol oldType) Contract.ThrowIfNull(oldSymbol); AnalyzeSymbolUpdate(oldSymbol, newSymbol, edit.NewNode, newCompilation, editScript.Match, capabilities, diagnostics, semanticEdits, syntaxMap, cancellationToken); - if (newSymbol is INamedTypeSymbol or IFieldSymbol or IPropertySymbol or IEventSymbol or IParameterSymbol or ITypeParameterSymbol) + + if (newSymbol is INamedTypeSymbol or IFieldSymbol or IParameterSymbol or ITypeParameterSymbol) + { + continue; + } + + // For renames where the symbol allows deletion, we don't create an update edit, we create a delete + // and an add. During emit an empty body will be created for the old name. Sometimes + // when members are moved between documents in partial classes, they can appear as renames, so + // we also check that the old symbol can't be resolved in the new compilation + if (oldSymbol.Name != newSymbol.Name && + AllowsDeletion(oldSymbol, willInsertNewMember: true, capabilities) && + SymbolKey.Create(oldSymbol, cancellationToken).Resolve(newCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol is null) + { + Contract.ThrowIfNull(oldDeclaration); + var activeStatementIndices = GetOverlappingActiveStatements(oldDeclaration, oldActiveStatements); + if (activeStatementIndices.Any()) + { + Contract.ThrowIfNull(newDeclaration); + AddRudeUpdateAroundActiveStatement(diagnostics, newDeclaration); + } + else + { + var containingSymbolKey = SymbolKey.Create(oldSymbol.ContainingType, cancellationToken); + + AddMemberOrAssociatedMemberSemanticEdits(semanticEdits, SemanticEditKind.Delete, oldSymbol, containingSymbolKey, syntaxMap, partialType: null, cancellationToken); + + AddMemberOrAssociatedMemberSemanticEdits(semanticEdits, SemanticEditKind.Insert, newSymbol, containingSymbolKey: null, syntaxMap, + partialType: IsPartialEdit(oldSymbol, newSymbol, editScript.Match.OldRoot.SyntaxTree, editScript.Match.NewRoot.SyntaxTree) ? symbolKey : null, + cancellationToken); + } + + continue; + } + + if (newSymbol is IPropertySymbol or IEventSymbol) { continue; } @@ -3182,23 +3218,32 @@ public ConstructorEdit(INamedTypeSymbol oldType) } } - private static bool TryAddMemberDeleteSemanticEdits(ArrayBuilder semanticEdits, ISymbol oldSymbol, SymbolKey containingSymbolKey, Func? syntaxMap, CancellationToken cancellationToken) + /// + /// Returns whether or not the specified symbol can be deleted by the user. Normally deletes are a rude edit + /// but for some kinds of symbols we allow deletes, and synthesize an update to an empty method body during + /// emit. + /// + private static bool AllowsDeletion(ISymbol symbol, bool willInsertNewMember, EditAndContinueCapabilitiesGrantor capabilities) { // We don't currently allow deleting virtual or abstract methods, because if those are in the middle of // an inheritance chain then throwing a missing method exception is not expected - if (oldSymbol.GetSymbolModifiers() is not { IsVirtual: false, IsAbstract: false, IsOverride: false }) + if (symbol.GetSymbolModifiers() is not { IsVirtual: false, IsAbstract: false, IsOverride: false }) return false; // Extern methods can't be deleted - if (oldSymbol.IsExtern) + if (symbol.IsExtern) return false; // We don't allow deleting members from interfaces etc. only normal classes and structs - if (oldSymbol.ContainingType is not { TypeKind: TypeKind.Class or TypeKind.Struct }) + if (symbol.ContainingType is not { TypeKind: TypeKind.Class or TypeKind.Struct }) return false; - // Wo store the containing symbol in NewSymbol of the edit for later use. - if (oldSymbol is IMethodSymbol + // If this delete will result in a new member being added, then we have to check capabilities too + if (willInsertNewMember && !CanAddNewMember(symbol, capabilities)) + return false; + + // We store the containing symbol in NewSymbol of the edit for later use. + if (symbol is IMethodSymbol { MethodKind: MethodKind.Ordinary or @@ -3212,38 +3257,45 @@ MethodKind.PropertyGet or MethodKind.PropertySet }) { - AddDeleteSemanticEdit(semanticEdits, oldSymbol, containingSymbolKey, syntaxMap, cancellationToken); - return true; } - if (oldSymbol is IPropertySymbol propertySymbol) - { - AddDeleteSemanticEdit(semanticEdits, propertySymbol.GetMethod, containingSymbolKey, syntaxMap, cancellationToken); - AddDeleteSemanticEdit(semanticEdits, propertySymbol.SetMethod, containingSymbolKey, syntaxMap, cancellationToken); + return symbol is IPropertySymbol or IEventSymbol; + } - return true; - } + /// + /// Add semantic edits for the specified symbol, or the associated members of the specified symbol, + /// for example, edits for each accessor if a property symbol is passed in. + /// + private static void AddMemberOrAssociatedMemberSemanticEdits(ArrayBuilder semanticEdits, SemanticEditKind editKind, ISymbol symbol, SymbolKey? containingSymbolKey, Func? syntaxMap, SymbolKey? partialType, CancellationToken cancellationToken) + { + Debug.Assert(symbol is IMethodSymbol or IPropertySymbol or IEventSymbol); - if (oldSymbol is IEventSymbol eventSymbol) + // We store the containing symbol in NewSymbol of the edit for later use. + if (symbol is IMethodSymbol) { - AddDeleteSemanticEdit(semanticEdits, eventSymbol.AddMethod, containingSymbolKey, syntaxMap, cancellationToken); - AddDeleteSemanticEdit(semanticEdits, eventSymbol.RemoveMethod, containingSymbolKey, syntaxMap, cancellationToken); - AddDeleteSemanticEdit(semanticEdits, eventSymbol.RaiseMethod, containingSymbolKey, syntaxMap, cancellationToken); - - return true; + AddEdit(symbol); + } + else if (symbol is IPropertySymbol propertySymbol) + { + AddEdit(propertySymbol.GetMethod); + AddEdit(propertySymbol.SetMethod); + } + else if (symbol is IEventSymbol eventSymbol) + { + AddEdit(eventSymbol.AddMethod); + AddEdit(eventSymbol.RemoveMethod); + AddEdit(eventSymbol.RaiseMethod); } - return false; - } - - private static void AddDeleteSemanticEdit(ArrayBuilder semanticEdits, ISymbol? symbol, SymbolKey containingSymbolKey, Func? syntaxMap, CancellationToken cancellationToken) - { - if (symbol is null) - return; + void AddEdit(ISymbol? symbol) + { + if (symbol is null) + return; - var symbolKey = SymbolKey.Create(symbol, cancellationToken); - semanticEdits.Add(new SemanticEditInfo(SemanticEditKind.Delete, symbolKey, syntaxMap, syntaxMapTree: null, partialType: null, deletedSymbolContainer: containingSymbolKey)); + var symbolKey = SymbolKey.Create(symbol, cancellationToken); + semanticEdits.Add(new SemanticEditInfo(editKind, symbolKey, syntaxMap, syntaxMapTree: null, partialType, deletedSymbolContainer: containingSymbolKey)); + } } private ImmutableArray<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetNamespaceSymbolEdits( @@ -3402,13 +3454,7 @@ public int GetHashCode([DisallowNull] SemanticEditInfo obj) rudeEdit = RudeEditKind.RenamingNotSupportedByRuntime; } } - else - { - rudeEdit = RudeEditKind.Renamed; - } - - // specialize rude edit for accessors and conversion operators: - if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) + else if (oldSymbol is IMethodSymbol oldMethod && newSymbol is IMethodSymbol newMethod) { if (oldMethod.AssociatedSymbol != null && newMethod.AssociatedSymbol != null) { @@ -3426,6 +3472,43 @@ public int GetHashCode([DisallowNull] SemanticEditInfo obj) { rudeEdit = RudeEditKind.ModifiersUpdate; } + else if (oldMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation || newMethod.MethodKind == MethodKind.ExplicitInterfaceImplementation) + { + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; + } + else if (!AllowsDeletion(oldSymbol, willInsertNewMember: true, capabilities)) + { + rudeEdit = RudeEditKind.Renamed; + } + } + else if (oldSymbol is IPropertySymbol oldProperty && newSymbol is IPropertySymbol newProperty) + { + if (!oldProperty.ExplicitInterfaceImplementations.IsEmpty || !newProperty.ExplicitInterfaceImplementations.IsEmpty) + { + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; + } + else if (!AllowsDeletion(oldSymbol, willInsertNewMember: true, capabilities)) + { + rudeEdit = RudeEditKind.Renamed; + } + } + else if (oldSymbol is IEventSymbol oldEvent && newSymbol is IEventSymbol newEvent) + { + if (!oldEvent.ExplicitInterfaceImplementations.IsEmpty || !newEvent.ExplicitInterfaceImplementations.IsEmpty) + { + // Can't change from explicit to implicit interface implementation, or one interface to another + rudeEdit = RudeEditKind.Renamed; + } + else if (!AllowsDeletion(oldSymbol, willInsertNewMember: true, capabilities)) + { + rudeEdit = RudeEditKind.Renamed; + } + } + else + { + rudeEdit = RudeEditKind.Renamed; } } @@ -3610,8 +3693,6 @@ public int GetHashCode([DisallowNull] SemanticEditInfo obj) if (rudeEdit != RudeEditKind.None) { - // so we'll just use the last global statement in the file - ReportUpdateRudeEdit(diagnostics, rudeEdit, oldSymbol, newSymbol, newNode, newCompilation, cancellationToken); } } @@ -4042,7 +4123,7 @@ static bool IsSecurityAttribute(INamedTypeSymbol namedTypeSymbol) private static bool CanAddNewMember(ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities) { - if (newSymbol is IMethodSymbol or IPropertySymbol) // Properties are just get_ and set_ methods + if (newSymbol is IMethodSymbol or IPropertySymbol or IEventSymbol) // Properties are just get_ and set_ methods { return capabilities.Grant(EditAndContinueCapabilities.AddMethodToExistingType); }