Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
MVVMTK0057 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | Invalid source name for [DependsOn]
MVVMTK0058 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | Dependency cycle for [DependsOn]
MVVMTK0059 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | Invalid sub-property source for [DependsOn]
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\INotifyPropertyChangedGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\AttributeInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\ChildPropertyChangedSubscriptionInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\INotifyPropertyChangedInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\ObservableRecipientInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\PropertyInfo.cs" />
Expand Down Expand Up @@ -113,4 +114,4 @@
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.SourceGenerators.props" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 CommunityToolkit.Mvvm.SourceGenerators.Helpers;

namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;

/// <summary>
/// A model representing generated child <c>PropertyChanged</c> forwarding for an observable property.
/// </summary>
/// <param name="PropertyChangedNames">The dependent property names to notify when the child instance changes.</param>
/// <param name="NotifiedCommandNames">The dependent command names to notify when the child instance changes.</param>
internal sealed record ChildPropertyChangedSubscriptionInfo(
EquatableArray<string> PropertyChangedNames,
EquatableArray<string> NotifiedCommandNames);
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// <param name="PropertyChangingNames">The sequence of property changing properties to notify.</param>
/// <param name="PropertyChangedNames">The sequence of property changed properties to notify.</param>
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
/// <param name="ChildPropertyChangedSubscriptions">The child property changed subscriptions to generate.</param>
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
Expand All @@ -41,6 +42,7 @@ internal sealed record PropertyInfo(
EquatableArray<string> PropertyChangingNames,
EquatableArray<string> PropertyChangedNames,
EquatableArray<string> NotifiedCommandNames,
EquatableArray<ChildPropertyChangedSubscriptionInfo> ChildPropertyChangedSubscriptions,
bool NotifyPropertyChangedRecipients,
bool NotifyDataErrorInfo,
bool IsOldPropertyValueDirectlyReferenced,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public sealed partial class ObservablePropertyGenerator : IIncrementalGenerator
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Gather diagnostics for [DependsOn] declarations, including cases where no observable property is generated in the target type.
IncrementalValuesProvider<EquatableArray<DiagnosticInfo>> dependsOnDiagnostics =
context.SyntaxProvider
.ForAttributeWithMetadataName(
"CommunityToolkit.Mvvm.ComponentModel.DependsOnAttribute",
static (node, _) => node is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.AttributeLists.Count > 0,
static (context, token) => Execute.GetDependsOnDiagnostics((IPropertySymbol)context.TargetSymbol, context.Attributes, token));

context.ReportDiagnostics(dependsOnDiagnostics);

// Gather info for all annotated fields
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<PropertyInfo?> Info)> propertyInfoWithErrors =
context.ForAttributeWithMetadataNameAndOptions(
Expand Down Expand Up @@ -86,6 +96,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
item.Properties
.Select(Execute.GetPropertySyntax)
.Concat(item.Properties.Select(Execute.GetOnPropertyChangeMethodsSyntax).SelectMany(static l => l))
.Concat(item.Properties.Select(Execute.GetChildPropertyChangedSubscriptionMembersSyntax).SelectMany(static l => l))
.ToImmutableArray();

// Insert all members into the same partial type declaration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,4 +944,52 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "Semi-auto properties should be converted to partial properties using [ObservableProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0056");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a <c>[DependsOn]</c> source property is invalid.
/// <para>
/// Format: <c>"The source property "{0}" for [DependsOn] has no valid match in type {1}"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor DependsOnInvalidSourceError = new DiagnosticDescriptor(
id: "MVVMTK0057",
title: "Invalid source name for [DependsOn]",
messageFormat: "The source property \"{0}\" for [DependsOn] has no valid match in type {1}",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The source properties for [DependsOn] must be different accessible properties in the parent type.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0057");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[DependsOn]</c> declarations create a cycle.
/// <para>
/// Format: <c>"The [DependsOn] declaration for property "{0}" creates a dependency cycle in type {1}"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor DependsOnCycleError = new DiagnosticDescriptor(
id: "MVVMTK0058",
title: "Dependency cycle for [DependsOn]",
messageFormat: "The [DependsOn] declaration for property \"{0}\" creates a dependency cycle in type {1}",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The dependency graph declared with [DependsOn] must not contain cycles.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0058");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[DependsOn(NotifyOnSubPropertyChanges = true)]</c> is used with an invalid source.
/// <para>
/// Format: <c>"The source property "{0}" for [DependsOn(NotifyOnSubPropertyChanges = true)] must be a generated observable property implementing INotifyPropertyChanged in type {1}"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor DependsOnInvalidSubPropertySourceError = new DiagnosticDescriptor(
id: "MVVMTK0059",
title: "Invalid sub-property source for [DependsOn]",
messageFormat: "The source property \"{0}\" for [DependsOn(NotifyOnSubPropertyChanges = true)] must be a generated observable property implementing INotifyPropertyChanged in type {1}",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Sources for [DependsOn(NotifyOnSubPropertyChanges = true)] must be generated observable properties with a type implementing INotifyPropertyChanged.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0059");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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.Linq;

namespace CommunityToolkit.Mvvm.ComponentModel;

/// <summary>
/// An attribute that can be used to declare dependencies for calculated properties.
/// When this attribute is used on a property, generated observable properties listed
/// in <see cref="PropertyNames"/> will also notify the annotated property when changed.
/// </summary>
/// <remarks>
/// This attribute is processed by the MVVM Toolkit source generators.
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
public sealed class DependsOnAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.
/// </summary>
/// <param name="propertyName">The name of the property this calculated property depends on.</param>
public DependsOnAttribute(string propertyName)
{
PropertyNames = new[] { propertyName };
}

/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.
/// </summary>
/// <param name="propertyName">The name of the property this calculated property depends on.</param>
/// <param name="otherPropertyNames">The other property names this calculated property depends on.</param>
public DependsOnAttribute(string propertyName, params string[] otherPropertyNames)
{
PropertyNames = new[] { propertyName }.Concat(otherPropertyNames).ToArray();
}

/// <summary>
/// Gets the property names this calculated property depends on.
/// </summary>
public string[] PropertyNames { get; }

/// <summary>
/// Gets or sets whether changes from child <see cref="System.ComponentModel.INotifyPropertyChanged"/> instances should also notify dependents.
/// </summary>
public bool NotifyOnSubPropertyChanges { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,80 @@ partial class MyViewModel
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
}

[TestMethod]
public void DependsOnGeneratedDependencyGraph_IncludesTransitiveNotificationsAndCommandInvalidation()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyApp;

partial class MyViewModel : ObservableObject
{
[ObservableProperty]
private string? name;

[DependsOn(nameof(Name))]
public bool CanSave => !string.IsNullOrWhiteSpace(Name);

[DependsOn(nameof(CanSave))]
public string State => CanSave ? "Ready" : "Blocked";

[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
}
}
""";

VerifyGeneratedSourceContains(
source,
new IIncrementalGenerator[] { new ObservablePropertyGenerator(), new RelayCommandGenerator() },
"MyApp.MyViewModel.g.cs",
[
"OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name);",
"OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.CanSave);",
"OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.State);",
"SaveCommand.NotifyCanExecuteChanged();"
]);
}

[TestMethod]
public void DependsOnGeneratedDependencyGraph_IncludesChildSubscriptionHelpers()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp;

partial class ChildViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}

partial class MyViewModel : ObservableObject
{
[ObservableProperty]
private ChildViewModel? selectedItem;

[DependsOn(nameof(SelectedItem), NotifyOnSubPropertyChanges = true)]
public string? SelectedItemName => SelectedItem?.Name;
}
""";

VerifyGeneratedSourceContains(
source,
new[] { new ObservablePropertyGenerator() },
"MyApp.MyViewModel.g.cs",
[
"PropertyChanged +=",
"PropertyChanged -=",
"OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.SelectedItemName);"
]);
}

[TestMethod]
public void ObservablePropertyWithNonNullableUnconstrainedGenericType_EmitsMemberNotNullAttribute()
{
Expand Down Expand Up @@ -3525,4 +3599,49 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies()
GC.KeepAlive(observableObjectType);
GC.KeepAlive(validationAttributeType);
}

/// <summary>
/// Generates sources and verifies that a generated file contains a set of snippets.
/// </summary>
/// <param name="source">The input source to process.</param>
/// <param name="generators">The generators to apply to the input syntax tree.</param>
/// <param name="filename">The generated filename to inspect.</param>
/// <param name="snippets">The snippets expected in the generated source.</param>
private static void VerifyGeneratedSourceContains(string source, IIncrementalGenerator[] generators, string filename, string[] snippets)
{
// Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded
Type observableObjectType = typeof(ObservableObject);
Type validationAttributeType = typeof(ValidationAttribute);

IEnumerable<MetadataReference> 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));

CSharpCompilation compilation = CSharpCompilation.Create(
"original",
[syntaxTree],
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

GeneratorDriver driver = CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);

_ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> diagnostics);

CollectionAssert.AreEquivalent(Array.Empty<Diagnostic>(), diagnostics);

SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filename);
string generatedText = generatedTree.ToString();

foreach (string snippet in snippets)
{
StringAssert.Contains(generatedText, snippet);
}

GC.KeepAlive(observableObjectType);
GC.KeepAlive(validationAttributeType);
}
}
Loading