diff --git a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs index 4282d9364c4b7..e063b2f2825b0 100644 --- a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs +++ b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs @@ -2707,8 +2707,12 @@ public void Type_DeleteInsert_Reloadable() { var srcA1 = ReloadableAttributeSrc + "[CreateNewOnMetadataUpdate]class C { void F() {} }"; var srcA2 = ReloadableAttributeSrc; + + var srcB1 = ""; + var srcB2 = "using System.Runtime.CompilerServices; [CreateNewOnMetadataUpdate]class C { void F() {} }"; + EditAndContinueValidation.VerifySemantics( - [GetTopEdits(srcA1, srcA2), GetTopEdits("", "[CreateNewOnMetadataUpdate]class C { void F() {} }")], + [GetTopEdits(srcA1, srcA2), GetTopEdits(srcB1, srcB2)], [ DocumentResults(), DocumentResults( @@ -5408,7 +5412,7 @@ public void NestedType_Replace_WithUpdateInNestedType_Partial_DifferentDocument( ]), DocumentResults(semanticEdits: [ - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.D.M")) + SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C"), partialType: "C") ]) ], capabilities: EditAndContinueCapabilities.NewTypeDefinition); @@ -5427,7 +5431,6 @@ public void NestedType_Replace_WithUpdateInNestedType_Partial_SameDocument() semanticEdits: [ SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.D.M")) ], capabilities: EditAndContinueCapabilities.NewTypeDefinition); } @@ -5444,8 +5447,7 @@ public void NestedType_Replace_WithUpdateInNestedType() edits, semanticEdits: [ - SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C")), - SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.D.M")) + SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C")) ], capabilities: EditAndContinueCapabilities.NewTypeDefinition); } @@ -5875,6 +5877,36 @@ public void NestedType_ClassDeleteInsert() Diagnostic(RudeEditKind.Move, "public class X", FeaturesResources.class_)); } + /// + /// Scenario: Razor page types are marked with CreateNewOnMetadataUpdateAttribute. + /// It is possible to define nested types via @functions block and any changes to this block should be allowed. + /// + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76989")] + public void NestedType_Enum_Update() + { + var src1 = ReloadableAttributeSrc + "[CreateNewOnMetadataUpdate]class C { enum E { A } }"; + var src2 = ReloadableAttributeSrc + "[CreateNewOnMetadataUpdate]class C { enum E { A, B } }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + semanticEdits: [SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C"))], + capabilities: EditAndContinueCapabilities.NewTypeDefinition); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76989")] + public void NestedType_Rename_Reloadable() + { + var src1 = ReloadableAttributeSrc + "[CreateNewOnMetadataUpdate]class C { class D1 { public void F() { Console.WriteLine(1); }}; }"; + var src2 = ReloadableAttributeSrc + "[CreateNewOnMetadataUpdate]class C { class D2 { public void F() { Console.WriteLine(2); }}; }"; + + var edits = GetTopEdits(src1, src2); + + edits.VerifySemantics( + semanticEdits: [SemanticEdit(SemanticEditKind.Replace, c => c.GetMember("C"))], + capabilities: EditAndContinueCapabilities.NewTypeDefinition); + } + /// /// A new generic type can be added whether it's nested and inherits generic parameters from the containing type, or top-level. /// diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs index d781f651a7624..add507817b28b 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs @@ -2815,6 +2815,18 @@ private async Task> AnalyzeSemanticsAsync( // Delete/insert/update edit of a member of a reloadable type (including nested types) results in Replace edit of the containing type. // If a Delete edit is part of delete-insert operation (member moved to a different partial type declaration or to a different file) // skip producing Replace semantic edit for this Delete edit as one will be reported by the corresponding Insert edit. + // + // Updates to types nested into reloadable type are handled as Replace edits of the reloadable type. + // + // Rationale: + // Any update to a member of a reloadable type results is a Replace edit of the type. + // Replace edit generates a new version of the entire reloadable type, including any types nested into it. + // Therefore, updating members results in new versions of all types nested in the reloadable type. + // It would be unnecessarily limiting and inconsistent to update nested types "in-place". + // + // Scenario: + // Razor page, which is a reloadable type, may define nested types using @functions block. + // Any changes should be allowed to be made in a Razor page, including changes to nested types defined in @functions block. var oldContainingType = oldSymbol?.ContainingType; var newContainingType = newSymbol?.ContainingType; @@ -2826,28 +2838,27 @@ private async Task> AnalyzeSemanticsAsync( oldContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(oldModel.Compilation, cancellationToken: cancellationToken).Symbol; newContainingType ??= (INamedTypeSymbol?)containingTypeSymbolKey.Resolve(newModel.Compilation, cancellationToken: cancellationToken).Symbol; - if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) + if (AddReloadableTypeSemanticEdit( + editScript, + newModel, + diagnostics, + capabilities, + processedSymbols, + semanticEdits, + oldTree, + newTree, + newDeclaration, + oldContainingType, + newContainingType, + cancellationToken)) { - if (processedSymbols.Add(newContainingType)) - { - if (capabilities.GrantNewTypeDefinition(containingType)) - { - semanticEdits.Add(SemanticEditInfo.CreateReplace(containingTypeSymbolKey, - IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? containingTypeSymbolKey : null)); - } - else - { - CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match). - Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); - } - } - continue; } } + // Handle changes to reloadable type itself (the above handles changes to its members and types). // Deleting a reloadable type is a rude edit, reported the same as for non-reloadable. - // Adding a reloadable type is a standard type addition (TODO: unless added to a reloadable type?). + // Adding a reloadable type is a standard type addition (unless added to a reloadable type). // Making reloadable attribute non-reloadable results in a new version of the type that is // not reloadable but does not update the old version in-place. if (syntacticEditKind != EditKind.Delete && oldSymbol is INamedTypeSymbol oldType && newSymbol is INamedTypeSymbol newType && IsReloadable(oldType)) @@ -3507,23 +3518,20 @@ IFieldSymbol or var oldContainingType = oldSymbol.ContainingType; var newContainingType = newSymbol.ContainingType; - if (oldContainingType != null && newContainingType != null && IsReloadable(oldContainingType)) + if (AddReloadableTypeSemanticEdit( + editScript, + newModel, + diagnostics, + capabilities, + processedSymbols, + semanticEdits, + oldTree, + newTree, + newDeclaration, + oldContainingType, + newContainingType, + cancellationToken)) { - if (processedSymbols.Add(newContainingType)) - { - if (capabilities.GrantNewTypeDefinition(newContainingType)) - { - var oldContainingTypeKey = SymbolKey.Create(oldContainingType, cancellationToken); - semanticEdits.Add(SemanticEditInfo.CreateReplace(oldContainingTypeKey, - IsPartialTypeEdit(oldContainingType, newContainingType, oldTree, newTree) ? oldContainingTypeKey : null)); - } - else - { - CreateDiagnosticContext(diagnostics, oldContainingType, newContainingType, newDeclaration, newModel, editScript.Match) - .Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); - } - } - continue; } @@ -4148,6 +4156,49 @@ private static bool HasRestartRequiredAttribute(ISymbol symbol) private static bool IsReloadable(INamedTypeSymbol type) => TypeOrBaseTypeHasCompilerServicesAttribute(type, CreateNewOnMetadataUpdateAttributeName); + private static INamedTypeSymbol? TryGetOutermostReloadableType(INamedTypeSymbol type) + => type.GetContainingTypesAndThis().FirstOrDefault(IsReloadable); + + private bool AddReloadableTypeSemanticEdit( + EditScript editScript, + DocumentSemanticModel newModel, + RudeEditDiagnosticsBuilder diagnostics, + EditAndContinueCapabilitiesGrantor capabilities, + PooledHashSet processedSymbols, + ArrayBuilder semanticEdits, + SyntaxTree oldTree, + SyntaxTree newTree, + SyntaxNode? newDeclaration, + INamedTypeSymbol? oldContainingType, + INamedTypeSymbol? newContainingType, + CancellationToken cancellationToken) + { + if (oldContainingType is null || + newContainingType is null || + TryGetOutermostReloadableType(oldContainingType) is not { } oldOutermostReloadableType || + TryGetOutermostReloadableType(newContainingType) is not { } newOutermostReloadableType) + { + return false; + } + + if (processedSymbols.Add(newOutermostReloadableType)) + { + if (capabilities.GrantNewTypeDefinition(newOutermostReloadableType)) + { + var oldOutermostReloadableTypeKey = SymbolKey.Create(oldOutermostReloadableType, cancellationToken); + semanticEdits.Add(SemanticEditInfo.CreateReplace(oldOutermostReloadableTypeKey, + IsPartialTypeEdit(oldOutermostReloadableType, newOutermostReloadableType, oldTree, newTree) ? oldOutermostReloadableTypeKey : null)); + } + else + { + CreateDiagnosticContext(diagnostics, oldOutermostReloadableType, newOutermostReloadableType, newDeclaration, newModel, editScript.Match). + Report(RudeEditKind.ChangingReloadableTypeNotSupportedByRuntime, cancellationToken); + } + } + + return true; + } + private static bool TypeOrBaseTypeHasCompilerServicesAttribute(INamedTypeSymbol type, string attributeName) { var current = type; diff --git a/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb b/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb index 6d5344244252e..60324fc815a46 100644 --- a/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb +++ b/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb @@ -1726,6 +1726,8 @@ End Class Dim srcA2 = ReloadableAttributeSrc Dim srcB2 = " +Imports System.Runtime.CompilerServices + Class C Sub F() diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/INamedTypeSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/INamedTypeSymbolExtensions.cs index 44270c933b55e..f539d52b2ade5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/INamedTypeSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/INamedTypeSymbolExtensions.cs @@ -30,6 +30,16 @@ public static IEnumerable GetBaseTypesAndThis(this INamedTypeS } } + public static IEnumerable GetContainingTypesAndThis(this INamedTypeSymbol? namedType) + { + var current = namedType; + while (current != null) + { + yield return current; + current = current.ContainingType; + } + } + public static ImmutableArray GetAllTypeParameters(this INamedTypeSymbol? symbol) { var stack = GetContainmentStack(symbol);