Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -5875,6 +5877,36 @@ public void NestedType_ClassDeleteInsert()
Diagnostic(RudeEditKind.Move, "public class X", FeaturesResources.class_));
}

/// <summary>
/// Scenario: Razor page types are marked with CreateNewOnMetadataUpdateAttribute.
/// It is possible to define nested types via <c>@functions</c> block and any changes to this block should be allowed.
/// </summary>
[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);
}

/// <summary>
/// A new generic type can be added whether it's nested and inherits generic parameters from the containing type, or top-level.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2815,6 +2815,18 @@ private async Task<ImmutableArray<SemanticEditInfo>> 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;
Expand All @@ -2826,28 +2838,27 @@ private async Task<ImmutableArray<SemanticEditInfo>> 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))
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<SyntaxNode> editScript,
DocumentSemanticModel newModel,
RudeEditDiagnosticsBuilder diagnostics,
EditAndContinueCapabilitiesGrantor capabilities,
PooledHashSet<ISymbol> processedSymbols,
ArrayBuilder<SemanticEditInfo> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,8 @@ End Class

Dim srcA2 = ReloadableAttributeSrc
Dim srcB2 = "
Imports System.Runtime.CompilerServices

<CreateNewOnMetadataUpdate>
Class C
Sub F()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ public static IEnumerable<INamedTypeSymbol> GetBaseTypesAndThis(this INamedTypeS
}
}

public static IEnumerable<INamedTypeSymbol> GetContainingTypesAndThis(this INamedTypeSymbol? namedType)
{
var current = namedType;
while (current != null)
{
yield return current;
current = current.ContainingType;
}
}

public static ImmutableArray<ITypeParameterSymbol> GetAllTypeParameters(this INamedTypeSymbol? symbol)
{
var stack = GetContainmentStack(symbol);
Expand Down
Loading