diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs new file mode 100644 index 00000000000..5566ed21b59 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs @@ -0,0 +1,56 @@ +// 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.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for necessary nullability attributes. + /// + [Generator] + public sealed class NullabilityAttributesGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + } + + /// + public void Execute(GeneratorExecutionContext context) + { + AddSourceCodeIfTypeIsNotPresent(context, "System.Diagnostics.CodeAnalysis.NotNullAttribute"); + AddSourceCodeIfTypeIsNotPresent(context, "System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute"); + } + + /// + /// Adds the source for a given attribute type if it's not present already in the compilation. + /// + private void AddSourceCodeIfTypeIsNotPresent(GeneratorExecutionContext context, string typeFullName) + { + if (context.Compilation.GetTypeByMetadataName(typeFullName) is not null) + { + return; + } + + string + typeName = typeFullName.Split('.').Last(), + filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeName}.cs"; + + Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); + StreamReader reader = new(stream); + + string + originalSource = reader.ReadToEnd(), + outputSource = originalSource.Replace("NETSTANDARD2_0", "true"); + + context.AddSource($"{typeFullName}.cs", SourceText.From(outputSource, Encoding.UTF8)); + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs index 72d14de5394..3f9a4a7c8e7 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -50,7 +51,7 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) /// /// The event is not raised if the current and new value for the target property are the same. /// - protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, newValue)) { @@ -75,7 +76,7 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string /// The instance to use to compare the input values. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(field, newValue)) { @@ -280,7 +281,7 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer< /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); } @@ -300,7 +301,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// /// The event is not raised if the current and new value for the target property are the same. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); } @@ -338,7 +339,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); } @@ -359,7 +360,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// /// The event is not raised if the current and new value for the target property are the same. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index 8e718f68287..fa2804b3282 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -9,6 +9,8 @@ + + @@ -17,6 +19,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs b/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs new file mode 100644 index 00000000000..7725aeeeaf4 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs @@ -0,0 +1,20 @@ +// 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. + +#if NETSTANDARD2_0 + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that an output will not be null even if the corresponding type allows it. + /// Specifies that an input argument was not null when the call returns. + /// + /// Internal copy from the BCL attribute. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} + +#endif \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs b/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs new file mode 100644 index 00000000000..df828c4deec --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs @@ -0,0 +1,27 @@ +// 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. + +#if NETSTANDARD2_0 + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that the output will be non-null if the named parameter is non-null. + /// + /// Internal copy from the BCL attribute. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } +} + +#endif \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs index 5685e3ed413..41fa934cfd6 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -84,7 +85,7 @@ protected void OnPropertyChanging([CallerMemberName] string? propertyName = null /// The and events are not raised /// if the current and new value for the target property are the same. /// - protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null) { // We duplicate the code here instead of calling the overload because we can't // guarantee that the invoked SetProperty will be inlined, and we need the JIT @@ -120,7 +121,7 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string /// The instance to use to compare the input values. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(field, newValue)) { @@ -340,7 +341,7 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer< /// indicates that the new value being assigned to is different than the previous one, /// and it does not mean the new instance passed as argument is in any particular state. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { // We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback. // The lambda expression here is transformed by the C# compiler into an empty closure class with a @@ -368,7 +369,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// The and events are not raised /// if the current and new value for the target property are the same. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); } @@ -407,7 +408,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// indicates that the new value being assigned to is different than the previous one, /// and it does not mean the new instance passed as argument is in any particular state. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); } @@ -430,7 +431,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// The and events are not raised /// if the current and new value for the target property are the same. /// - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) + protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); } diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs index cbff751fea1..23e0d0bb6bb 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs @@ -7,15 +7,9 @@ // This file is inspired from the MvvmLight library (lbugnion/MvvmLight), // more info in ThirdPartyNotices.txt in the root of the project. -// ================================= NOTE ================================= -// This file is mirrored in the ObservableRecipient annotated copy -// (for debugging info) in the Mvvm.SourceGenerators project. -// If any changes are made to this file, they should also be appropriately -// ported to that file as well to keep the behavior consistent. -// ======================================================================== - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.Toolkit.Mvvm.Messaging; using Microsoft.Toolkit.Mvvm.Messaging.Messages; @@ -145,7 +139,7 @@ protected virtual void Broadcast(T oldValue, T newValue, string? propertyName /// the and events /// are not raised if the current and new value for the target property are the same. /// - protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null) { T oldValue = field; @@ -174,7 +168,7 @@ protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMe /// If , will also be invoked. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null) { T oldValue = field; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs index 4e25376064f..a21f36e3de8 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Linq.Expressions; @@ -137,7 +138,7 @@ protected ObservableValidator(ValidationContext validationContext) /// the and events /// are not raised if the current and new value for the target property are the same. /// - protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null) { bool propertyChanged = SetProperty(ref field, newValue, propertyName); @@ -162,7 +163,7 @@ protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMem /// If , will also be validated. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string? propertyName = null) + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string? propertyName = null) { bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);