// 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; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_SourceGeneratorsCodegen { [TestMethod] public void ObservablePropertyWithPartialMethodWithPreviousValuesNotUsed_DoesNotGenerateFieldReadAndMarksOldValueAsNullable() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private string name = null!; } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { partial class MyViewModel { /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public string Name { get => name; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) { OnNameChanging(value); OnNameChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); name = value; OnNameChanged(value); OnNameChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); } } } /// 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.1.0.0")] partial void OnNameChanging(string 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.1.0.0")] partial void OnNameChanging(string? oldValue, string 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.1.0.0")] partial void OnNameChanged(string 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.1.0.0")] partial void OnNameChanged(string? oldValue, string newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } // See https://github.com/CommunityToolkit/dotnet/issues/601 [TestMethod] public void ObservablePropertyWithForwardedAttributesWithNumberLiterals_PreservesType() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { const double MyDouble = 3.14; const float MyFloat = 3.14f; [ObservableProperty] [property: DefaultValue(0.0)] [property: DefaultValue(1.24)] [property: DefaultValue(0.0f)] [property: DefaultValue(0.0f)] [property: DefaultValue(MyDouble)] [property: DefaultValue(MyFloat)] private object? a; } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { partial class MyViewModel { /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::MyApp.DefaultValueAttribute(0D)] [global::MyApp.DefaultValueAttribute(1.24D)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(3.14D)] [global::MyApp.DefaultValueAttribute(3.14F)] 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.1.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.1.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.1.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.1.0.0")] partial void OnAChanged(object? oldValue, object? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void ObservablePropertyWithinGenericAndNestedTypes() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class Foo { partial class MyViewModel : ObservableObject { [ObservableProperty] private string? a; } } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { partial class Foo { partial class MyViewModel { /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public string? 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.1.0.0")] partial void OnAChanging(string? 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.1.0.0")] partial void OnAChanging(string? oldValue, string? 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.1.0.0")] partial void OnAChanged(string? 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.1.0.0")] partial void OnAChanged(string? oldValue, string? newValue); } } } """; #if ROSLYN_4_3_1_OR_GREATER VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.Foo+MyViewModel`1.g.cs", result)); #else VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.Foo.MyViewModel_1.g.cs", result)); #endif } /// /// Generates the requested sources /// /// The input source to process. /// The generators to apply to the input syntax tree. /// The source files to compare. private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string Text)[] results) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); Type validationAttributeType = typeof(ValidationAttribute); // Get all assembly references for the loaded assemblies (easy way to pull in all necessary dependencies) IEnumerable references = from assembly in AppDomain.CurrentDomain.GetAssemblies() where !assembly.IsDynamic let reference = MetadataReference.CreateFromFile(assembly.Location) select reference; SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10)); // Create a syntax tree with the input source CSharpCompilation compilation = CSharpCompilation.Create( "original", new SyntaxTree[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); GeneratorDriver driver = CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options); // Run all source generators on the input source code _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics); // Ensure that no diagnostics were generated CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); foreach ((string filename, string text) in results) { SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filename); Assert.AreEqual(text, generatedTree.ToString()); } GC.KeepAlive(observableObjectType); GC.KeepAlive(validationAttributeType); } }