diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.cs index 4538afaf..da13fd42 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.cs @@ -134,10 +134,9 @@ public sealed record Enum(string TypeName, object Value) : TypedConstantInfo /// public override ExpressionSyntax GetSyntax() { - return - CastExpression( - IdentifierName(TypeName), - LiteralExpression(SyntaxKind.NumericLiteralExpression, ParseToken(Value.ToString()))); + // We let Roslyn parse the value expression, so that it can automatically handle both positive and negative values. This + // is needed because negative values have a different syntax tree (UnaryMinuxExpression holding the numeric expression). + return CastExpression(IdentifierName(TypeName), ParseExpression(Value.ToString())); } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index 0062d61d..6edc38b5 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -1283,6 +1283,278 @@ partial class MyViewModel VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/681 + [TestMethod] + public void ObservablePropertyWithForwardedAttributesWithEnumWithNegativeValue_WorksCorrectly() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [property: DefaultValue(NegativeEnum1.Problem)] + [property: DefaultValue(NegativeEnum2.Problem)] + [property: DefaultValue(NegativeEnum3.Problem)] + [property: DefaultValue(NegativeEnum4.Problem)] + private object? a; + } + + public class DefaultValueAttribute : Attribute + { + public DefaultValueAttribute(object value) + { + } + } + + public enum NegativeEnum1 + { + Problem = -1073741824, + OK = 0 + } + + public enum NegativeEnum2 : long + { + Problem = long.MinValue, + OK = 0 + } + + public enum NegativeEnum3 : short + { + Problem = -1234, + OK = 0 + } + + public enum NegativeEnum4 : sbyte + { + Problem = -1, + OK = 0 + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] + public object? A + { + get => a; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) + { + OnAChanging(value); + OnAChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); + a = value; + OnAChanged(value); + OnAChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanging(object? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanging(object? oldValue, object? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanged(object? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanged(object? oldValue, object? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithForwardedAttributesWithEnumWithCombinedValues_WorksCorrectly() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [property: DefaultValue(NegativeEnum1.A)] + [property: DefaultValue(NegativeEnum1.B)] + [property: DefaultValue((NegativeEnum1)42)] + [property: DefaultValue((NegativeEnum1)OtherClass.MyNumber)] + [property: DefaultValue(OtherClass.MyEnumValue)] + [property: DefaultValue(NegativeEnum2.A)] + [property: DefaultValue(NegativeEnum2.B)] + [property: DefaultValue((NegativeEnum2)42)] + [property: DefaultValue((NegativeEnum2)OtherClass.MyNumber)] + [property: DefaultValue((NegativeEnum2)(int)OtherClass.MyEnumValue)] + [property: DefaultValue(NegativeEnum3.A)] + [property: DefaultValue(NegativeEnum3.B)] + [property: DefaultValue((NegativeEnum3)42)] + [property: DefaultValue((NegativeEnum3)OtherClass.MyNumber)] + [property: DefaultValue((NegativeEnum3)(int)OtherClass.MyEnumValue)] + [property: DefaultValue(NegativeEnum4.A)] + [property: DefaultValue(NegativeEnum4.B)] + [property: DefaultValue((NegativeEnum4)42)] + [property: DefaultValue((NegativeEnum4)unchecked((sbyte)OtherClass.MyNumber))] + [property: DefaultValue((NegativeEnum4)(int)OtherClass.MyEnumValue)] + private object? a; + } + + public static class OtherClass + { + public const int MyNumber = 456; + public const NegativeEnum1 MyEnumValue = NegativeEnum1.C; + } + + public class DefaultValueAttribute : Attribute + { + public DefaultValueAttribute(object value) + { + } + } + + public enum NegativeEnum1 + { + A = 0, + B = -1073741824, + C = 123 + } + + public enum NegativeEnum2 : long + { + A = 0, + B = long.MinValue + } + + public enum NegativeEnum3 : short + { + A = 1, + B = -1234 + } + + public enum NegativeEnum4 : sbyte + { + A = 1, + B = -1 + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)0)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)42)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)456)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)123)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)0)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)42)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)456)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)123)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)1)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)42)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)456)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)123)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)1)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)42)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-56)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)123)] + public object? A + { + get => a; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) + { + OnAChanging(value); + OnAChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); + a = value; + OnAChanged(value); + OnAChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanging(object? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanging(object? oldValue, object? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanged(object? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnAChanged(object? oldValue, object? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + // See https://github.com/CommunityToolkit/dotnet/issues/632 [TestMethod] public void RelayCommandMethodWithPartialDeclarations_TriggersCorrectly() @@ -1450,6 +1722,96 @@ partial class MyViewModel VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2)); } + // See https://github.com/CommunityToolkit/dotnet/issues/681 + [TestMethod] + public void RelayCommandMethodWithForwardedAttributesWithEnumValues_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.Input; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel + { + [RelayCommand] + [field: DefaultValue(NegativeEnum1.Problem)] + [field: DefaultValue(NegativeEnum2.Problem)] + [field: DefaultValue(NegativeEnum3.Problem)] + [field: DefaultValue(NegativeEnum4.Problem)] + [property: DefaultValue(NegativeEnum1.Problem)] + [property: DefaultValue(NegativeEnum2.Problem)] + [property: DefaultValue(NegativeEnum3.Problem)] + [property: DefaultValue(NegativeEnum4.Problem)] + private void Test() + { + } + } + + public class DefaultValueAttribute : Attribute + { + public DefaultValueAttribute(object value) + { + } + } + + public enum NegativeEnum1 + { + Problem = -1073741824, + OK = 0 + } + + public enum NegativeEnum2 : long + { + Problem = long.MinValue, + OK = 0 + } + + public enum NegativeEnum3 : short + { + Problem = -1234, + OK = 0 + } + + public enum NegativeEnum4 : sbyte + { + Problem = -1, + OK = 0 + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// The backing field for . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] + private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand; + /// Gets an instance wrapping . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] + [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] + public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test)); + } + } + """; + + VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithinGenericAndNestedTypes() {