Skip to content

Commit 0a08615

Browse files
committed
Added handling for modifiers in sealed classes
1 parent 4cd8a7c commit 0a08615

File tree

3 files changed

+119
-1
lines changed

3 files changed

+119
-1
lines changed

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ private void OnExecute(
108108
ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().First();
109109
UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>().ToArray();
110110

111+
IEnumerable<MemberDeclarationSyntax> generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration);
112+
113+
// If the target class is sealed, make protected members private and remove the virtual modifier
114+
if (classDeclarationSymbol.IsSealed)
115+
{
116+
generatedMembers = generatedMembers.Select(static member => member
117+
.ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword)
118+
.RemoveModifier(SyntaxKind.VirtualKeyword));
119+
}
120+
111121
// Create the class declaration for the user type. This will produce a tree as follows:
112122
//
113123
// <MODIFIERS> <CLASS_NAME> : <BASE_TYPES>
@@ -118,7 +128,7 @@ private void OnExecute(
118128
ClassDeclaration(classDeclaration.Identifier.Text)
119129
.WithModifiers(classDeclaration.Modifiers)
120130
.WithBaseList(sourceDeclaration.BaseList)
121-
.AddMembers(FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray());
131+
.AddMembers(generatedMembers.ToArray());
122132

123133
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
124134

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.Contracts;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
10+
11+
namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions
12+
{
13+
/// <summary>
14+
/// Extension methods for the <see cref="MemberDeclarationSyntax"/> type.
15+
/// </summary>
16+
internal static class MemberDeclarationSyntaxExtensions
17+
{
18+
/// <summary>
19+
/// Replaces a specific modifier.
20+
/// </summary>
21+
/// <param name="memberDeclaration">The input <see cref="MemberDeclarationSyntax"/> instance.</param>
22+
/// <param name="oldKind">The target modifier kind to replace.</param>
23+
/// <param name="newKind">The new modifier kind to add or replace.</param>
24+
/// <returns>A <see cref="MemberDeclarationSyntax"/> instance with the target modifier.</returns>
25+
[Pure]
26+
public static MemberDeclarationSyntax ReplaceModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind oldKind, SyntaxKind newKind)
27+
{
28+
int index = memberDeclaration.Modifiers.IndexOf(oldKind);
29+
30+
if (index != -1)
31+
{
32+
return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.Replace(memberDeclaration.Modifiers[index], Token(newKind)));
33+
}
34+
35+
return memberDeclaration;
36+
}
37+
38+
/// <summary>
39+
/// Removes a specific modifier.
40+
/// </summary>
41+
/// <param name="memberDeclaration">The input <see cref="MemberDeclarationSyntax"/> instance.</param>
42+
/// <param name="kind">The modifier kind to remove.</param>
43+
/// <returns>A <see cref="MemberDeclarationSyntax"/> instance without the specified modifier.</returns>
44+
[Pure]
45+
public static MemberDeclarationSyntax RemoveModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind kind)
46+
{
47+
int index = memberDeclaration.Modifiers.IndexOf(kind);
48+
49+
if (index != -1)
50+
{
51+
return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.RemoveAt(index));
52+
}
53+
54+
return memberDeclaration;
55+
}
56+
}
57+
}

UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,45 @@ public void Test_ObservableObjectAttribute_Events()
5050
Assert.AreEqual(changed.Item2, 42);
5151
}
5252

53+
[TestCategory("Mvvm")]
54+
[TestMethod]
55+
public void Test_ObservableObjectAttribute_OnSealedClass_Events()
56+
{
57+
var model = new SampleModelSealed();
58+
59+
(PropertyChangingEventArgs, int) changing = default;
60+
(PropertyChangedEventArgs, int) changed = default;
61+
62+
model.PropertyChanging += (s, e) =>
63+
{
64+
Assert.IsNull(changing.Item1);
65+
Assert.IsNull(changed.Item1);
66+
Assert.AreSame(model, s);
67+
Assert.IsNotNull(s);
68+
Assert.IsNotNull(e);
69+
70+
changing = (e, model.Data);
71+
};
72+
73+
model.PropertyChanged += (s, e) =>
74+
{
75+
Assert.IsNotNull(changing.Item1);
76+
Assert.IsNull(changed.Item1);
77+
Assert.AreSame(model, s);
78+
Assert.IsNotNull(s);
79+
Assert.IsNotNull(e);
80+
81+
changed = (e, model.Data);
82+
};
83+
84+
model.Data = 42;
85+
86+
Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModelSealed.Data));
87+
Assert.AreEqual(changing.Item2, 0);
88+
Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModelSealed.Data));
89+
Assert.AreEqual(changed.Item2, 42);
90+
}
91+
5392
[ObservableObject]
5493
public partial class SampleModel
5594
{
@@ -61,5 +100,17 @@ public int Data
61100
set => SetProperty(ref data, value);
62101
}
63102
}
103+
104+
[ObservableObject]
105+
public sealed partial class SampleModelSealed
106+
{
107+
private int data;
108+
109+
public int Data
110+
{
111+
get => data;
112+
set => SetProperty(ref data, value);
113+
}
114+
}
64115
}
65116
}

0 commit comments

Comments
 (0)