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);