From b1fb60f331fc021e35d90e6a77261dfcb8aad532 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 14:24:10 +0200 Subject: [PATCH] Added handling for modifiers in sealed classes --- .../TransitiveMembersGenerator.cs | 12 +++- .../MemberDeclarationSyntaxExtensions.cs | 57 +++++++++++++++++++ .../Mvvm/Test_ObservableObjectAttribute.cs | 51 +++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 2f6ae7d0..2b7908d9 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -108,6 +108,16 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + IEnumerable generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration); + + // If the target class is sealed, make protected members private and remove the virtual modifier + if (classDeclarationSymbol.IsSealed) + { + generatedMembers = generatedMembers.Select(static member => member + .ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword) + .RemoveModifier(SyntaxKind.VirtualKeyword)); + } + // Create the class declaration for the user type. This will produce a tree as follows: // // : @@ -118,7 +128,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) .WithBaseList(sourceDeclaration.BaseList) - .AddMembers(FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); + .AddMembers(generatedMembers.ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs new file mode 100644 index 00000000..cdc63118 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions +{ + /// + /// Extension methods for the type. + /// + internal static class MemberDeclarationSyntaxExtensions + { + /// + /// Replaces a specific modifier. + /// + /// The input instance. + /// The target modifier kind to replace. + /// The new modifier kind to add or replace. + /// A instance with the target modifier. + [Pure] + public static MemberDeclarationSyntax ReplaceModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind oldKind, SyntaxKind newKind) + { + int index = memberDeclaration.Modifiers.IndexOf(oldKind); + + if (index != -1) + { + return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.Replace(memberDeclaration.Modifiers[index], Token(newKind))); + } + + return memberDeclaration; + } + + /// + /// Removes a specific modifier. + /// + /// The input instance. + /// The modifier kind to remove. + /// A instance without the specified modifier. + [Pure] + public static MemberDeclarationSyntax RemoveModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind kind) + { + int index = memberDeclaration.Modifiers.IndexOf(kind); + + if (index != -1) + { + return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.RemoveAt(index)); + } + + return memberDeclaration; + } + } +} diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs index dfb49ec6..6b3cdf0d 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs @@ -50,6 +50,45 @@ namespace UnitTests.Mvvm Assert.AreEqual(changed.Item2, 42); } + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableObjectAttribute_OnSealedClass_Events() + { + var model = new SampleModelSealed(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModelSealed.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModelSealed.Data)); + Assert.AreEqual(changed.Item2, 42); + } + [ObservableObject] public partial class SampleModel { @@ -61,5 +100,17 @@ namespace UnitTests.Mvvm set => SetProperty(ref data, value); } } + + [ObservableObject] + public sealed partial class SampleModelSealed + { + private int data; + + public int Data + { + get => data; + set => SetProperty(ref data, value); + } + } } }