diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8fc2c32f..ab64c9f19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -354,4 +354,4 @@ jobs: if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }} with: name: linux-logs - path: ./**/*.*log + path: ./**/*.*log \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs index ed86c952a..3cdd1dc31 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs @@ -34,7 +34,7 @@ public static bool IsUwpTarget(Compilation compilation, AnalyzerConfigOptions an { if (bool.TryParse(propertyValue, out bool useUwpTools)) { - return true; + return useUwpTools; } } diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs index f85e2a73b..9764606a8 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs @@ -78,7 +78,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); // Gather all interfaces, and only enable this branch if the target is a UWP app (the host) - IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceHostInfo = + IncrementalValuesProvider<(HierarchyInfo, AppServiceInfo)> appServiceHostInfo = context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.AppServices.AppServiceAttribute", static (node, _) => node is InterfaceDeclarationSyntax, @@ -105,6 +105,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); + // Gather all methods for the app service type ImmutableArray methods = MethodInfo.From(typeSymbol, token); token.ThrowIfCancellationRequested(); @@ -113,8 +114,47 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }) .Where(static item => item.Hierarchy is not null); - // Produce the host type - context.RegisterSourceOutput(appServiceHostInfo, static (context, item) => + // Also gather all explicitly requested host implementation types + IncrementalValuesProvider<(HierarchyInfo, AppServiceInfo)> additionalAppServiceHostInfo = + context.ForAttributeWithMetadataNameAndOptions( + "CommunityToolkit.AppServices.GeneratedAppServiceHostAttribute", + static (node, _) => true, + static (context, token) => + { + // Only retrieve host info if the target is a UWP application + if (!Helpers.IsUwpTarget(context.SemanticModel.Compilation, context.GlobalOptions)) + { + return default; + } + + // Get the target interface + if (context.Attributes[0].ConstructorArguments is not [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol appServiceType }]) + { + return default; + } + + // Check if the current interface is in fact an app service type + if (!appServiceType.TryGetAppServicesNameFromAttribute(out string? appServiceName)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + HierarchyInfo hierarchy = HierarchyInfo.From(appServiceType, appServiceType.Name.Substring(1)); + + token.ThrowIfCancellationRequested(); + + ImmutableArray methods = MethodInfo.From(appServiceType, token); + + token.ThrowIfCancellationRequested(); + + return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName, appServiceType.GetFullyQualifiedName())); + }) + .Where(static item => item.Hierarchy is not null); + + // Shared helper to emit all discovered types + static void GenerateAppServiceHostType(SourceProductionContext context, (HierarchyInfo Hierarchy, AppServiceInfo Info) item) { ConstructorDeclarationSyntax constructorSyntax = Host.GetConstructorSyntax(item.Hierarchy, item.Info); ImmutableArray methodDeclarations = Host.GetMethodDeclarationsSyntax(item.Info); @@ -126,6 +166,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) $"/// A generated host implementation for the interface."); context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8)); - }); + } + + // Produce the host types + context.RegisterSourceOutput(appServiceHostInfo, GenerateAppServiceHostType); + context.RegisterSourceOutput(additionalAppServiceHostInfo, GenerateAppServiceHostType); } } diff --git a/components/AppServices/src/AppServiceHost.cs b/components/AppServices/src/AppServiceHost.cs index 28cb0b727..cd193e855 100644 --- a/components/AppServices/src/AppServiceHost.cs +++ b/components/AppServices/src/AppServiceHost.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.AppServices.Helpers; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.AppService; @@ -15,7 +16,7 @@ using Windows.Foundation.Collections; using Windows.Foundation.Metadata; using Windows.System.Profile; -using CommunityToolkit.AppServices.Helpers; +using Windows.UI.Core.Preview; #pragma warning disable CA1068 @@ -143,6 +144,58 @@ public bool OnBackgroundActivated(BackgroundActivatedEventArgs args) return true; } + /// + /// Handles the app service host shutdown when is raised. + /// + /// The args for the close request. + /// + /// + /// This method should be used as follows (from App.xaml.cs): + /// + /// private void OnCloseRequested(object? sender, SystemNavigationCloseRequestedPreviewEventArgs e) + /// { + /// // Any other work, possibly marking the request as handled + /// + /// DesktopExtension.OnCloseRequested(e); + /// } + /// + /// + /// + /// The app might be holding a deferral for the app service connection to the extension process, which is currently only completed when the + /// connection is closed. This means that when the application is closed, that deferral will actually try to keep the connection alive, until + /// the OS will eventually force terminate it. This will cause following launches of the app to be delayed until the previous process is + /// completely gone, meaning that closing the app and immediately reopening it will cause it to remain stuck at the splash screen for a few + /// seconds. Note that during this time, no app code is actually executed, it's just that the OS is waiting to terminate the existing connection + /// and fully close the previous instance before allowing a new one to be started. To avoid this issue, this method takes care of fully closing + /// any existing connection (by canceling its associated deferral), when the app is about to exit. This avoids the OS timeout for the connection. + /// + /// + public void OnCloseRequested(SystemNavigationCloseRequestedPreviewEventArgs args) + { + // Do nothing if the close request has been handled + if (args.Handled) + { + return; + } + + // Remove the registered connection handlers + if (_appServiceConnection is { } appServiceConnection) + { + appServiceConnection.ServiceClosed -= AppServiceConnection_ServiceClosed; + appServiceConnection.RequestReceived -= AppServiceConnection_RequestReceived; + + _appServiceConnection = null; + } + + // Cancel the deferral, if present + if (_appServiceDeferral is { } appServiceDeferral) + { + appServiceDeferral.Complete(); + + _appServiceDeferral = null; + } + } + /// /// Creates a new for a given operation. /// diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index fa98329b0..b30639268 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -6,7 +6,13 @@ true CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) + false false + false + + + false + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; @@ -29,7 +35,7 @@ - + Windows Desktop Extensions for the UWP diff --git a/components/AppServices/src/GeneratedAppServiceHostAttribute.cs b/components/AppServices/src/GeneratedAppServiceHostAttribute.cs new file mode 100644 index 000000000..9b5d251af --- /dev/null +++ b/components/AppServices/src/GeneratedAppServiceHostAttribute.cs @@ -0,0 +1,28 @@ +// 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; + +namespace CommunityToolkit.AppServices; + +/// +/// An attribute that can be used to request the generator to emit a host implementation of a given app service. +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] +public sealed class GeneratedAppServiceHostAttribute : Attribute +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The type of the app service. + public GeneratedAppServiceHostAttribute(Type appServiceType) + { + AppServiceType = appServiceType; + } + + /// + /// Gets the type of the app service. + /// + public Type AppServiceType { get; } +} diff --git a/components/DependencyPropertyGenerator/.gitattributes b/components/DependencyPropertyGenerator/.gitattributes new file mode 100644 index 000000000..64d6ecc10 --- /dev/null +++ b/components/DependencyPropertyGenerator/.gitattributes @@ -0,0 +1,10 @@ +# All file types: +# - Treat as text +# - Normalize to LF line endings +* text=auto eol=lf + +# Explicit settings for well known types +*.cs text eol=lf +*.csproj text eol=lf +*.projitems text eol=lf +*.shprroj text eol=lf \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj new file mode 100644 index 000000000..76b6c0f7c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + enable + true + true + + + $(NoWarn);IDE0130 + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs new file mode 100644 index 000000000..80b5ffc19 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs @@ -0,0 +1,155 @@ +// 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.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A code fixer that updates property declarations to be fields instead, for dependency properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseFieldDeclarationCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [DependencyPropertyFieldDeclarationId]; + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the property declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration) + { + // We only support this code fix for static properties without modifiers and attributes + if (!IsCodeFixSupportedForPropertyDeclaration(propertyDeclaration)) + { + return; + } + + // We can now register the code fix to convert the property into a field + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare dependency property as field", + createChangedDocument: token => ConvertDependencyPropertyToFieldDeclaration(context.Document, root, propertyDeclaration), + equivalenceKey: "Declare dependency property as field"), + diagnostic); + } + } + + /// + /// Checks whether the code fixer can be applied to a target property declaration. + /// + /// The to update. + /// Whether the code fixer can be applied to . + private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclarationSyntax propertyDeclaration) + { + // We don't support properties with attributes, as those might not work on fields and need special handling + if (propertyDeclaration.AttributeLists.Count > 0) + { + return false; + } + + foreach (SyntaxToken modifier in propertyDeclaration.Modifiers) + { + // Accessibility modifiers are allowed (the property will however become public) + if (SyntaxFacts.IsAccessibilityModifier(modifier.Kind())) + { + continue; + } + + // If the property is abstract or an override, or other weird things (which shouldn't really happen), we don't support it + if (modifier.Kind() is SyntaxKind.AbstractKeyword or SyntaxKind.OverrideKeyword or SyntaxKind.PartialKeyword or SyntaxKind.ExternKeyword) + { + return false; + } + } + + // Properties with an expression body are supported and will be converted to field initializers + if (propertyDeclaration.ExpressionBody is not null) + { + return true; + } + + // The property must have at least an accessor + if (propertyDeclaration.AccessorList is not { Accessors.Count: > 0 } accessorList) + { + return false; + } + + // One of the accessors must be a getter + if (!accessorList.Accessors.Any(accessor => accessor.IsKind(SyntaxKind.GetAccessorDeclaration))) + { + return false; + } + + return true; + } + + /// + /// Applies the code fix to a target property declaration and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The to update. + /// An updated document with the applied code fix. + private static async Task ConvertDependencyPropertyToFieldDeclaration(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration) + { + await Task.CompletedTask; + + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + syntaxEditor.ReplaceNode(propertyDeclaration, (node, generator) => + { + // If the property had an initializer, carry that over + ExpressionSyntax? initializerExpression = propertyDeclaration switch + { + { ExpressionBody.Expression: { } arrowExpression } => arrowExpression, + { Initializer.Value: { } equalsExpression } => equalsExpression, + _ => null + }; + + // Create the field declaration and make it 'public static readonly' (same as the other analyzer) + SyntaxNode updatedNode = generator.FieldDeclaration( + name: propertyDeclaration.Identifier.Text, + type: propertyDeclaration.Type, + accessibility: Accessibility.Public, + modifiers: DeclarationModifiers.Static | DeclarationModifiers.ReadOnly, + initializer: initializerExpression); + + // Keep the 'new' modifier, if needed + if (propertyDeclaration.Modifiers.Any(SyntaxKind.NewKeyword)) + { + updatedNode = generator.WithModifiers(updatedNode, generator.GetModifiers(updatedNode).WithIsNew(true)); + } + + return updatedNode.WithTriviaFrom(propertyDeclaration); + }); + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs new file mode 100644 index 000000000..5e750ab4b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs @@ -0,0 +1,94 @@ +// 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.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A code fixer that updates field declarations to ensure they follow the recommended rules for dependency properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseFieldDeclarationCorrectlyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [IncorrectDependencyPropertyFieldDeclarationId]; + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the the field declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { } fieldDeclaration) + { + // Register the code fix to update the field to be correctly declared + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare dependency property field correctly", + createChangedDocument: token => FixDependencyPropertyFieldDeclaration(context.Document, root, fieldDeclaration), + equivalenceKey: "Declare dependency property field correctly"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target field declaration and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The to update. + /// An updated document with the applied code fix. + private static async Task FixDependencyPropertyFieldDeclaration(Document document, SyntaxNode root, FieldDeclarationSyntax fieldDeclaration) + { + await Task.CompletedTask; + + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + // We use the lambda overload mostly for convenient, so we can easily get a generator to use + syntaxEditor.ReplaceNode(fieldDeclaration, (node, generator) => + { + // Keep the original node to get the trivia back from it + SyntaxNode originalNode = node; + + // Update the field to ensure it's declared as 'public static readonly' + node = generator.WithAccessibility(node, Accessibility.Public); + node = generator.WithModifiers(node, DeclarationModifiers.Static | DeclarationModifiers.ReadOnly); + + // If the type is declared as nullable, unwrap it and remove the annotation. + // We need to make sure to carry the space after the element type. When the + // type is nullable, that space is attached to the question mark token. + if (((FieldDeclarationSyntax)node).Declaration is { Type: NullableTypeSyntax { ElementType: { } fieldElementType } nullableType } variableDeclaration) + { + TypeSyntax typeDeclaration = fieldElementType.WithTrailingTrivia(nullableType.QuestionToken.TrailingTrivia); + + node = ((FieldDeclarationSyntax)node).WithDeclaration(variableDeclaration.WithType(typeDeclaration)); + } + + return node.WithTriviaFrom(originalNode); + }); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..6b222901d --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,664 @@ +// 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.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A code fixer that converts manual properties into partial properties using [GeneratedDependencytProperty]. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseGeneratedDependencyPropertyOnManualPropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [UseGeneratedDependencyPropertyForManualPropertyId]; + + /// + public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider() + { + return new FixAllProvider(); + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + + // Get all additional locations we expect from the analyzer + if (!TryGetAdditionalLocations( + diagnostic, + out Location? fieldLocation, + out Location? propertyTypeExpressionLocation, + out Location? defaultValueExpressionLocation)) + { + return; + } + + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the property declaration and the field declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration && + root.FindNode(fieldLocation.SourceSpan) is FieldDeclarationSyntax fieldDeclaration) + { + // Get the semantic model, as we need to resolve symbols + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // Register the code fix to update the semi-auto property to a partial property + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty( + context.Document, + semanticModel, + root, + propertyDeclaration, + fieldDeclaration, + propertyTypeExpressionLocation, + defaultValue, + defaultValueTypeReferenceId, + defaultValueExpressionLocation), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Tries to get an for the [GeneratedDependencyProperty] attribute. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The resulting attribute list, if successfully retrieved. + /// Whether could be retrieved successfully. + private static bool TryGetGeneratedDependencyPropertyAttributeList( + Document document, + SemanticModel semanticModel, + [NotNullWhen(true)] out AttributeListSyntax? generatedDependencyPropertyAttributeList) + { + // Make sure we can resolve the '[GeneratedDependencyProperty]' attribute + if (semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute) is not INamedTypeSymbol attributeSymbol) + { + generatedDependencyPropertyAttributeList = null; + + return false; + } + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new '[GeneratedDependencyProperty]' attribute here too + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(attributeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + + return true; + } + + /// + /// Updates an for the [GeneratedDependencyProperty] attribute with the right default value. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The original tree root belonging to the current document. + /// The location of the property type expression to use in metadata, if available. + /// The expression for the default value of the property, if present + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. + /// The updated attribute syntax. + private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + AttributeListSyntax generatedDependencyPropertyAttributeList, + Location propertyTypeExpressionLocation, + string? defaultValueExpression, + string? defaultValueTypeReferenceId, + Location defaultValueExpressionLocation) + { + void HandlePropertyType(ref AttributeListSyntax generatedDependencyPropertyAttributeList) + { + // If the property needs an explicit type in metadata, just carry it over from the original field declaration initializer + if (root.FindNode(propertyTypeExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } propertyTypeOriginalExpression }) + { + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("PropertyType", propertyTypeOriginalExpression)]); + } + } + + void HandleDefaultValue(ref AttributeListSyntax generatedDependencyPropertyAttributeList) + { + // If we do have a default value expression, set it in the attribute. + // We extract the generated attribute so we can add the new argument. + // It's important to reuse it, as it has the "add usings" annotation. + if (defaultValueExpression is not null) + { + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Special case for 'UnsetValue', we need to convert to the 'GeneratedDependencyProperty' type + if (defaultValueExpression == $"\"{UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.UnsetValueSpecialIdentifier}\"") + { + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", ParseExpression("GeneratedDependencyProperty.UnsetValue"))]); + + return; + } + + // Special case if we have a location for the original expression, and we can resolve the node. + // In this case, we want to just carry that over with no changes (this is used for named constants). + // See notes below for how this method is constructing the new attribute argument to insert. + if (root.FindNode(defaultValueExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } defaultValueOriginalExpression }) + { + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", defaultValueOriginalExpression)]); + + return; + } + + ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); + + // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'. + // We have two cases to handle, which require different logic to ensure the correct tree is always generated. + // For nested enum types, we'll have a reference id. In this case, we manually insert annotations. + if (defaultValueTypeReferenceId is not null) + { + // Here we're relying on the special 'SymbolId' annotation, which is used internally by Roslyn to track + // necessary imports for type expressions. We need to rely on this implementation detail here because + // there is no public API to correctly produce a tree for an enum member access on a nested type. + // This internal detail is one that many generators take a dependency on already, so it's safe-ish. + parsedExpression = parsedExpression.WithAdditionalAnnotations( + Simplifier.Annotation, + Simplifier.AddImportsAnnotation, + new SyntaxAnnotation("SymbolId", defaultValueTypeReferenceId)); + + // Create the attribute argument to insert + SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", parsedExpression); + + // Actually add the argument to the existing attribute syntax + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + + return; + } + + // For normal enum member accesses, we resolve the type and then construct the tree from that expression. + if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + { + string fullyQualifiedMetadataName = expressionSyntax.ToFullString(); + + // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name) + if (fullyQualifiedMetadataName.StartsWith("global::")) + { + fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..]; + } + + // Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not + // a fully qualified type name. However, for virtually all cases for enum types, the two should match. + // That is, they will be the same if the type is not nested, and not generic, which is what we expect. + if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol) + { + // Create the identifier syntax for the enum type, with the right annotations + SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + + // Create the member access expression for the target enum type + SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName); + + // Create the attribute argument, like in the previous case + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax)]); + + return; + } + } + + // Otherwise, just add the new default value normally + generatedDependencyPropertyAttributeList = + AttributeList(SingletonSeparatedList( + generatedDependencyPropertyAttributeList.Attributes[0] + .AddArgumentListArguments( + AttributeArgument(ParseExpression(defaultValueExpression)) + .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + } + } + + HandlePropertyType(ref generatedDependencyPropertyAttributeList); + HandleDefaultValue(ref generatedDependencyPropertyAttributeList); + + return generatedDependencyPropertyAttributeList; + } + + /// + /// Applies the code fix to a target property declaration and returns an updated document. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The original tree root belonging to the current document. + /// The for the property being updated. + /// The for the declared property to remove. + /// The location of the property type expression to use in metadata, if available. + /// The expression for the default value of the property, if present + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + FieldDeclarationSyntax fieldDeclaration, + Location propertyTypeExpressionLocation, + string? defaultValueExpression, + string? defaultValueTypeReferenceId, + Location defaultValueExpressionLocation) + { + await Task.CompletedTask; + + // If we can't generate the new attribute list, bail (this should never happen) + if (!TryGetGeneratedDependencyPropertyAttributeList(document, semanticModel, out AttributeListSyntax? generatedDependencyPropertyAttributeList)) + { + return document; + } + + // Create an editor to perform all mutations + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + ConvertToPartialProperty( + document, + semanticModel, + root, + propertyDeclaration, + fieldDeclaration, + generatedDependencyPropertyAttributeList, + syntaxEditor, + propertyTypeExpressionLocation, + defaultValueExpression, + defaultValueTypeReferenceId, + defaultValueExpressionLocation); + + RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The original tree root belonging to the current document. + /// The for the property being updated. + /// The for the declared property to remove. + /// The with the attribute to add. + /// The instance to use. + /// The location of the property type expression to use in metadata, if available. + /// The expression for the default value of the property, if present + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static void ConvertToPartialProperty( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + FieldDeclarationSyntax fieldDeclaration, + AttributeListSyntax generatedDependencyPropertyAttributeList, + SyntaxEditor syntaxEditor, + Location propertyTypeExpressionLocation, + string? defaultValueExpression, + string? defaultValueTypeReferenceId, + Location defaultValueExpressionLocation) + { + // Replace the property with the partial property using the attribute. Note that it's important to use the + // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and + // replacing the original one. Doing that would cause the following 'ReplaceNode' call to adjust the leading + // trivia of trailing members after the fields being removed to not work incorrectly, and fail to be resolved. + syntaxEditor.ReplaceNode(propertyDeclaration, (node, _) => + { + PropertyDeclarationSyntax propertyDeclaration = (PropertyDeclarationSyntax)node; + + // Update the attribute to insert with the default value, if present + generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList( + document, + semanticModel, + root, + generatedDependencyPropertyAttributeList, + propertyTypeExpressionLocation, + defaultValueExpression, + defaultValueTypeReferenceId, + defaultValueExpressionLocation); + + // Start setting up the updated attribute lists + SyntaxList attributeLists = propertyDeclaration.AttributeLists; + + if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) + { + // Remove the trivia from the original first attribute + attributeLists = attributeLists.Replace( + nodeInList: firstAttributeListSyntax, + newNode: firstAttributeListSyntax.WithoutTrivia()); + + // If the property has at least an attribute list, move the trivia from it to the new attribute + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + + // Insert the new attribute + attributeLists = attributeLists.Insert(0, generatedDependencyPropertyAttributeList); + } + else + { + // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(propertyDeclaration); + + // Save the new attribute list + attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); + } + + // Append any attributes we want to forward (any attributes on the field, they've already been validated). + // We also need to strip all trivia, to avoid accidentally carrying over XML docs from the field declaration. + foreach (AttributeListSyntax fieldAttributeList in fieldDeclaration.AttributeLists) + { + attributeLists = attributeLists.Add(fieldAttributeList.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.StaticKeyword))).WithoutTrivia()); + } + + // Get a new property that is partial and with semicolon token accessors + return + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithoutLeadingTrivia() + .WithAttributeLists(attributeLists) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAccessorList(AccessorList(List( + [ + // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only + propertyDeclaration.AccessorList!.Accessors[0] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation), + propertyDeclaration.AccessorList!.Accessors[1] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); + }); + + // Also remove the field declaration (it'll be generated now) + syntaxEditor.RemoveNode(fieldDeclaration); + + // Find the parent type for the property (we need to do this for all ancestor types, as the type might be bested) + for (TypeDeclarationSyntax? typeDeclaration = propertyDeclaration.FirstAncestor(); + typeDeclaration is not null; + typeDeclaration = typeDeclaration.FirstAncestor()) + { + // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). + // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + } + } + } + + /// + /// Removes any leftover leading end of lines on remaining members following any removed fields. + /// + /// The collection of all fields that have been removed. + /// The instance to use. + private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection fieldDeclarations, SyntaxEditor syntaxEditor) + { + foreach (FieldDeclarationSyntax fieldDeclaration in fieldDeclarations) + { + // Special handling for the leading trivia of members following the field declaration we are about to remove. + // There is an edge case that can happen when a type declaration is as follows: + // + // class ContainingType + // { + // public static readonly DependencyProperty NameProperty = ...; + // + // public void SomeOtherMember() { } + // + // public string? Name { ... } + // } + // + // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty') + // will cause an extra blank line to be left after the edits, right above the member immediately following the field. + // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line. + if (fieldDeclaration.Parent is not TypeDeclarationSyntax fieldParentTypeDeclaration) + { + continue; + } + + int fieldDeclarationIndex = fieldParentTypeDeclaration.Members.IndexOf(fieldDeclaration); + + // Check whether there is a member immediatley following the field + if (fieldDeclarationIndex == -1 || fieldDeclarationIndex >= fieldParentTypeDeclaration.Members.Count - 1) + { + continue; + } + + MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; + + // It's especially important to skip members that have been removed. This would otherwise fail when computing + // the final document. We only care about fixing trivia for members that will still be present after all edits. + if (fieldDeclarations.Contains(nextMember)) + { + continue; + } + + SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); + + // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one + if (leadingTrivia is not [SyntaxTrivia(SyntaxKind.EndOfLineTrivia), ..]) + { + continue; + } + + bool hasAnyPersistentPrecedingMemberDeclarations = false; + + // Last check: we only want to actually remove the end of line if there are no other members before the current + // one, that have persistend in the containing type after all edits. If that is not the case, that is, if there + // are other members before the current one, we want to keep that end of line. Otherwise, we'd end up with the + // current member being incorrectly declared right after the previous one, without a separating blank line. + for (int i = 0; i < fieldDeclarationIndex + 1; i++) + { + hasAnyPersistentPrecedingMemberDeclarations |= !fieldDeclarations.Contains(fieldParentTypeDeclaration.Members[i]); + } + + // If there's any other persistent members, stop here + if (hasAnyPersistentPrecedingMemberDeclarations) + { + continue; + } + + // Finally, we can actually remove this end of line trivia, as we're sure it's not actually intended + syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); + } + } + + /// + /// Gets the additional locations provided by the analyzer. + /// + /// The instance currently being processed. + /// The location of the field to remove. + /// The location of the property type expression to use in metadata. + /// The location for the default value. + /// Whether the additional locations were retrieved correctly. + private static bool TryGetAdditionalLocations( + Diagnostic diagnostic, + [NotNullWhen(true)] out Location? fieldLocation, + [NotNullWhen(true)] out Location? propertyTypeExpressionLocation, + [NotNullWhen(true)] out Location? defaultValueExpressionLocation) + { + // Ensure we have the additional location kind, and parse it + if (!Enum.TryParse( + diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.AdditionalLocationKindPropertyName], + out AdditionalLocationKind additionalLocationKind)) + { + fieldLocation = null; + propertyTypeExpressionLocation = null; + defaultValueExpressionLocation = null; + + return false; + } + + int currentLocationIndex = 0; + + // Helper to extract an additional location with a specified kind + bool TryExtractAdditionalLocation(AdditionalLocationKind currentLocationKind, [NotNullWhen(true)] out Location? fieldLocation) + { + // Ensure the current kind is present and that we can extract an additional location + if (!additionalLocationKind.HasFlag(currentLocationKind)) + { + fieldLocation = null; + + return false; + } + + // Parse the additional location + fieldLocation = diagnostic.AdditionalLocations[currentLocationIndex++]; + + return true; + } + + // We always expect to have the field location + if (!TryExtractAdditionalLocation(AdditionalLocationKind.FieldLocation, out fieldLocation)) + { + fieldLocation = null; + propertyTypeExpressionLocation = null; + defaultValueExpressionLocation = null; + + return false; + } + + // Try to extract all optional additional locations + _ = TryExtractAdditionalLocation(AdditionalLocationKind.PropertyTypeExpressionLocation, out propertyTypeExpressionLocation); + _ = TryExtractAdditionalLocation(AdditionalLocationKind.DefaultValueExpressionLocation, out defaultValueExpressionLocation); + + // None of the additional locations should ever be 'null' + propertyTypeExpressionLocation ??= Location.None; + defaultValueExpressionLocation ??= Location.None; + + return true; + } + + /// + /// A custom with the logic from . + /// + private sealed class FixAllProvider : DocumentBasedFixAllProvider + { + /// + protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + // Get the semantic model, as we need to resolve symbols + if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel) + { + return document; + } + + // Get the document root (this should always succeed) + if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root) + { + return document; + } + + // If we can't generate the new attribute list, bail (this should never happen) + if (!TryGetGeneratedDependencyPropertyAttributeList(document, semanticModel, out AttributeListSyntax? generatedDependencyPropertyAttributeList)) + { + return document; + } + + // Create an editor to perform all mutations (across all edits in the file) + SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services); + + // Create the set to track all fields being removed, to adjust whitespaces + HashSet fieldDeclarations = []; + + // Step 1: rewrite all properties and remove the fields + foreach (Diagnostic diagnostic in diagnostics) + { + // Get the current property declaration for the diagnostic + if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration) + { + continue; + } + + // Get all additional locations we expect from the analyzer + if (!TryGetAdditionalLocations( + diagnostic, + out Location? fieldLocation, + out Location? propertyTypeExpressionLocation, + out Location? defaultValueExpressionLocation)) + { + continue; + } + + // Also check that we can find the target field to remove + if (root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) + { + continue; + } + + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; + + ConvertToPartialProperty( + document, + semanticModel, + root, + propertyDeclaration, + fieldDeclaration, + generatedDependencyPropertyAttributeList, + syntaxEditor, + propertyTypeExpressionLocation, + defaultValue, + defaultValueTypeReferenceId, + defaultValueExpressionLocation); + + fieldDeclarations.Add(fieldDeclaration); + } + + // Step 2: remove any leftover leading end of lines on members following fields that have been removed + RemoveLeftoverLeadingEndOfLines(fieldDeclarations, syntaxEditor); + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..e8f0ff5f4 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,41 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +WCTDPG0001 | DependencyPropertyGenerator | Error | +WCTDPG0002 | DependencyPropertyGenerator | Error | +WCTDPG0003 | DependencyPropertyGenerator | Error | +WCTDPG0004 | DependencyPropertyGenerator | Error | +WCTDPG0005 | DependencyPropertyGenerator | Error | +WCTDPG0006 | DependencyPropertyGenerator | Error | +WCTDPG0007 | DependencyPropertyGenerator | Error | +WCTDPG0008 | DependencyPropertyGenerator | Error | +WCTDPG0009 | DependencyPropertyGenerator | Warning | +WCTDPG0010 | DependencyPropertyGenerator | Warning | +WCTDPG0011 | DependencyPropertyGenerator | Warning | +WCTDPG0012 | DependencyPropertyGenerator | Error | +WCTDPG0013 | DependencyPropertyGenerator | Error | +WCTDPG0014 | DependencyPropertyGenerator | Error | +WCTDPG0015 | DependencyPropertyGenerator | Error | +WCTDPG0016 | DependencyPropertyGenerator | Info | +WCTDPG0017 | DependencyPropertyGenerator | Info | +WCTDPG0018 | DependencyPropertyGenerator | Error | +WCTDPG0019 | DependencyPropertyGenerator | Error | +WCTDPG0020 | DependencyPropertyGenerator | Warning | +WCTDPG0021 | DependencyPropertyGenerator | Warning | +WCTDPG0022 | DependencyPropertyGenerator | Warning | +WCTDPG0023 | DependencyPropertyGenerator | Error | +WCTDPG0024 | DependencyPropertyGenerator | Warning | +WCTDPG0025 | DependencyPropertyGenerator | Warning | +WCTDPG0026 | DependencyPropertyGenerator | Warning | +WCTDPG0027 | DependencyPropertyGenerator | Warning | +WCTDPG0028 | DependencyPropertyGenerator | Warning | +WCTDPG0029 | DependencyPropertyGenerator | Warning | +WCTDPG0030 | DependencyPropertyGenerator | Warning | +WCTDPG0031 | DependencyPropertyGenerator | Warning | +WCTDPG0032 | DependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..17d4678ce --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,2 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj new file mode 100644 index 000000000..994ec7f6f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -0,0 +1,33 @@ + + + netstandard2.0 + enable + true + true + + + $(NoWarn);IDE0130 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs new file mode 100644 index 000000000..f5f81c523 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs @@ -0,0 +1,21 @@ +// 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. + +namespace CommunityToolkit.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for properties used by source generators and analyzers. +/// +internal static class WellKnownPropertyNames +{ + /// + /// The MSBuild property to control the XAML mode. + /// + public const string DependencyPropertyGeneratorUseWindowsUIXaml = nameof(DependencyPropertyGeneratorUseWindowsUIXaml); + + /// + /// The MSBuild property to control whether the project is a WinRT component. + /// + public const string CsWinRTComponent = nameof(CsWinRTComponent); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs new file mode 100644 index 000000000..36502c28f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs @@ -0,0 +1,21 @@ +// 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. + +namespace CommunityToolkit.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for tracking steps, to test the incremental generators. +/// +internal static class WellKnownTrackingNames +{ + /// + /// The initial transform node. + /// + public const string Execute = nameof(Execute); + + /// + /// The filtered transform with just output sources. + /// + public const string Output = nameof(Output); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs new file mode 100644 index 000000000..da021cad5 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs @@ -0,0 +1,97 @@ +// 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. + +namespace CommunityToolkit.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for types used by source generators and analyzers. +/// +internal static class WellKnownTypeNames +{ + /// + /// The fully qualified type name for the [GeneratedDependencyProperty] type. + /// + public const string GeneratedDependencyPropertyAttribute = "CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"; + + /// + /// The fully qualified name for the GeneratedDependencyProperty type. + /// + public const string GeneratedDependencyProperty = "CommunityToolkit.WinUI.GeneratedDependencyProperty"; + + /// + /// The fully qualified name for the Windows.UI.Xaml namespace. + /// + public const string WindowsUIXamlNamespace = "Windows.UI.Xaml"; + + /// + /// The fully qualified name for the Microsoft.UI.Xaml namespace. + /// + public const string MicrosoftUIXamlNamespace = "Microsoft.UI.Xaml"; + + /// + /// Gets the fully qualified name for theXAML namespace. + /// + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + public static string XamlNamespace(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? WindowsUIXamlNamespace + : MicrosoftUIXamlNamespace; + } + + /// + /// Gets the fully qualified type name for the DependencyObject type for a given XAML mode. + /// + /// + public static string DependencyObject(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyObject)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyObject)}"; + } + + /// + /// Gets the fully qualified type name for the DependencyProperty type. + /// + /// + public static string DependencyProperty(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyProperty)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyProperty)}"; + } + + /// + /// Gets the fully qualified type name for the DependencyPropertyChangedEventArgs type. + /// + /// + public static string DependencyPropertyChangedEventArgs(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}"; + } + + /// + /// Gets the fully qualified type name for the PropertyMetadata type. + /// + /// + public static string PropertyMetadata(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(PropertyMetadata)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(PropertyMetadata)}"; + } + + /// + /// Gets the fully qualified type name for the CreateDefaultValueCallback type. + /// + /// + public static string CreateDefaultValueCallback(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}"; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs new file mode 100644 index 000000000..ec89759ea --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -0,0 +1,1115 @@ +// 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.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +partial class DependencyPropertyGenerator +{ + /// + /// A container for all the logic for . + /// + private static partial class Execute + { + /// + /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. + /// + /// The input value to use to emit sources. + public static void GeneratePostInitializationSources(IncrementalGeneratorPostInitializationContext context) + { + void GenerateSource(string typeName) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + string fileName = $"{typeName}.g.cs"; + string sourceText; + + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)) + using (StreamReader reader = new(stream)) + { + sourceText = reader.ReadToEnd(); + } + + context.CancellationToken.ThrowIfCancellationRequested(); + + string updatedSourceText = sourceText + .Replace("", GeneratorName) + .Replace("", typeof(Execute).Assembly.GetName().Version.ToString()); + + context.CancellationToken.ThrowIfCancellationRequested(); + + context.AddSource(fileName, updatedSourceText); + } + + GenerateSource("GeneratedDependencyProperty"); + GenerateSource("GeneratedDependencyPropertyAttribute"); + } + + /// + /// Checks whether an input syntax node is a candidate property declaration for the generator. + /// + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidateSyntaxValid(SyntaxNode node, CancellationToken token) + { + // Initial check that's identical to the analyzer + if (!InvalidPropertySyntaxDeclarationAnalyzer.IsValidPropertyDeclaration(node)) + { + return false; + } + + // Make sure that all containing types are partial, otherwise declaring a partial property + // would not be valid. We don't need to emit diagnostics here, the compiler will handle that. + for (TypeDeclarationSyntax? parentNode = node.FirstAncestor(); + parentNode is not null; + parentNode = parentNode.FirstAncestor()) + { + if (!parentNode.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return false; + } + } + + // Here we can also easily filter out ref-returning properties just using syntax + if (((PropertyDeclarationSyntax)node).Type.IsKind(SyntaxKind.RefType)) + { + return false; + } + + return true; + } + + /// + /// Checks whether an input symbol is a candidate property declaration for the generator. + /// + /// The input symbol to check. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether is a candidate property declaration. + public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol, bool useWindowsUIXaml) + { + // Ensure that the property declaration is a partial definition with no implementation + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + return false; + } + + // Also ignore all properties returning a byref-like value. We don't need to also + // check for ref values here, as that's already validated by the syntax filter. + if (propertySymbol.Type.IsRefLikeType) + { + return false; + } + + // Pointer types are never allowed + if (propertySymbol.Type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer) + { + return false; + } + + // Ensure we do have a valid containing + if (propertySymbol.ContainingType is not { } typeSymbol) + { + return false; + } + + // Ensure that the containing type derives from 'DependencyObject' + if (!typeSymbol.InheritsFromFullyQualifiedMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml))) + { + return false; + } + + // If the generated property name is called "Property" and the type is either object or 'DependencyPropertyChangedEventArgs', + // consider it invalid. This is needed because if such a property was generated, the partial 'OnChanged' + // methods would conflict. + if (propertySymbol.Name == "Property") + { + bool propertyTypeWouldCauseConflicts = + propertySymbol.Type.SpecialType == SpecialType.System_Object || + propertySymbol.Type.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml)); + + return !propertyTypeWouldCauseConflicts; + } + + return true; + } + + /// + /// Gathers all allowed property modifiers that should be forwarded to the generated property. + /// + /// The input node. + /// The returned set of property modifiers, if any. + public static ImmutableArray GetPropertyModifiers(PropertyDeclarationSyntax node) + { + // We only allow a subset of all possible modifiers (aside from the accessibility modifiers) + ReadOnlySpan candidateKinds = + [ + SyntaxKind.NewKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.RequiredKeyword + ]; + + using ImmutableArrayBuilder builder = new(); + + // Track all modifiers from the allowed set on the input property declaration + foreach (SyntaxKind kind in candidateKinds) + { + if (node.Modifiers.Any(kind)) + { + builder.Add(kind); + } + } + + return builder.ToImmutable(); + } + + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input node. + /// The input instance. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + public static bool TryGetAccessibilityModifiers( + PropertyDeclarationSyntax node, + IPropertySymbol propertySymbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + declaredAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (propertySymbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // Track the property accessibility if explicitly set + if (node.Modifiers.Count > 0) + { + declaredAccessibility = propertySymbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + { + if (accessor.Modifiers.Count == 0) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input instance. + /// The input that triggered the annotation. + /// The type name for the generated property (without nullability annotations). + /// The type name for the generated property, including nullability annotations. + /// The type name for the metadata declaration of the property, if explicitly set. + /// The type symbol for the metadata declaration of the property, if explicitly set. + public static void GetPropertyTypes( + IPropertySymbol propertySymbol, + AttributeData attributeData, + out string typeName, + out string typeNameWithNullabilityAnnotations, + out string? metadataTypeName, + out ITypeSymbol? metadataTypeSymbol) + { + // These type names are always present and directly derived from the property type + typeName = propertySymbol.Type.GetFullyQualifiedName(); + typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + // Check if the user has specified an explicit property type to use in metadata + if (attributeData.TryGetNamedArgument("PropertyType", out TypedConstant propertyType)) + { + // Also make sure we do have a type. We don't need to perform additional validation here, since + // the resulting code will always compile even if the type isn't actually compatible. We can do + // that validation just from an analizer, and emit warnings if the requested type is incorrect. + if (propertyType is { Kind: TypedConstantKind.Type, IsNull: false, Value: ITypeSymbol typeSymbol }) + { + metadataTypeName = typeSymbol.GetFullyQualifiedName(); + metadataTypeSymbol = typeSymbol; + + return; + } + } + + // By default, we'll just match the declared property type + metadataTypeName = null; + metadataTypeSymbol = null; + } + + /// + /// Gets the default value to use to initialize the generated property, if explicitly specified. + /// + /// The input that triggered the annotation. + /// The input instance. + /// The type symbol for the metadata declaration of the property, if explicitly set. + /// The for the current compilation. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// The used to cancel the operation, if needed. + /// The default value to use to initialize the generated property. + public static DependencyPropertyDefaultValue GetDefaultValue( + AttributeData attributeData, + IPropertySymbol propertySymbol, + ITypeSymbol? metadataTypeSymbol, + SemanticModel semanticModel, + bool useWindowsUIXaml, + CancellationToken token) + { + // First, check if we have a callback + if (attributeData.TryGetNamedArgument("DefaultValueCallback", out TypedConstant defaultValueCallback)) + { + // This must be a valid 'string' value + if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName }) + { + // Check that we can find a potential candidate callback method + if (InvalidPropertyDefaultValueCallbackTypeAnalyzer.TryFindDefaultValueCallbackMethod(propertySymbol, methodName, out IMethodSymbol? methodSymbol)) + { + // Validate the method has a valid signature as well + if (InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + return new DependencyPropertyDefaultValue.Callback(methodName); + } + } + } + + // Invalid callback, the analyzer will emit an error + return DependencyPropertyDefaultValue.Null.Instance; + } + + token.ThrowIfCancellationRequested(); + + // Next, check whether the default value is explicitly set or not + if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + // If the explicit value is anything other than 'null', we can return it directly + if (!defaultValue.IsNull) + { + return new DependencyPropertyDefaultValue.Constant(TypedConstantInfo.Create(defaultValue)); + } + + // If we do have a default value, we also want to check whether it's the special 'UnsetValue' placeholder. + // To do so, we get the application syntax, find the argument, then get the operation and inspect it. + if (attributeData.ApplicationSyntaxReference?.GetSyntax(token) is AttributeSyntax attributeSyntax) + { + foreach (AttributeArgumentSyntax attributeArgumentSyntax in attributeSyntax.ArgumentList?.Arguments ?? []) + { + // Let's see whether the current argument is the one that set the 'DefaultValue' property + if (attributeArgumentSyntax.NameEquals?.Name.Identifier.Text is "DefaultValue") + { + IOperation? operation = semanticModel.GetOperation(attributeArgumentSyntax.Expression, token); + + // Double check that it's a constant field reference (it could also be a literal of some kind, etc.) + if (operation is IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol }) + { + // Last step: we want to validate that the reference is actually to the special placeholder + if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) + { + return new DependencyPropertyDefaultValue.UnsetValue(useWindowsUIXaml); + } + } + } + } + } + + // Otherwise, the value has been explicitly set to 'null', so let's respect that + return DependencyPropertyDefaultValue.Null.Instance; + } + + token.ThrowIfCancellationRequested(); + + // In all other cases, we'll automatically use the default value of the type in question. + // First we need to special case non nullable values, as for those we need 'default'. + if (!propertySymbol.Type.IsDefaultValueNull()) + { + // We need special logic to handle cases where the metadata type is different. For instance, + // the XAML initialization won't work if the metadata type on a property is just 'object'. + ITypeSymbol effectiveMetadataTypeSymbol = metadataTypeSymbol ?? propertySymbol.Type; + + // For non nullable types, we return 'default(T)', unless we can optimize for projected types + return new DependencyPropertyDefaultValue.Default( + TypeName: propertySymbol.Type.GetFullyQualifiedName(), + IsProjectedType: effectiveMetadataTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); + } + + // If the property type is nullable, but the metadata type is not, and it's a projected WinRT value + // type (meaning that XAML would initialize it to a value), we need to explicitly set it to 'null'. + if (metadataTypeSymbol?.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml) is true) + { + return DependencyPropertyDefaultValue.ExplicitNull.Instance; + } + + // For all other ones, we can just use the 'null' placeholder again + return DependencyPropertyDefaultValue.Null.Instance; + } + + /// + /// Checks whether the generated code has to register the property changed callback with WinRT. + /// + /// The input that triggered the annotation. + /// Whether the generated should register the property changed callback. + public static bool IsLocalCachingEnabled(AttributeData attributeData) + { + return attributeData.GetNamedArgument("IsLocalCacheEnabled", defaultValue: false); + } + + /// + /// Checks whether the generated code has to register the property changed callback with WinRT. + /// + /// The input instance to process. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether the generated should register the property changed callback. + public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol, bool useWindowsUIXaml) + { + // Check for any 'OnChanged' methods + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}PropertyChanged")) + { + // We're looking for methods with one parameters, so filter on that first + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol argsType }] }) + { + continue; + } + + // There might be other property changed callback methods when field caching is enabled, or in other scenarios. + // Because the callback method existing adds overhead (since we have to register it with WinRT), we want to + // avoid false positives. To do that, we check that the parameter type is exactly the one we need. + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml))) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether the generated code has to register the shared property changed callback with WinRT. + /// + /// The input instance to process. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether the generated should register the shared property changed callback. + public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol, bool useWindowsUIXaml) + { + // Check for any 'OnPropertyChanged' methods + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers("OnPropertyChanged")) + { + // Same filter as above + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol argsType }] }) + { + continue; + } + + // Also same actual check as above + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml))) + { + return true; + } + } + + return false; + } + + /// + /// Gathers all forwarded attributes for the generated property. + /// + ///The input node. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + public static void GetForwardedAttributes( + PropertyDeclarationSyntax node, + SemanticModel semanticModel, + CancellationToken token, + out ImmutableArray staticFieldAttributes) + { + using ImmutableArrayBuilder builder = new(); + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in node.AttributeLists) + { + // Only look for the 'static' attribute target, which can be used to target the generated 'DependencyProperty' static field. + // Roslyn will normally emit a 'CS0658' warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support '[GeneratedDependencyProperty]'. We can't use 'field' + // as trigger, as that's used for the actual 'field' keyword, when local caching is enabled. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.StaticKeyword)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the 'AttributeData' as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the 'AttributeInfo' model. After all, attributes only support constant values, 'typeof(T)' + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? []; + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + { + continue; + } + + builder.Add(attributeInfo); + } + } + + staticFieldAttributes = builder.ToImmutable(); + } + + /// + /// Writes all implementations of partial dependency property declarations. + /// + /// The input set of declared dependency properties. + /// The instance to write into. + public static void WritePropertyDeclarations(EquatableArray propertyInfos, IndentedTextWriter writer) + { + // Helper to get the nullable type name for the initial property value + static string GetOldValueTypeNameAsNullable(DependencyPropertyInfo propertyInfo) + { + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference + // type, the previous value might be null even if the property type is not nullable, as the first invocation would + // happen when the property is first set to some value that is not null (but the backing field would still be so). + // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability + // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. + return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + { + true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") + => $"{propertyInfo.TypeNameWithNullabilityAnnotations}?", + _ => propertyInfo.TypeNameWithNullabilityAnnotations + }; + } + + // Helper to get the accessibility with a trailing space + static string GetExpressionWithTrailingSpace(Accessibility accessibility) + { + return SyntaxFacts.GetText(accessibility) switch + { + { Length: > 0 } expression => expression + " ", + _ => "" + }; + } + + string typeQualifiedName = propertyInfos[0].Hierarchy.Hierarchy[0].QualifiedName; + + // First, generate all the actual dependency property fields + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string typeMetadata = propertyInfo switch + { + // Shared codegen + { DefaultValue: DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default(_, true), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => "null", + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName})) + """, + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", + + // Codegen for legacy UWP + { IsAdditionalTypesGenerationSupported: false } => propertyInfo switch + { + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e)) + """, + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e)) + """, + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } + => $$""" + global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}.Create( + createDefaultValueCallback: new {{WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}}({{methodName}}), + propertyChangedCallback: static (d, e) => { (({{typeQualifiedName}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).OnPropertyChanged(e); }) + """, + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e)) + """, + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e)) + """, + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } + => $$""" + new global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}( + defaultValue: {{defaultValue}}, + propertyChangedCallback: static (d, e) => + { + {{typeQualifiedName}} __this = ({{typeQualifiedName}})d; + + __this.On{{propertyInfo.PropertyName}}PropertyChanged(e); + __this.OnPropertyChanged(e); + }) + """, + _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), + }, + + // Codegen for .NET 8 or greater + { DefaultValue: DependencyPropertyDefaultValue.Null } + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: null, + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName) } + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, + { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, + _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), + }; + + writer.WriteLine($$""" + /// + /// The backing instance for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + + // Write any forwarded attributes + foreach (AttributeInfo attributeInfo in propertyInfo.StaticFieldAttributes) + { + writer.WriteLine($"[{attributeInfo}]"); + } + + // Use the explicitly requested type name, if present, or the declared property type otherwise + string propertyType = propertyInfo.MetadataTypeName ?? propertyInfo.TypeName; + + writer.Write($$""" + public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( + name: "{{propertyInfo.PropertyName}}", + propertyType: typeof({{propertyType}}), + ownerType: typeof({{typeQualifiedName}}), + typeMetadata: + """, isMultiline: true); + writer.IncreaseIndent(); + writer.WriteLine($"{typeMetadata});", isMultiline: true); + writer.DecreaseIndent(); + writer.WriteLine(); + } + + // After the properties, generate all partial property implementations at the top of the partial type declaration + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + + // Declare the property + writer.WriteLine(skipIfPresent: true); + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(GeneratorName); + writer.Write(GetExpressionWithTrailingSpace(propertyInfo.DeclaredAccessibility)); + + // Add all gathered modifiers + foreach (SyntaxKind modifier in propertyInfo.PropertyModifiers.AsImmutableArray().AsSyntaxKindArray()) + { + writer.Write($"{SyntaxFacts.GetText(modifier)} "); + } + + // The 'partial' modifier always goes last, right before the property type and the property name. + // We will never have the 'partial' modifier in the set of property modifiers processed above. + writer.WriteLine($"partial {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}"); + + using (writer.WriteBlock()) + { + // We need very different codegen depending on whether local caching is enabled or not + if (propertyInfo.IsLocalCachingEnabled) + { + writer.WriteLine($$""" + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get => field; + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + if (global::System.Collections.Generic.EqualityComparer<{{oldValueTypeNameAsNullable}}>.Default.Equals(field, value)) + { + return; + } + + {{oldValueTypeNameAsNullable}} __oldValue = field; + + On{{propertyInfo.PropertyName}}Changing(value); + On{{propertyInfo.PropertyName}}Changing(__oldValue, value); + + field = value; + + object? __boxedValue = value; + """, isMultiline: true); + writer.WriteLineIf(propertyInfo.TypeName != "object", $""" + + On{propertyInfo.PropertyName}Set(ref __boxedValue); + """, isMultiline: true); + writer.Write($$""" + + SetValue({{propertyInfo.PropertyName}}Property, __boxedValue); + + On{{propertyInfo.PropertyName}}Changed(value); + On{{propertyInfo.PropertyName}}Changed(__oldValue, value); + } + """, isMultiline: true); + + // If the default value is not what the default field value would be, add an initializer + if (propertyInfo.DefaultValue is not (DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default or DependencyPropertyDefaultValue.Callback)) + { + writer.Write($" = {propertyInfo.DefaultValue};"); + } + + // Always leave a newline after the end of the property declaration, in either case + writer.WriteLine(); + } + else if (propertyInfo.TypeName == "object") + { + // If local caching is not enabled, we simply relay to the 'DependencyProperty' value. We cannot raise any methods + // to explicitly notify of changes that rely on the previous value. Retrieving it to conditionally invoke the methods + // would introduce a lot of overhead. If callers really do want to have a callback being invoked, they can implement + // the one wired up to the property metadata directly. We can still invoke the ones only using the new value, though. + writer.WriteLine($$""" + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get + { + object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); + + On{{propertyInfo.PropertyName}}Get(ref __boxedValue); + + return __boxedValue; + } + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + SetValue({{propertyInfo.PropertyName}}Property, value); + + On{{propertyInfo.PropertyName}}Changed(value); + } + """, isMultiline: true); + } + else + { + // Same as above but with the extra typed hook for both accessors + writer.WriteLine($$""" + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get + { + object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); + + On{{propertyInfo.PropertyName}}Get(ref __boxedValue); + + {{propertyInfo.TypeNameWithNullabilityAnnotations}} __unboxedValue = ({{propertyInfo.TypeNameWithNullabilityAnnotations}})__boxedValue; + + On{{propertyInfo.PropertyName}}Get(ref __unboxedValue); + + return __unboxedValue; + } + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + object? __boxedValue = value; + + On{{propertyInfo.PropertyName}}Set(ref __boxedValue); + + SetValue({{propertyInfo.PropertyName}}Property, __boxedValue); + + On{{propertyInfo.PropertyName}}Changed(value); + } + """, isMultiline: true); + + } + } + } + + // Next, emit all partial method declarations at the bottom of the file + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + string objectTypeNameWithNullabilityAnnotation = propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") ? "object?" : "object"; + + if (!propertyInfo.IsLocalCachingEnabled) + { + // OnGet 'object' overload (only without local caching, as otherwise we just return the field value) + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Get(ref {objectTypeNameWithNullabilityAnnotation} propertyValue);"); + + // OnGet typed overload + if (propertyInfo.TypeName != "object") + { + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Get(ref {propertyInfo.TypeNameWithNullabilityAnnotations} propertyValue);"); + } + } + + // OnSet 'object' overload + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Set(ref {objectTypeNameWithNullabilityAnnotation} propertyValue);"); + + if (propertyInfo.TypeName != "object") + { + // OnSet typed overload + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Set(ref {propertyInfo.TypeNameWithNullabilityAnnotations} propertyValue);"); + } + + // We can only generate the direct callback methods when using local caching (see notes above) + if (propertyInfo.IsLocalCachingEnabled) + { + // OnChanging, only with new value + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + + // OnChanging, with both values + writer.WriteLine(); + writer.WriteLine($""" + /// 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. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + } + + // OnChanged, only with new value (this is always supported) + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + + // OnChanged, with both values (once again, this is only supported when local caching is enabled) + if (propertyInfo.IsLocalCachingEnabled) + { + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + } + + // OnChanged, for the property metadata callback + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}PropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs(propertyInfo.UseWindowsUIXaml)} e);"); + } + + // OnPropertyChanged, for the shared property metadata callback + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void OnPropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs(propertyInfos[0].UseWindowsUIXaml)} e);"); + } + + /// + /// Checks whether additional types are required for the input set of properties. + /// + /// The input set of declared dependency properties. + /// Whether additional types are required. + public static bool RequiresAdditionalTypes(EquatableArray propertyInfos) + { + // Check whether generating additional types is supported. This is a performance optimization for some + // scenarios. We can only do this in some cases though. For instance, this is only supported on .NET 8 + // and above, as we need some additional types from the BCL (as '[UnsafeAccessor]'). Furthermore, if the + // containing type is generic, we cannot generate these additional types either, as it's not really viable + // to handle forwarding all type parameters to all generated accessors. In this case, we'll just use inline + // lambda expressions. This results in marginally worse codegen, but it's better than not supporting this. + if (!propertyInfos[0].IsAdditionalTypesGenerationSupported) + { + return false; + } + + // We need the additional type holding the generated callbacks if at least one WinRT-based callback is present + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + if (propertyInfo.IsPropertyChangedCallbackImplemented || propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + return true; + } + } + + return false; + } + + /// + /// Registers a callback to generate additional types, if needed. + /// + /// The input set of declared dependency properties. + /// The instance to write into. + public static void WriteAdditionalTypes(EquatableArray propertyInfos, IndentedTextWriter writer) + { + string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName(); + + // Define the 'PropertyChangedCallbacks' type + writer.WriteLine("using global::System.Runtime.CompilerServices;"); + writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace(propertyInfos[0].UseWindowsUIXaml)};"); + writer.WriteLine(); + writer.WriteLine($$""" + /// + /// Contains shared property changed callbacks for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName); + writer.WriteLine("file sealed class PropertyChangedCallbacks"); + + using (writer.WriteBlock()) + { + // Shared dummy instance field (to make delegate invocations faster) + writer.WriteLine(""" + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + """, isMultiline: true); + + int numberOfSharedPropertyCallbacks = propertyInfos.Count(static property => !property.IsPropertyChangedCallbackImplemented && property.IsSharedPropertyChangedCallbackImplemented); + bool shouldCacheSharedPropertyChangedCallback = numberOfSharedPropertyCallbacks > 1; + bool shouldGenerateSharedPropertyCallback = numberOfSharedPropertyCallbacks > 0; + + // If the shared callback should be cached, do that here + if (shouldCacheSharedPropertyChangedCallback) + { + writer.WriteLine(); + writer.WriteLine(""" + /// Shared instance, for all properties only using the shared callback. + private static readonly PropertyChangedCallback SharedPropertyChangedCallback = new(Instance.OnPropertyChanged); + """, isMultiline: true); + } + + // Write the public accessors to use in property initializers + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + if (!propertyInfo.IsPropertyChangedCallbackImplemented && !propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + continue; + } + + writer.WriteLine(); + writer.WriteLine($$""" + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback {{propertyInfo.PropertyName}}() + { + """, isMultiline: true); + writer.IncreaseIndent(); + + // There are 3 possible scenarios to handle: + // 1) The property uses a dedicated property changed callback. In this case we always need a dedicated stub. + // 2) The property uses the shared callback only, and there's more than one property like this. Reuse the instance. + // 3) This is the only property using the shared callback only. In that case, create a new delegate over it. + if (propertyInfo.IsPropertyChangedCallbackImplemented) + { + writer.WriteLine($"return new(Instance.On{propertyInfo.PropertyName}PropertyChanged);"); + } + else if (shouldCacheSharedPropertyChangedCallback) + { + writer.WriteLine("return SharedPropertyChangedCallback;"); + } + else + { + writer.WriteLine("return new(Instance.OnPropertyChanged);"); + } + + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Write the private combined + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + if (!propertyInfo.IsPropertyChangedCallbackImplemented) + { + continue; + } + + writer.WriteLine(); + writer.WriteLine($$""" + /// + private void On{{propertyInfo.PropertyName}}PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; + + PropertyChangedUnsafeAccessors.On{{propertyInfo.PropertyName}}PropertyChanged(__this, e); + """, isMultiline: true); + + // Shared callback, if needed + if (propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + writer.IncreaseIndent(); + writer.WriteLine($"PropertyChangedUnsafeAccessors.On{propertyInfo.PropertyName}PropertyChanged(__this, e);"); + writer.DecreaseIndent(); + } + + writer.WriteLine("}"); + } + + // If we need to generate the shared callback, let's also generate its target method + if (shouldGenerateSharedPropertyCallback) + { + writer.WriteLine(); + writer.WriteLine($$""" + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + """, isMultiline: true); + } + } + + // Define the 'PropertyChangedAccessors' type + writer.WriteLine(); + writer.WriteLine($""" + /// + /// Contains all unsafe accessors for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName); + writer.WriteLine("file sealed class PropertyChangedUnsafeAccessors"); + + using (writer.WriteBlock()) + { + // Write the accessors for all WinRT-based callbacks (not the shared one) + foreach (DependencyPropertyInfo propertyInfo in propertyInfos.Where(static property => property.IsPropertyChangedCallbackImplemented)) + { + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "On{propertyInfo.PropertyName}PropertyChanged")] + public static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + """, isMultiline: true); + } + + // Also emit one for the shared callback, if it's ever used + if (propertyInfos.Any(static property => property.IsSharedPropertyChangedCallbackImplemented)) + { + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + """, isMultiline: true); + } + } + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs new file mode 100644 index 000000000..b2082b826 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -0,0 +1,202 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A source generator creating implementations of dependency properties. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class DependencyPropertyGenerator : IIncrementalGenerator +{ + /// + /// The name of generator to include in the generated code. + /// + internal const string GeneratorName = "CommunityToolkit.WinUI.DependencyPropertyGenerator"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Generate the sources for the 'PrivateAssets="all"' mode + context.RegisterPostInitializationOutput(Execute.GeneratePostInitializationSources); + + // Get the info on all dependency properties to generate + IncrementalValuesProvider propertyInfo = + context.ForAttributeWithMetadataNameAndOptions( + WellKnownTypeNames.GeneratedDependencyPropertyAttribute, + Execute.IsCandidateSyntaxValid, + static (context, token) => + { + // We need C# 13, double check that it's the case + if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + return null; + } + + bool isLocalCachingEnabled = Execute.IsLocalCachingEnabled(context.Attributes[0]); + + // This generator requires C# preview to be used (due to the use of the 'field' keyword). + // The 'field' keyword is actually only used when local caching is enabled, so filter to that. + if (isLocalCachingEnabled && !context.SemanticModel.Compilation.IsLanguageVersionPreview()) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Ensure we do have a property + if (context.TargetSymbol is not IPropertySymbol propertySymbol) + { + return null; + } + + // Get the XAML mode to use + bool useWindowsUIXaml = context.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Do an initial filtering on the symbol as well + if (!Execute.IsCandidateSymbolValid(propertySymbol, useWindowsUIXaml)) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Get all additional modifiers for the property + ImmutableArray propertyModifiers = Execute.GetPropertyModifiers((PropertyDeclarationSyntax)context.TargetNode); + + token.ThrowIfCancellationRequested(); + + // Get the accessibility values, if the property is valid + if (!Execute.TryGetAccessibilityModifiers( + node: (PropertyDeclarationSyntax)context.TargetNode, + propertySymbol: propertySymbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + Execute.GetPropertyTypes( + propertySymbol, + context.Attributes[0], + out string typeName, + out string typeNameWithNullabilityAnnotations, + out string? metadataTypeName, + out ITypeSymbol? metadataTypeSymbol); + + token.ThrowIfCancellationRequested(); + + bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); + bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); + bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); + bool isContainedWithinGenericType = propertySymbol.ContainingType.IsGenericType; + bool isAdditionalTypesGenerationSupported = isNet8OrGreater && !isContainedWithinGenericType; + + token.ThrowIfCancellationRequested(); + + // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. + // This will cover both reference types as well T when the constraints are not struct or unmanaged. + // If this is true, it means the field storage can potentially be in a null state (even if not annotated). + bool isReferenceTypeOrUnconstraindTypeParameter = !propertySymbol.Type.IsValueType; + + // Also get the default value (this might be slightly expensive, so do it towards the end) + DependencyPropertyDefaultValue defaultValue = Execute.GetDefaultValue( + context.Attributes[0], + propertySymbol, + metadataTypeSymbol, + context.SemanticModel, + useWindowsUIXaml, + token); + + // The 'UnsetValue' can only be used when local caching is disabled + if (defaultValue is DependencyPropertyDefaultValue.UnsetValue && isLocalCachingEnabled) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Get any forwarded attributes + Execute.GetForwardedAttributes( + (PropertyDeclarationSyntax)context.TargetNode, + context.SemanticModel, + token, + out ImmutableArray staticFieldAttributes); + + token.ThrowIfCancellationRequested(); + + // Finally, get the hierarchy too + HierarchyInfo hierarchyInfo = HierarchyInfo.From(propertySymbol.ContainingType); + + token.ThrowIfCancellationRequested(); + + return new DependencyPropertyInfo( + Hierarchy: hierarchyInfo, + PropertyName: propertySymbol.Name, + PropertyModifiers: propertyModifiers.AsUnderlyingType(), + DeclaredAccessibility: declaredAccessibility, + GetterAccessibility: getterAccessibility, + SetterAccessibility: setterAccessibility, + TypeName: typeName, + TypeNameWithNullabilityAnnotations: typeNameWithNullabilityAnnotations, + MetadataTypeName: metadataTypeName, + DefaultValue: defaultValue, + IsReferenceTypeOrUnconstraindTypeParameter: isReferenceTypeOrUnconstraindTypeParameter, + IsLocalCachingEnabled: isLocalCachingEnabled, + IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, + IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, + IsAdditionalTypesGenerationSupported: isAdditionalTypesGenerationSupported, + UseWindowsUIXaml: useWindowsUIXaml, + StaticFieldAttributes: staticFieldAttributes); + }) + .WithTrackingName(WellKnownTrackingNames.Execute) + .Where(static item => item is not null)!; + + // Split and group by containing type + IncrementalValuesProvider> groupedPropertyInfo = + propertyInfo + .GroupBy( + keySelector: static item => item.Hierarchy, + elementSelector: static item => item, + resultSelector: static item => item.Values) + .WithTrackingName(WellKnownTrackingNames.Output); + + // Generate the source files, if any + context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) => + { + using IndentedTextWriter writer = new(); + + item[0].Hierarchy.WriteSyntax( + state: item, + writer: writer, + baseTypes: [], + memberCallbacks: [Execute.WritePropertyDeclarations]); + + if (Execute.RequiresAdditionalTypes(item)) + { + writer.WriteLine(); + writer.WriteLine($"namespace {GeneratorName}"); + + using (writer.WriteBlock()) + { + Execute.WriteAdditionalTypes(item, writer); + } + } + + context.AddSource($"{item[0].Hierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs new file mode 100644 index 000000000..36a9637f9 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -0,0 +1,125 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates diagnostics when using [GeneratedDependencyProperty] with 'PropertyType' set incorrectly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ExplicitPropertyMetadataTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + UnnecessaryDependencyPropertyExplicitMetadataType, + IncompatibleDependencyPropertyExplicitMetadataType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If an explicit property type isn't set, there's nothing to do + if (!attributeData.TryGetNamedArgument("PropertyType", out TypedConstant propertyType)) + { + return; + } + + // Special case for 'null': this will already warn due to nullability annotations, nothing more to do here either. + // This will also catch invalid types, which Roslyn will already emit errors for (so we can ignore them as well). + if (propertyType is not { Kind: TypedConstantKind.Type, IsNull: false, Value: ITypeSymbol typeSymbol }) + { + return; + } + + // If the explicit type matches the property type, then it's unnecessary, so we can warn and stop here + if (SymbolEqualityComparer.Default.Equals(propertySymbol.Type, typeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnnecessaryDependencyPropertyExplicitMetadataType, + attributeData.GetNamedArgumentOrAttributeLocation("PropertyType"), + propertySymbol, + propertySymbol.Type)); + + return; + } + + // Emit a diagnostic if the explicit type is incompatible + if (!IsValidPropertyMetadataType(propertySymbol.Type, typeSymbol, context.Compilation)) + { + context.ReportDiagnostic(Diagnostic.Create( + IncompatibleDependencyPropertyExplicitMetadataType, + attributeData.GetNamedArgumentOrAttributeLocation("PropertyType"), + propertySymbol, + typeSymbol, + propertySymbol.Type)); + } + }, SymbolKind.Property); + }); + } + + /// + /// Checks whether a given type is a valid property type for metadata, for a given dependency property. + /// + /// The property type on the property definition. + /// The type to use for the property declaration in metadata. + /// The instance for the current compilation. + /// Whether the target property type to use in metadata is valid for the property declaration. + internal static bool IsValidPropertyMetadataType(ITypeSymbol declaredPropertyTypeSymbol, ITypeSymbol explicitPropertyTypeSymbol, Compilation compilation) + { + // If the explicit type is the nullable version of the property type, that is not a supported scenario + if (explicitPropertyTypeSymbol.IsNullableValueTypeWithUnderlyingType(declaredPropertyTypeSymbol)) + { + return false; + } + + // Common check for whether the explicit type is not compatible (i.e. there's no implicit conversion and the type is not the underlying nullable type) + if (!compilation.HasImplicitConversion(declaredPropertyTypeSymbol, explicitPropertyTypeSymbol) && + !declaredPropertyTypeSymbol.IsNullableValueTypeWithUnderlyingType(explicitPropertyTypeSymbol)) + { + return false; + } + + // Special case: we want to also block incompatible assignments that would have an implicit conversion (eg. 'float' -> 'double') + if (declaredPropertyTypeSymbol.IsValueType && + explicitPropertyTypeSymbol.IsValueType && + !declaredPropertyTypeSymbol.IsNullableValueType() && + !explicitPropertyTypeSymbol.IsNullableValueType() && + !SymbolEqualityComparer.Default.Equals(declaredPropertyTypeSymbol, explicitPropertyTypeSymbol)) + { + return false; + } + + return true; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs new file mode 100644 index 000000000..06158fb20 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs @@ -0,0 +1,69 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] would generate conflicts. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyConflictingDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclarationWouldCauseConflicts]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyPropertyChangedEventArgs' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml)) is not { } dependencyPropertyChangedEventArgsSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Same logic as 'IsCandidateSymbolValid' in the generator + if (propertySymbol.Name == "Property") + { + // Check for collisions with the generated helpers and the property, only happens with these 2 types + if (propertySymbol.Type.SpecialType == SpecialType.System_Object || + SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertyChangedEventArgsSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationWouldCauseConflicts, + attributeData.GetLocation(), + propertySymbol)); + } + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs new file mode 100644 index 000000000..e06c981ef --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs @@ -0,0 +1,64 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] is in an invalid type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyContainingTypeDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclarationContainingTypeIsNotDependencyObject]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Emit the diagnostic if the target is not valid + if (!propertySymbol.ContainingType.InheritsFromType(dependencyObjectSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationContainingTypeIsNotDependencyObject, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs new file mode 100644 index 000000000..9bca68162 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -0,0 +1,156 @@ +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value callback argument. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyDefaultValueCallbackTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDeclarationDefaultValueCallbackMixed, + InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound, + InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If 'DefaultValueCallback' is not set, there's nothing to do + if (!attributeData.TryGetNamedArgument("DefaultValueCallback", out string? defaultValueCallback)) + { + return; + } + + // Emit a diagnostic if 'DefaultValue' is also being set + if (attributeData.TryGetNamedArgument("DefaultValue", out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackMixed, + attributeData.GetLocation(), + propertySymbol)); + } + + // If 'DefaultValueCallback' is 'null', ignore it (Roslyn will already warn here) + if (defaultValueCallback is null) + { + return; + } + + // Emit a diagnostic if we can't find a candidate method + if (!TryFindDefaultValueCallbackMethod(propertySymbol, defaultValueCallback, out IMethodSymbol? methodSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound, + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValueCallback"), + propertySymbol, + defaultValueCallback)); + } + else if (!IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + // Emit a diagnostic if the candidate method is not valid + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod, + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValueCallback"), + propertySymbol, + defaultValueCallback)); + } + + }, SymbolKind.Property); + }); + } + + /// + /// Tries to find a candidate default value callback method for a given property. + /// + /// The currently being targeted by the analyzer. + /// The name of the default value callback method to look for. + /// The for the resulting default value callback candidate method, if found. + /// Whether could be found. + public static bool TryFindDefaultValueCallbackMethod(IPropertySymbol propertySymbol, string methodName, [NotNullWhen(true)] out IMethodSymbol? methodSymbol) + { + ImmutableArray memberSymbols = propertySymbol.ContainingType!.GetMembers(methodName); + + foreach (ISymbol member in memberSymbols) + { + // Ignore all other member types + if (member is not IMethodSymbol candidateSymbol) + { + continue; + } + + // Match the exact method name too + if (candidateSymbol.Name == methodName) + { + methodSymbol = candidateSymbol; + + return true; + } + } + + methodSymbol = null; + + return false; + } + + /// + /// Checks whether a given default value callback method is valid for a given property. + /// + /// The currently being targeted by the analyzer. + /// The for the candidate default value callback method to validate. + /// Whether is a valid default value callback method for . + public static bool IsDefaultValueCallbackValid(IPropertySymbol propertySymbol, IMethodSymbol methodSymbol) + { + // We need methods which are static and with no parameters (and that are not explicitly implemented) + if (methodSymbol is not { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] }) + { + return false; + } + + // We have a candidate, now we need to match the return type. First, + // we just check whether the return is 'object', or an exact match. + if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || + SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType)) + { + return true; + } + + // Otherwise, try to see if the return is the type argument of a nullable value type + return propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(methodSymbol.ReturnType); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs new file mode 100644 index 000000000..26b82dbc8 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -0,0 +1,127 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyDefaultValueTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDefaultValueNull, + InvalidPropertyDefaultValueType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterOperationAction(context => + { + // We only care about attributes on properties + if (context.ContainingSymbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // Make sure the attribute operation is valid, and that we can get the attribute type symbol + if (context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation { Type: INamedTypeSymbol attributeTypeSymbol } objectOperation }) + { + return; + } + + // Filter out all attributes of other types + if (!generatedDependencyPropertyAttributeSymbols.Contains(attributeTypeSymbol, SymbolEqualityComparer.Default)) + { + return; + } + + // Also get the actual attribute data for '[GeneratedDependencyProperty]' (this should always succeed at this point) + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Get the default value, if present (if it's not set, nothing to do) + if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + return; + } + + // If the value is 'null', handle all possible cases: + // - Special placeholder for 'UnsetValue' + // - Explicit 'null' value + if (defaultValue.IsNull) + { + // Go through all named arguments of the attribute to look for 'UnsetValue' + foreach (IOperation argumentOperation in objectOperation.Initializer?.Initializers ?? []) + { + // We found its assignment: check if it's the 'UnsetValue' placeholder + if (argumentOperation is ISimpleAssignmentOperation { Value: IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol } }) + { + // Validate that the reference is actually to the special placeholder + if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) + { + return; + } + + // If it's not a match, we can just stop iterating: we know for sure the value is something else explicitly set + break; + } + } + + // Warn if the value is not nullable + if (!propertySymbol.Type.IsDefaultValueNull()) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueNull, + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValue"), + propertySymbol, + propertySymbol.Type, + propertySymbol.Name)); + } + } + else + { + // Get the target type with a special case for 'Nullable' + ITypeSymbol propertyTypeSymbol = propertySymbol.Type.IsNullableValueType() + ? ((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0] + : propertySymbol.Type; + + // Warn if the type of the default value is not compatible + if (!SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, defaultValue.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueType, + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValue"), + propertySymbol, + propertySymbol.Type, + defaultValue.Value, + defaultValue.Type, + propertySymbol.Name)); + } + } + }, OperationKind.Attribute); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs new file mode 100644 index 000000000..b0dd21068 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs @@ -0,0 +1,106 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] is using invalid forwarded attributes. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyForwardedAttributeDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidDependencyPropertyTargetedAttributeType, + InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + context.RegisterSyntaxNodeAction(context => + { + foreach (AttributeListSyntax attributeList in ((PropertyDeclarationSyntax)context.Node).AttributeLists) + { + // Only target attributes that would be forwarded, ignore all others + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.StaticKeyword)) + { + continue; + } + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Emit a diagnostic (and stop here for this attribute) if we can't resolve the symbol for the attribute to forward + if (!context.SemanticModel.GetSymbolInfo(attribute, context.CancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDependencyPropertyTargetedAttributeType, + attribute.GetLocation(), + propertySymbol, + attribute.Name.ToFullString())); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? []; + + // Also emit a diagnostic if we fail to create the object model for the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, context.CancellationToken, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression, + attribute.GetLocation(), + propertySymbol, + attributeTypeSymbol)); + } + } + } + }, SyntaxKind.PropertyDeclaration); + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs new file mode 100644 index 000000000..c93b3988e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -0,0 +1,193 @@ +// 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.Collections.Immutable; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning when a property with [GeneratedDependencyProperty] would generate a nullability annotations violation. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + NonNullablePropertyDeclarationIsNotEnforced, + NotNullResilientAccessorsForNotNullablePropertyDeclaration, + NotNullResilientAccessorsForNullablePropertyDeclaration + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Attempt to also get the '[MaybeNull]', '[NotNull]', '[AllowNull]' and '[DisallowNull]' symbols (there might be multiples, due to polyfills) + ImmutableArray maybeNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + ImmutableArray notNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.NotNullAttribute"); + ImmutableArray allowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.AllowNullAttribute"); + ImmutableArray disallowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.DisallowNullAttribute"); + + context.RegisterSymbolAction(context => + { + // Validate that we have a property that is of some type that could potentially become 'null' + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null, Type.IsValueType: false, NullableAnnotation: not NullableAnnotation.None } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Handle nullable and non-null properties differently + if (propertySymbol.NullableAnnotation is NullableAnnotation.Annotated) + { + // If we don't have '[NotNull]', we'll never need to emit a diagnostic. + // That is, the default nullable state will always be correct already. + if (!propertySymbol.HasAttributeWithAnyType(notNullAttributeSymbols)) + { + return; + } + + // If we have '[NotNull]', it means the property getter must always ensure that a non-null value is returned. + // This can be achieved in two different ways: + // 1) By implementing one of the 'On___Get' methods, and adding '[NotNull]' on the parameter. + // 2) By having '[DisallowNull]' on the property or implementing one of the 'On___Set' methods with '[NotNull]' + // on the parameter, and either marking the property as required, or providing a non-null default value. + if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) && + !((propertySymbol.HasAttributeWithAnyType(disallowNullAttributeSymbols) || IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) && + (propertySymbol.IsRequired || IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)))) + { + context.ReportDiagnostic(Diagnostic.Create( + NotNullResilientAccessorsForNullablePropertyDeclaration, + propertySymbol.Locations.FirstOrDefault(), + propertySymbol, + propertySymbol.Name)); + } + } + else + { + // If the property is not nullable and it has '[MaybeNull]', we never need to emit a diagnostic. + // That is, setting 'null' is valid, and the initial state doesn't matter, as the return is nullable. + if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbols)) + { + return; + } + + // If the getter is null-resilient, then we never need to emit a warning here + if (IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols)) + { + return; + } + + // If setting 'null' values is allowed, then the initial state (and the default value) don't matter anymore. + // In order to be correct, we must have '[NotNull]' on any implemented getter or setter methods (same as above). + if (propertySymbol.HasAttributeWithAnyType(allowNullAttributeSymbols) && + !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) + { + context.ReportDiagnostic(Diagnostic.Create( + NotNullResilientAccessorsForNotNullablePropertyDeclaration, + propertySymbol.Locations.FirstOrDefault(), + propertySymbol, + propertySymbol.Name)); + } + + // In either case, we need to check that either the property is required, or that the default value is not 'null'. + // This is because when the nullability of the setter is correct, then the default value takes precedence. + if (!propertySymbol.IsRequired && !IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + propertySymbol.Locations.FirstOrDefault(), + propertySymbol)); + } + } + }, SymbolKind.Property); + }); + } + /// + /// Checks whether a given generated accessor method has [NotNull] on its parameter. + /// + /// The instance to inspect. + /// The syntax kind for the accessor method to look for. + /// The instances for [NotNull]. + /// Whether has a generated accessor method with its parameter marked with [NotNull]. + private static bool IsAccessorMethodMarkedAsNotNull(IPropertySymbol propertySymbol, SyntaxKind accessorKind, ImmutableArray notNullAttributeSymbols) + { + string suffix = accessorKind == SyntaxKind.GetAccessorDeclaration ? "Get" : "Set"; + + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}{suffix}")) + { + // We really only expect to match our own generated methods, but do some basic filtering just in case + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol, RefKind: RefKind.Ref } propertyValue] }) + { + continue; + } + + // Check if the parameter has '[NotNull]' on it + if (propertyValue.HasAttributeWithAnyType(notNullAttributeSymbols)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a given property has a default value that is not . + /// + /// The instance to inspect. + /// The instance on . + /// The instances for [MaybeNull]. + /// The instances for [NotNull]. + /// + private static bool IsDefaultValueNotNull( + IPropertySymbol propertySymbol, + AttributeData attributeData, + ImmutableArray maybeNullAttributeSymbols, + ImmutableArray notNullAttributeSymbols) + { + // If we have a default value, check that it's not 'null' + if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + return !defaultValue.IsNull; + } + + // If we have a callback, validate its return type + if (attributeData.TryGetNamedArgument("DefaultValueCallback", out TypedConstant defaultValueCallback)) + { + // Find the target method (same logic here as in the generator) + if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName } && + InvalidPropertyDefaultValueCallbackTypeAnalyzer.TryFindDefaultValueCallbackMethod(propertySymbol, methodName, out IMethodSymbol? methodSymbol) && + InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + // Verify that the return type can't possibly be 'null', including using attributes + return + (methodSymbol.ReturnNullableAnnotation is NullableAnnotation.NotAnnotated && !methodSymbol.HasReturnAttributeWithAnyType(maybeNullAttributeSymbols)) || + (methodSymbol.ReturnNullableAnnotation is NullableAnnotation.Annotated && methodSymbol.HasReturnAttributeWithAnyType(notNullAttributeSymbols)); + } + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs new file mode 100644 index 000000000..9284c6593 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs @@ -0,0 +1,109 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertySymbolDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDeclarationIsNotIncompletePartialDefinition, + InvalidPropertyDeclarationReturnsByRef, + InvalidPropertyDeclarationReturnsRefLikeType, + InvalidPropertyDeclarationReturnsPointerType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + // This generator is intentionally also analyzing generated code, because Roslyn will interpret properties + // that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts). + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the '[GeneratedCode]' symbol + if (context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Emit an error if the property is not a partial definition with no implementation... + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + // ...But only if it wasn't actually generated by the [ObservableProperty] generator. + bool isImplementationAllowed = + propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } && + implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) && + generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) && + toolName == DependencyPropertyGenerator.GeneratorName; + + // Emit the diagnostic only for cases that were not valid generator outputs + if (!isImplementationAllowed) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationIsNotIncompletePartialDefinition, + attributeData.GetLocation(), + propertySymbol)); + } + } + + // Emit an error if the property returns a value by ref + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsByRef, + attributeData.GetLocation(), + propertySymbol)); + } + else if (propertySymbol.Type.IsRefLikeType) + { + // Emit an error if the property type is a ref struct + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsRefLikeType, + attributeData.GetLocation(), + propertySymbol)); + } + else if (propertySymbol.Type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer) + { + // Emit a diagnostic if the type is a pointer type + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsPointerType, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs new file mode 100644 index 000000000..b26fa16ad --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs @@ -0,0 +1,99 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertySyntaxDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // If the property isn't using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidPropertyDeclaration(propertyNode)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclaration, + attributeData.GetLocation(), + propertySymbol)); + + return; + } + } + }, SymbolKind.Property); + }); + } + + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// Whether is a valid property. + internal static bool IsValidPropertyDeclaration(SyntaxNode node) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return false; + } + + // Static properties are not supported + if (property.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + return false; + } + + return true; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs new file mode 100644 index 000000000..71634a8ca --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs @@ -0,0 +1,59 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a diagnostic whenever [GeneratedDependencyProperty] is used on a property with the 'Property' suffix in its name. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PropertyDeclarationWithPropertyNameSuffixAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [PropertyDeclarationWithPropertySuffix]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // We only want to lookup the attribute if the property name actually ends with the 'Property' suffix + if (!propertySymbol.Name.EndsWith("Property")) + { + return; + } + + // Emit a diagnostic if the property is using '[GeneratedDependencyProperty]' + if (propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + context.ReportDiagnostic(Diagnostic.Create( + PropertyDeclarationWithPropertySuffix, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs new file mode 100644 index 000000000..a9c022456 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -0,0 +1,75 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when using [GeneratedDependencyProperty] without the right C# version. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + PropertyDeclarationRequiresCSharp13, + LocalCachingRequiresCSharpPreview + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If we're using C# 'preview', we'll never emit any errors + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + bool isLocalCachingEnabled = attributeData.GetNamedArgument("IsLocalCacheEnabled", defaultValue: false); + + // Emit only up to one diagnostic, for whichever the highest required C# version would be + if (isLocalCachingEnabled && !context.Compilation.IsLanguageVersionPreview()) + { + context.ReportDiagnostic(Diagnostic.Create( + LocalCachingRequiresCSharpPreview, + attributeData.GetLocation(), + propertySymbol)); + } + else if (!isLocalCachingEnabled && !context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + context.ReportDiagnostic(Diagnostic.Create( + PropertyDeclarationRequiresCSharp13, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs new file mode 100644 index 000000000..0e0956524 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs @@ -0,0 +1,115 @@ +// 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.Collections.Immutable; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning whenever a dependency property is declared as a property. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseFieldDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DependencyPropertyFieldDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the 'DependencyProperty' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol) + { + return; + } + + // Check whether the current project is a WinRT component (modern .NET uses CsWinRT, legacy .NET produces .winmd files directly) + bool isWinRTComponent = + context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.CsWinRTComponent) || + context.Compilation.Options.OutputKind is OutputKind.WindowsRuntimeMetadata; + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // Ignore instance properties (same as in the other analyzer), and interface members + if (propertySymbol is { IsStatic: false } or { ContainingType.TypeKind: TypeKind.Interface }) + { + return; + } + + // Also ignore properties that are write-only (there can't really be normal dependency properties) + if (propertySymbol.IsWriteOnly) + { + return; + } + + // We only care about properties which are of type 'DependencyProperty' + if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertySymbol)) + { + return; + } + + // If the property is an explicit interface implementation, allow it + if (propertySymbol.ExplicitInterfaceImplementations.Length > 0) + { + return; + } + + // Next, make sure this property isn't (implicitly) implementing any interface properties. + // If that's the case, we'll also allow it, as otherwise fixing this would break things. + foreach (INamedTypeSymbol interfaceSymbol in propertySymbol.ContainingType.AllInterfaces) + { + // Go over all properties (we can filter to just those with the same name) in each interface + foreach (IPropertySymbol interfacePropertySymbol in interfaceSymbol.GetMembers(propertySymbol.Name).OfType()) + { + // The property must have the same type to possibly be an interface implementation + if (!SymbolEqualityComparer.Default.Equals(interfacePropertySymbol.Type, propertySymbol.Type)) + { + continue; + } + + // If the property is not implemented at all, ignore it + if (propertySymbol.ContainingType.FindImplementationForInterfaceMember(interfacePropertySymbol) is not IPropertySymbol implementationSymbol) + { + continue; + } + + // If the current property is the one providing the implementation, then we allow it and stop here + if (SymbolEqualityComparer.Default.Equals(implementationSymbol, propertySymbol)) + { + return; + } + } + } + + // Make an exception for WinRT components: in this case declaring properties is valid, as they're needed for WinRT + if (isWinRTComponent && propertySymbol.GetEffectiveAccessibility() is Accessibility.Public) + { + return; + } + + // At this point, we know for sure the property isn't valid, so emit a diagnostic + context.ReportDiagnostic(Diagnostic.Create( + DependencyPropertyFieldDeclaration, + propertySymbol.Locations.First(), + propertySymbol)); + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs new file mode 100644 index 000000000..2b6394afd --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs @@ -0,0 +1,74 @@ +// 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.Collections.Immutable; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning whenever a dependency property is declared as an incorrect field. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseFieldDeclarationCorrectlyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [IncorrectDependencyPropertyFieldDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the 'DependencyProperty' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + IFieldSymbol fieldSymbol = (IFieldSymbol)context.Symbol; + + // Ignore instance fields, to reduce false positives. There might be edge cases + // where property instances are used as state, and that's technically not invalid. + if (!fieldSymbol.IsStatic) + { + return; + } + + // We only care about fields which are of type 'DependencyProperty' + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.Type, dependencyPropertySymbol)) + { + return; + } + + // Fields should always be public, static, readonly, and with nothing else on them + if (fieldSymbol is + { DeclaredAccessibility: not Accessibility.Public } or + { IsRequired: true } or + { IsReadOnly: false } or + { IsVolatile: true } or + { NullableAnnotation: NullableAnnotation.Annotated }) + { + context.ReportDiagnostic(Diagnostic.Create( + IncorrectDependencyPropertyFieldDeclaration, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol)); + } + }, SymbolKind.Field); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs new file mode 100644 index 000000000..f2264ee3a --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -0,0 +1,1036 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [GeneratedDependencytProperty] should be used over a manual property. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + /// The number of pooled flags per stack (ie. how many properties we expect on average per type). + /// + private const int NumberOfPooledFlagsPerStack = 20; + + /// + /// Shared pool for instances for properties. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> PropertyMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for instances for fields, for dependency properties. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> FieldMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for -s of property flags, one per type being processed. + /// + private static readonly ObjectPool> PropertyFlagsStackPool = new(CreateFlagsStack); + + /// + /// Shared pool for -s of field flags, one per type being processed. + /// + private static readonly ObjectPool> FieldFlagsStackPool = new(CreateFlagsStack); + + /// + /// The property name for the serialized property value, if present. + /// + public const string DefaultValuePropertyName = "DefaultValue"; + + /// + /// The property name for the fully qualified metadata name of the default value, if present. + /// + public const string DefaultValueTypeReferenceIdPropertyName = "DefaultValueTypeReferenceId"; + + /// + /// The property name for the serialized value. + /// + public const string AdditionalLocationKindPropertyName = "AdditionalLocationKind"; + + /// + /// The special identifier to represent the unset value. This allows marshalling the value as a , for simplicity. + /// + public const string UnsetValueSpecialIdentifier = "E5FDFA8B-7E6B-4217-8844-52E5B30084B1"; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + UseGeneratedDependencyPropertyForManualProperty, + NoPropertySuffixOnDependencyPropertyField, + InvalidPropertyNameOnDependencyPropertyField, + MismatchedPropertyNameOnDependencyPropertyField, + InvalidOwningTypeOnDependencyPropertyField, + InvalidPropertyTypeOnDependencyPropertyField, + InvalidDefaultValueNullOnDependencyPropertyField, + InvalidDefaultValueTypeOnDependencyPropertyField + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If the language is not at least C#, the generator can't be used, so we shouldn't emit warnings. + // If we did so, users wouldn't be able to fix them anyway without bumping the language first. + if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + return; + } + + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) + { + return; + } + + // Get the symbol for the 'GetValue' method as well + if (dependencyObjectSymbol.GetMembers("GetValue") is not [IMethodSymbol { Parameters: [_], ReturnType.SpecialType: SpecialType.System_Object } getValueSymbol]) + { + return; + } + + // Get the symbol for the 'SetValue' method as well + if (dependencyObjectSymbol.GetMembers("SetValue") is not [IMethodSymbol { Parameters: [_, _], ReturnsVoid: true } setValueSymbol]) + { + return; + } + + // We also need the 'DependencyProperty' and 'PropertyMetadata' symbols + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol || + context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.PropertyMetadata(useWindowsUIXaml)) is not { } propertyMetadataSymbol) + { + return; + } + + // Next, we need to get the 'DependencyProperty.Register' symbol as well, to validate initializers. + // No need to validate this more, as there's only a single overload defined on this type. + if (dependencyPropertySymbol.GetMembers("Register") is not [IMethodSymbol dependencyPropertyRegisterSymbol]) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // We only care about types that could derive from 'DependencyObject' + if (context.Symbol is not INamedTypeSymbol { IsStatic: false, IsReferenceType: true, BaseType.SpecialType: not SpecialType.System_Object } typeSymbol) + { + return; + } + + // If the type does not derive from 'DependencyObject', ignore it + if (!typeSymbol.InheritsFromType(dependencyObjectSymbol)) + { + return; + } + + Dictionary propertyMap = PropertyMapPool.Allocate(); + Dictionary fieldMap = FieldMapPool.Allocate(); + Stack propertyFlagsStack = PropertyFlagsStackPool.Allocate(); + Stack fieldFlagsStack = FieldFlagsStackPool.Allocate(); + + // Crawl all members to discover properties that might be of interest + foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) + { + // First, look for properties that might be valid candidates for conversion + if (memberSymbol is IPropertySymbol + { + IsStatic: false, + IsPartialDefinition: false, + PartialDefinitionPart: null, + PartialImplementationPart: null, + ReturnsByRef: false, + ReturnsByRefReadonly: false, + Type.IsRefLikeType: false, + GetMethod: not null, + SetMethod.IsInitOnly: false + } propertySymbol) + { + // We can safely ignore properties that already have '[GeneratedDependencyProperty]' + if (propertySymbol.HasAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols)) + { + continue; + } + + // Take a new flags object from the stack or create a new one otherwise + PropertyFlags flags = propertyFlagsStack.Count > 0 + ? propertyFlagsStack.Pop() + : new(); + + // Track the property for later + propertyMap.Add(propertySymbol, flags); + } + else if (memberSymbol is IFieldSymbol + { + IsStatic: true, + IsFixedSizeBuffer: false, + IsRequired: false, + Type.IsReferenceType: true, + IsVolatile: false + } fieldSymbol) + { + // We only care about fields that are 'DependencyProperty'. Note that we should also be checking the declared + // accessibility here and the fact the field is readonly, but we delay that in the initialization callback. + // This is done so that we can still emit more diagnostics when analyzing the symbol completes below. + if (!SymbolEqualityComparer.Default.Equals(dependencyPropertySymbol, fieldSymbol.Type)) + { + continue; + } + + // Same as above for the field flags + fieldMap.Add( + key: fieldSymbol, + value: fieldFlagsStack.Count > 0 ? fieldFlagsStack.Pop() : new FieldFlags()); + } + } + + // We want to process both accessors, where we specifically need both the syntax + // and their semantic model to verify what they're doing. We can use a code callback. + context.RegisterOperationBlockAction(context => + { + // Handle a 'get' accessor (for any property) + void HandleGetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFlags) + { + // We expect a top-level block operation, that immediately returns an expression + if (context.OperationBlocks is not [IBlockOperation { Operations: [IReturnOperation returnOperation] }]) + { + return; + } + + // Next, check whether we have an invocation operation. This is the case when the getter is just + // calling 'GetValue' and returning it directly, which only works when the property type allows + // direct conversion. Generally speaking this happens when properties are just of type 'object'. + if (returnOperation is not { ReturnedValue: IInvocationOperation invocationOperation }) + { + // Alternatively, we expect a conversion (a cast) + if (returnOperation is not { ReturnedValue: IConversionOperation { IsTryCast: false } conversionOperation }) + { + return; + } + + // Check the conversion is actually valid + if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, conversionOperation.Type)) + { + return; + } + + // Try to extract the invocation from the conversion + if (conversionOperation.Operand is not IInvocationOperation operandInvocationOperation) + { + return; + } + + invocationOperation = operandInvocationOperation; + } + + // Now that we have the invocation, first filter the target method + if (invocationOperation.TargetMethod is not { Name: "GetValue", IsGenericMethod: false, IsStatic: false } methodSymbol) + { + return; + } + + // Next, make sure we're actually calling 'DependencyObject.GetValue' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol, getValueSymbol)) + { + return; + } + + // Make sure we have one argument, which will be the dependency property + if (invocationOperation.Arguments is not [{ } dependencyPropertyArgument]) + { + return; + } + + // This argument should be a field reference (we'll fully validate it later) + if (dependencyPropertyArgument.Value is not IFieldReferenceOperation { Field: { } fieldSymbol }) + { + return; + } + + // We can in the meantime at least verify that we do have the field in the set + if (!fieldMap.ContainsKey(fieldSymbol)) + { + return; + } + + // If the property is also valid, then the accessor is valid + propertyFlags.GetValueTargetField = fieldSymbol; + } + + // Handle a 'set' accessor (for any property) + void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFlags) + { + // We expect a top-level block operation, that immediately performs an invocation + if (context.OperationBlocks is not [IBlockOperation { Operations: [IExpressionStatementOperation { Operation: IInvocationOperation invocationOperation }] }]) + { + return; + } + + // Brief filtering of the target method + if (invocationOperation.TargetMethod is not { Name: "SetValue", IsGenericMethod: false, IsStatic: false } methodSymbol) + { + return; + } + + // First, check that we're calling 'DependencyObject.SetValue' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol, setValueSymbol)) + { + return; + } + + // We matched the method, now let's validate the arguments + if (invocationOperation.Arguments is not [{ } dependencyPropertyArgument, { } valueArgument]) + { + return; + } + + // Like for the getter, the first argument should be a field reference... + if (dependencyPropertyArgument.Value is not IFieldReferenceOperation { Field: { } fieldSymbol }) + { + return; + } + + // ...and the field should be in the set (not fully guaranteed to be valid yet, but partially) + if (!fieldMap.ContainsKey(fieldSymbol)) + { + return; + } + + // The value is just the 'value' keyword + if (valueArgument.Value is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + // If this is not the case, check whether the parameter reference was wrapped in a conversion (boxing) + if (valueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + { + return; + } + + // Check for 'value' again + if (conversionOperation.Operand is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + return; + } + } + + // The 'set' accessor is valid if the field is valid, like above + propertyFlags.SetValueTargetField = fieldSymbol; + } + + // Only look for method symbols, for property accessors + if (context.OwningSymbol is not IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet, AssociatedSymbol: IPropertySymbol propertySymbol }) + { + return; + } + + // If so, check that we are actually processing one of the properties we care about + if (!propertyMap.TryGetValue(propertySymbol, out PropertyFlags? propertyFlags)) + { + return; + } + + // Handle the 'get' and 'set' logic + if (SymbolEqualityComparer.Default.Equals(propertySymbol.GetMethod, context.OwningSymbol)) + { + HandleGetAccessor(propertySymbol, propertyFlags); + } + else if (SymbolEqualityComparer.Default.Equals(propertySymbol.SetMethod, context.OwningSymbol)) + { + HandleSetAccessor(propertySymbol, propertyFlags); + } + }); + + // Same as above, but targeting field initializers (we can't just inspect field symbols) + context.RegisterOperationAction(context => + { + // Only look for field symbols, which we should always get here, and an invocation in the initializer block (the 'DependencyProperty.Register' call). + // As far as the additional diagnostics are concerned (that is, the ones for failure cases), we don't need this to be 100% accurate. For instance, we + // don't care about initializers assigning to multiple fields, as practically speaking nobody will ever do that. Similarly, we also don't worry about + // spotting fields without an initializers, as nobody would realistically be declaring dependency property fields without an initializer. + if (context.Operation is not IFieldInitializerOperation { InitializedFields: [{ } fieldSymbol], Value: IInvocationOperation invocationOperation }) + { + return; + } + + // Validate that we are calling 'DependencyProperty.Register' + if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) + { + return; + } + + // Check that the field is one of the ones we expect to encounter + if (fieldMap.TryGetValue(fieldSymbol, out FieldFlags? fieldFlags)) + { + // Start to set some field flags, if we could retrieve an instance. This comment applies to all equivalent + // statements below. We want to still emit any applicable diagnostics even in case the field is orphaned. + // To do so, we do that validation even if we couldn't retrieve the flags, and simply skip any assignments. + fieldFlags.FieldSymbol = fieldSymbol; + } + + // Do the accessibility and readonly check here, to still enable diagnostics at the end in more scenarios. + // If the field is invalid, just mark it as such, but continue executing the rest of the analysis here. + if (fieldSymbol is { DeclaredAccessibility: not Accessibility.Public } or { IsReadOnly: false }) + { + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + + // Additional diagnostic #1: the dependency property field name should have the "Property" suffix + if (!fieldSymbol.Name.EndsWith("Property")) + { + context.ReportDiagnostic(Diagnostic.Create( + NoPropertySuffixOnDependencyPropertyField, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol)); + + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + + // Next, make sure we have the arguments we expect + if (invocationOperation.Arguments is not [{ } nameArgument, { } propertyTypeArgument, { } ownerTypeArgument, { } propertyMetadataArgument]) + { + return; + } + + if (fieldFlags is not null) + { + fieldFlags.PropertyNameExpressionLocation = nameArgument.Syntax.GetLocation(); + fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); + } + + // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later + if (nameArgument.Value.ConstantValue is { HasValue: true, Value: string propertyName }) + { + if (fieldFlags is not null) + { + fieldFlags.PropertyName = propertyName; + } + + // Additional diagnostic #2: the property name should be the same as the field name, without the "Property" suffix (as per convention) + if (fieldSymbol.Name.EndsWith("Property") && propertyName != fieldSymbol.Name[..^"Property".Length]) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyNameOnDependencyPropertyField, + nameArgument.Syntax.GetLocation(), + fieldSymbol, + propertyName)); + + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + } + + // Extract the owning type, we can validate it right now. Move this before checking the property type, even though this + // comes after it, so that we can still produce this diagnostic correctly if the property type is missing or invalid. + if (ownerTypeArgument.Value is ITypeOfOperation { TypeOperand: { } owningTypeSymbol }) + { + // Additional diagnostic #3: the owning type always has to be exactly the same as the containing type + if (!SymbolEqualityComparer.Default.Equals(owningTypeSymbol, typeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidOwningTypeOnDependencyPropertyField, + ownerTypeArgument.Syntax.GetLocation(), + fieldSymbol, + owningTypeSymbol, + typeSymbol)); + + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + } + + // Extract the property type, we can validate it later + if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) + { + return; + } + + if (fieldFlags is not null) + { + fieldFlags.PropertyType = propertyTypeSymbol; + } + + // First, check if the metadata is 'null' (simplest case) + if (propertyMetadataArgument.Value.ConstantValue is { HasValue: true, Value: null }) + { + // Here we need to special case non nullable value types that are not well known WinRT projected types. + // In this case, we cannot rely on XAML calling their default constructor. Rather, we need to preserve + // the explicit 'null' value that users had in their code. The analyzer will then warn on these cases. + if (!propertyTypeSymbol.IsDefaultValueNull() && + !propertyTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)) + { + if (fieldFlags is not null) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + } + else + { + // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type + if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument, ..] } objectCreationOperation) + { + // Before failing, check whether the argument is 'new (...)`. In this case, the target type is 'PropertyMetadata' anyway, + // which we also validate right after this check as well anyway. With 'new()', we expect a conversion operation instead. + if (propertyMetadataArgument.Value is not IConversionOperation { IsImplicit: true } objectCreationConversionOperation || + objectCreationConversionOperation.Operand is not IObjectCreationOperation { Arguments: [_, ..] } conversionObjectCreationOperation) + { + return; + } + + defaultValueArgument = conversionObjectCreationOperation.Arguments[0]; + objectCreationOperation = conversionObjectCreationOperation; + } + + // Make sure the object being created is actually 'PropertyMetadata' + if (!SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, propertyMetadataSymbol)) + { + return; + } + + // If we have a second argument, a 'null' literal is the only supported value for it. If that's not the case, + // we mark the propertry as not valid, but don't stop here. The reason is that even if we do have a callback, + // meaning we cannot enable the code fixer, we still want to analyze the default value argument. + if (objectCreationOperation.Arguments is not ([_] or [_, { Value.ConstantValue: { HasValue: true, Value: null } }])) + { + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + + bool isDependencyPropertyUnsetValue = false; + + // Emit diagnostics for invalid default values (equivalent logic to 'InvalidPropertyDefaultValueTypeAnalyzer'). + // Note: we don't flag the property if we emit diagnostics here, as we can still apply the code fixer either way. + // If we do that, then the other analyzer will simply start producing an error on the attribute, rather than here. + if (defaultValueArgument.Value is { ConstantValue: { HasValue: true, Value: null } }) + { + // Default value diagnostic #1: the value is 'null' but the property type is not nullable + if (!propertyTypeSymbol.IsDefaultValueNull()) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDefaultValueNullOnDependencyPropertyField, + defaultValueArgument.Syntax.GetLocation(), + fieldSymbol, + propertyTypeSymbol)); + } + } + else + { + isDependencyPropertyUnsetValue = + defaultValueArgument.Value is IPropertyReferenceOperation { Property: { IsStatic: true, Name: "UnsetValue" } unsetValuePropertySymbol } && + SymbolEqualityComparer.Default.Equals(unsetValuePropertySymbol.ContainingType, dependencyPropertySymbol); + + // We never emit diagnostics when assigning 'UnsetValue': this value is special, so let's just ignore it + // for the purposes of diagnostics. However, we do need to track the special identifier for the code fixer. + if (isDependencyPropertyUnsetValue) + { + if (fieldFlags is not null) + { + fieldFlags.DefaultValue = new TypedConstantInfo.Primitive.String(UnsetValueSpecialIdentifier); + } + } + else if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object, Operand.Type: { } operandTypeSymbol }) + { + // Get the type of the conversion operation (same as the one below, but we get it here too just to opportunistically emit a diagnostic). + // Additionally, we need to get the target type with a special case for 'Nullable' (same as in the other analyzer). + ITypeSymbol unwrappedPropertyTypeSymbol = propertyTypeSymbol.IsNullableValueType() + ? ((INamedTypeSymbol)propertyTypeSymbol).TypeArguments[0] + : propertyTypeSymbol; + + // Warn if the type of the default value is not compatible + if (!SymbolEqualityComparer.Default.Equals(unwrappedPropertyTypeSymbol, operandTypeSymbol) && + !SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, operandTypeSymbol) && + context.Compilation.ClassifyConversion(operandTypeSymbol, propertyTypeSymbol) is not ({ IsBoxing: true } or { IsReference: true })) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDefaultValueTypeOnDependencyPropertyField, + defaultValueArgument.Syntax.GetLocation(), + fieldSymbol, + operandTypeSymbol, + propertyTypeSymbol)); + } + } + } + + // The argument should be a conversion operation (boxing) + if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + { + if (fieldFlags is not null) + { + // Store the operation for later, as we need to wait to also get the metadata type to do the full validation + fieldFlags.DefaultValueOperation = conversionOperation.Operand; + } + } + else if (!isDependencyPropertyUnsetValue) + { + // The only other supported case is for 'UnsetValue', so stop here if that's not it + return; + } + } + + // At this point we've exhausted all possible diagnostics that are also valid on orphaned fields. + // We can now validate that we did manage to retrieve the field flags, and stop if we didn't. + if (fieldFlags is null) + { + return; + } + + // Find the parent field for the operation (we're guaranteed to only fine one) + if (context.Operation.Syntax.FirstAncestor() is not { } fieldDeclaration) + { + return; + } + + // Ensure that the field only has attributes we can forward, or not attributes at all + if (fieldDeclaration.AttributeLists.Any(static list => list.Target is { Identifier: not SyntaxToken(SyntaxKind.FieldKeyword) })) + { + return; + } + + fieldFlags.FieldLocation = fieldDeclaration.GetLocation(); + }, OperationKind.FieldInitializer); + + // Finally, we can consume this information when we finish processing the symbol + context.RegisterSymbolEndAction(context => + { + // Emit a diagnostic for each property that was a valid match + foreach (KeyValuePair pair in propertyMap) + { + // Make sure we have target fields for each accessor. This also implies the accessors themselves are valid. + if (pair.Value is not { GetValueTargetField: { } getValueFieldSymbol, SetValueTargetField: { } setValueFieldSymbol }) + { + continue; + } + + // The two fields must of course be the same + if (!SymbolEqualityComparer.Default.Equals(getValueFieldSymbol, setValueFieldSymbol)) + { + continue; + } + + // Next, check that the field are present in the mapping (otherwise for sure they're not valid) + if (!fieldMap.TryGetValue(getValueFieldSymbol, out FieldFlags? fieldFlags)) + { + continue; + } + + // Additional diagnostic #4: we only support rewriting when the property name matches the field being + // initialized. Note that the property name here is the literal being passed for the 'name' parameter. + // These two names should always match, and if they don't that's most definitely a bug in the code. + if (fieldFlags.FieldSymbol is not null && pair.Key.Name != fieldFlags.PropertyName) + { + // Ensure we do have a property name before emitting a diagnostic (if it's 'null', that's a user error) + if (fieldFlags.PropertyName is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + MismatchedPropertyNameOnDependencyPropertyField, + fieldFlags.PropertyNameExpressionLocation!, + fieldFlags.FieldSymbol, + fieldFlags.PropertyName, + pair.Key.Name)); + } + + fieldFlags.HasAnyDiagnostics = true; + } + + // Execute the deferred default value validation, if necessary. If we have an operation + // here, it means we had some constructed property metadata. Otherwise, it was 'null'. + if (fieldFlags.DefaultValueOperation is null) + { + // Special case: the whole property metadata is 'null', and the metadata type is nullable, but + // the property type isn't. In this case, we need to ensure the explicit 'null' is preserved. + if (fieldFlags.IsExplicitConversionFromNonNullableToNullableMetdataType(pair.Key.Type)) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else + { + bool isNullableValueType = pair.Key.Type.IsNullableValueType(); + + // Check whether the value is a default constant value. If it is, then the property is valid (no explicit value). + // We need to special case nullable value types, as the default value for the underlying type is not the actual default. + if (!fieldFlags.DefaultValueOperation.IsConstantValueDefault() || isNullableValueType) + { + // The value is just 'null' with no type, special case this one and skip the other checks below + if (fieldFlags.DefaultValueOperation is { Type: null, ConstantValue: { HasValue: true, Value: null } }) + { + // This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless. + // It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties. + // If the type is not actually nullable, make it explicit. This still allows rewriting the property to use the + // attribute, but it will cause the other analyzer to emit a diagnostic. This guarantees that even in this case, + // the original semantics are preserved (and developers can fix the code), rather than the fixer altering things. + if (!pair.Key.Type.IsReferenceType && !isNullableValueType) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + else if (fieldFlags.IsExplicitConversionFromNonNullableToNullableMetdataType(pair.Key.Type)) + { + // Special case: the property type is not nullable, but the property metadata type is explicitly declared as + // a nullable type, and the default value is set to 'null'. In this case, we need to preserve this value. + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else if (TypedConstantInfo.TryCreate(fieldFlags.DefaultValueOperation, out fieldFlags.DefaultValue)) + { + // We have found a valid constant. If it's an enum type, we have a couple special cases to handle. + if (fieldFlags.DefaultValueOperation.Type is { TypeKind: TypeKind.Enum } operandType) + { + // As an optimization, we check whether the constant was the default value (not other enum member) + // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the + // case, the XAML infrastructure can default that values automatically, meaning we can skip the + // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. + // We also need to exclude scenarios where the property is actually nullable, but we're assigning a value. + if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) && !isNullableValueType) + { + // Before actually enabling the optimization, validate that the default value is actually + // the same as the default value of the enum (ie. the value of its first declared field). + if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && + fieldFlags.DefaultValueOperation.ConstantValue.Value == defaultValue) + { + fieldFlags.DefaultValue = null; + } + } + else if (operandType.ContainingType is not null) + { + // If the enum is nested, we need to also track the type symbol specifically, as the fully qualified + // expression we'd be using otherwise would not be the same as the metadata name, and resolving the + // enum type symbol from that in the code fixer would fail. This is an edge case, but it can happen. + fieldFlags.DefaultValueTypeReferenceId = DocumentationCommentId.CreateReferenceId(operandType); + } + } + + // Special case for named constants: in this case we want to carry the whole expression over, rather + // than serializing the constant value itself. This preserves the actual fields being referenced. + // We skip enum fields, as those are not named constants. Those will be handled by the logic above. + if (fieldFlags.DefaultValueOperation is IFieldReferenceOperation { Field: { IsConst: true, ContainingType.TypeKind: not TypeKind.Enum } }) + { + fieldFlags.DefaultValueExpressionLocation = fieldFlags.DefaultValueOperation.Syntax.GetLocation(); + } + } + else if (fieldFlags.DefaultValueOperation is IFieldReferenceOperation { Field: { ContainingType.SpecialType: SpecialType.System_String, Name: "Empty" } }) + { + // Special handling of the 'string.Empty' field. This is not a constant value, but we can still treat it as a constant, by just + // pretending this were the empty string literal instead. This way we can still support the property and convert to an attribute. + fieldFlags.DefaultValue = TypedConstantInfo.Primitive.String.Empty; + } + else if (fieldFlags.DefaultValueOperation is IDefaultValueOperation { Type: { } defaultValueExpressionType }) + { + // We don't have a constant. As a last resort, check if this is explicitly a 'default(T)' expression. + // If so, store the expression type for later, so we can validate it. We cannot validate it here, as + // we still want to execute the rest of the checks below to potentially emit more diagnostics first. + fieldFlags.DefaultValueExpressionType = defaultValueExpressionType; + } + else + { + // At this point we know the property cannot possibly be converted, so mark it as invalid. We do this + // rather than returning immediately, to still allow more diagnostics to be produced in the steps below. + fieldFlags.HasAnyDiagnostics = true; + } + } + } + + // Additional diagnostic #5: make sure that the 'propertyType' value matches the actual type + // of the property. We are intentionally not handling combinations of nullable value types here. + // The logic in 'IsValidPropertyMetadataType' will already cover all supported combinations. + if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && + (fieldFlags.PropertyType is null || !ExplicitPropertyMetadataTypeAnalyzer.IsValidPropertyMetadataType(pair.Key.Type, fieldFlags.PropertyType, context.Compilation))) + { + // Let's be extra cautious, in case the field initializer is entirely missing, we don't want to produce a garbage diagnostic + if (fieldFlags.FieldSymbol is not null && fieldFlags.PropertyType is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyTypeOnDependencyPropertyField, + fieldFlags.PropertyTypeExpressionLocation ?? fieldFlags.FieldSymbol.Locations.FirstOrDefault(), + fieldFlags.FieldSymbol, + fieldFlags.PropertyType, + pair.Key.Type, + pair.Key)); + } + + fieldFlags.HasAnyDiagnostics = true; + } + + // If the property type is an exact match for the property type in metadata, we can remove the location of that argument. + // This is because in that case there's no need to forward this type explicitly: the type will be the same by default. + if (SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType)) + { + fieldFlags.PropertyTypeExpressionLocation = null; + } + + // Also make sure the default value type matches the property type (it's not technically guaranteed). + // If this succeeds, we can safely convert the property, the generated code will be fine. If this + // does not match, the generated code will be different, and we want to avoid changing semantics. + if (fieldFlags.DefaultValueExpressionType is not null && + !SymbolEqualityComparer.Default.Equals(fieldFlags.DefaultValueExpressionType, pair.Key.Type)) + { + continue; + } + + // The field must follow the expected naming pattern. This check could be in the getter, but we're delaying + // it to here so that we can still emit all other diagnostics even if this check would otherwise fail. + if (getValueFieldSymbol.Name != $"{pair.Key.Name}Property") + { + continue; + } + + // If any diagnostics were produced, we consider the property as invalid. We use this sytem to be + // able to still execute the rest of the logic, so we can emit all applicable diagnostics at the + // same time. Otherwise we'd only ever produce one at a time, which makes for a frustrating experience. + if (fieldFlags.HasAnyDiagnostics) + { + continue; + } + + // Finally, check whether the field was valid (if so, we will have a valid location) + if (fieldFlags.FieldLocation is Location fieldLocation) + { + // Additional locations for the code fixer. The contract is shared between the analyzer and the code fixer: + // 1) The field location (this is always present) + // 2) The location of the 'typeof(...)' expression for the property type, if present + // 3) The location of the default value expression, if it should be directly carried over + Location?[] additionalLocations = + [ + fieldLocation, + fieldFlags.PropertyTypeExpressionLocation, + fieldFlags.DefaultValueExpressionLocation + ]; + + // Track the available locations, so we can extract them back. We cannot rely on the length + // of the supplied array, because Roslyn will remove all 'None' locations from the array. To + // match this behavior in tests too, we'll just filter out all 'null' elements below as well. + AdditionalLocationKind additionalLocationKind = + AdditionalLocationKind.FieldLocation | + (additionalLocations[1] is not null ? AdditionalLocationKind.PropertyTypeExpressionLocation : 0) | + (additionalLocations[2] is not null ? AdditionalLocationKind.DefaultValueExpressionLocation : 0); + + context.ReportDiagnostic(Diagnostic.Create( + UseGeneratedDependencyPropertyForManualProperty, + pair.Key.Locations.FirstOrDefault(), + additionalLocations.OfType(), + ImmutableDictionary.Create() + .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) + .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId) + .Add(AdditionalLocationKindPropertyName, additionalLocationKind.ToString()), + pair.Key)); + } + } + + // Before clearing the dictionary, move back all values to the stack + foreach (PropertyFlags propertyFlags in propertyMap.Values) + { + // Make sure the flags is cleared before returning it + propertyFlags.GetValueTargetField = null; + propertyFlags.SetValueTargetField = null; + + propertyFlagsStack.Push(propertyFlags); + } + + // Same for the field flags + foreach (FieldFlags fieldFlags in fieldMap.Values) + { + fieldFlags.FieldSymbol = null; + fieldFlags.PropertyName = null; + fieldFlags.PropertyNameExpressionLocation = null; + fieldFlags.PropertyType = null; + fieldFlags.PropertyTypeExpressionLocation = null; + fieldFlags.DefaultValueOperation = null; + fieldFlags.DefaultValue = null; + fieldFlags.DefaultValueTypeReferenceId = null; + fieldFlags.DefaultValueExpressionLocation = null; + fieldFlags.DefaultValueExpressionType = null; + fieldFlags.FieldLocation = null; + fieldFlags.HasAnyDiagnostics = false; + + fieldFlagsStack.Push(fieldFlags); + } + + // We are now done processing the symbol, we can return the dictionary. + // Note that we must clear it before doing so to avoid leaks and issues. + propertyMap.Clear(); + + PropertyMapPool.Free(propertyMap); + + // Also do the same for the stacks, except we don't need to clean them (since it roots no compilation objects) + PropertyFlagsStackPool.Free(propertyFlagsStack); + FieldFlagsStackPool.Free(fieldFlagsStack); + }); + }, SymbolKind.NamedType); + }); + } + + /// + /// Produces a new instance to pool. + /// + /// The type of flags objects to create. + /// The resulting instance to use. + private static Stack CreateFlagsStack() + where T : class, new() + { + static IEnumerable EnumerateFlags() + { + for (int i = 0; i < NumberOfPooledFlagsPerStack; i++) + { + yield return new(); + } + } + + return new(EnumerateFlags()); + } + + /// + /// Flags to track properties to warn on. + /// + private sealed class PropertyFlags + { + /// + /// The target field for the GetValue method. + /// + public IFieldSymbol? GetValueTargetField; + + /// + /// The target field for the SetValue method. + /// + public IFieldSymbol? SetValueTargetField; + } + + /// + /// Flags to track fields. + /// + private sealed class FieldFlags + { + /// + /// The symbol for the field being initialized. + /// + public IFieldSymbol? FieldSymbol; + + /// + /// The name of the property. + /// + public string? PropertyName; + + /// + /// The location for the expression defining the property name. + /// + public Location? PropertyNameExpressionLocation; + + /// + /// The type of the property (as in, of values that can be assigned to it). + /// + public ITypeSymbol? PropertyType; + + /// + /// The location for the expression defining the property type. + /// + public Location? PropertyTypeExpressionLocation; + + /// + /// The operation for the default value conversion, for deferred validation. + /// + public IOperation? DefaultValueOperation; + + /// + /// The default value to use (not present if it does not need to be set explicitly). + /// + public TypedConstantInfo? DefaultValue; + + /// + /// The documentation comment reference id for type of the default value, if needed. + /// + public string? DefaultValueTypeReferenceId; + + /// + /// The location for the default value, if available. + /// + public Location? DefaultValueExpressionLocation; + + /// + /// The type of the expression used for the default value, if validation is required. + /// + public ITypeSymbol? DefaultValueExpressionType; + + /// + /// The location of the target field being initialized. + /// + public Location? FieldLocation; + + /// + /// Indicates whether any diagnostics were produced for the field. + /// + public bool HasAnyDiagnostics; + + /// + /// Checks whether the field has an explicit conversion to a nullable metadata type (where the property isn't). + /// + /// The property type. + /// Whether the field has an explicit conversion to a nullable metadata type + public bool IsExplicitConversionFromNonNullableToNullableMetdataType(ITypeSymbol propertyType) + { + return + !SymbolEqualityComparer.Default.Equals(propertyType, PropertyType) && + !propertyType.IsDefaultValueNull() && + PropertyType!.IsDefaultValueNull(); + } + } +} + +/// +/// An enum indicating the type of additional locations that are produced by the analyzer. +/// +[Flags] +public enum AdditionalLocationKind +{ + /// + /// The location of the field to remove, always present. + /// + FieldLocation = 1 << 0, + + /// + /// The location of the property type expression, if present. + /// + PropertyTypeExpressionLocation = 1 << 1, + + /// + /// The location of the default value expression, if present. + /// + DefaultValueExpressionLocation = 1 << 2 +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 000000000..87a5d7c61 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,449 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Diagnostics; + +/// +/// A container for all instances for errors reported by analyzers in this project. +/// +internal static class DiagnosticDescriptors +{ + /// + /// The category to use for all diagnostics produced by analyzers in this project. + /// + private const string DiagnosticCategory = "DependencyPropertyGenerator"; + + /// + /// The diagnostic id for . + /// + public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDPG0017"; + + /// + /// The diagnostic id for . + /// + public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDPG0020"; + + /// + /// The diagnostic id for . + /// + public const string DependencyPropertyFieldDeclarationId = "WCTDPG0021"; + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclaration = new( + id: "WCTDPG0001", + title: "Invalid property declaration for [GeneratedDependencyProperty]", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be instance (non static) partial properties, with a getter and a setter that is not init-only.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationIsNotIncompletePartialDefinition = new( + id: "WCTDPG0002", + title: "Using [GeneratedDependencyProperty] on an invalid partial property (not incomplete partial definition)", + messageFormat: """The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [GeneratedDependencyProperty] is not a partial implementation part ([GeneratedDependencyProperty] must be used on partial property definitions with no implementation part).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsByRef = new( + id: "WCTDPG0003", + title: "Using [GeneratedDependencyProperty] on a property that returns byref", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a ref value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsRefLikeType = new( + id: "WCTDPG0004", + title: "Using [GeneratedDependencyProperty] on a property that returns byref-like", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a byref-like value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationContainingTypeIsNotDependencyObject = new( + id: "WCTDPG0005", + title: "Using [GeneratedDependencyProperty] on a property with invalid containing type", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be contained in a type that inherits from DependencyObject.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file). + /// + public static readonly DiagnosticDescriptor PropertyDeclarationRequiresCSharp13 = new( + id: "WCTDPG0006", + title: "Using [GeneratedDependencyProperty] requires C# 13", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file)", + category: typeof(UnsupportedCSharpLanguageVersionAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be contained in a project using C# 13 or greater. Make sure to add 13.0 to your .csproj/.props file.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file). + /// + public static readonly DiagnosticDescriptor LocalCachingRequiresCSharpPreview = new( + id: "WCTDPG0007", + title: "Using [GeneratedDependencyProperty] with 'IsLocalCachingEnabled' requires C# 'preview'", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and using the 'IsLocalCachingEnabled' option must be contained in a project using C# 'preview'. Make sure to add preview to your .csproj/.props file.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs'). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationWouldCauseConflicts = new( + id: "WCTDPG0008", + title: "Conflicting property declaration for [GeneratedDependencyProperty]", + messageFormat: "The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs')", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not be declared in such a way that would cause generate members to cause conflicts. In particular, they cannot be named 'Property' and be of type either 'object' or 'DependencyPropertyChangedEventArgs'.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable). + /// + public static readonly DiagnosticDescriptor NonNullablePropertyDeclarationIsNotEnforced = new( + id: "WCTDPG0009", + title: "Non-nullable dependency property is not guaranteed to not be null", + messageFormat: "The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Non-nullable properties annotated with [GeneratedDependencyProperty] should guarantee that their values will not be null upon exiting the constructor. This can be enforced by adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( + id: "WCTDPG0010", + title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueType = new( + id: "WCTDPG0011", + title: "Invalid default value type for [GeneratedDependencyProperty] use", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsPointerType = new( + id: "WCTDPG0012", + title: "Using [GeneratedDependencyProperty] on a property that returns pointer type", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a pointer value ([GeneratedDependencyProperty] must be used on properties returning a non pointer value)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a pointer value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackMixed = new( + id: "WCTDPG0013", + title: "Using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback'", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] cannot use both 'DefaultValue' and 'DefaultValueCallback' at the same time.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound = new( + id: "WCTDPG0014", + title: "Using [GeneratedDependencyProperty] with missing 'DefaultValueCallback' method", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of an accessible method in their same containing type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod = new( + id: "WCTDPG0015", + title: "Using [GeneratedDependencyProperty] with invalid 'DefaultValueCallback' method", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of a method with a valid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object').", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)". + /// + public static readonly DiagnosticDescriptor PropertyDeclarationWithPropertySuffix = new( + id: "WCTDPG0016", + title: "Using [GeneratedDependencyProperty] on a property with the 'Property' suffix", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] should not have the 'Property' suffix in their name, as it is redundant (the generated dependency properties will always add the 'Property' suffix to the name of their associated properties).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The manual property '{0}' can be converted to a partial property using [GeneratedDependencyProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)". + /// + public static readonly DiagnosticDescriptor UseGeneratedDependencyPropertyForManualProperty = new( + id: UseGeneratedDependencyPropertyForManualPropertyId, + title: "Prefer using [GeneratedDependencyProperty] over manual properties", + messageFormat: """The manual property '{0}' can be converted to a partial property using [GeneratedDependencyProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Manual properties should be converted to partial properties using [GeneratedDependencyProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)". + /// + public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeType = new( + id: "WCTDPG0018", + title: "Invalid dependency property targeted attribute type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must correctly be resolved to valid types.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)". + /// + public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression = new( + id: "WCTDPG0019", + title: "Invalid dependency property targeted attribute expression", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must have arguments using supported expressions.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)". + /// + public static readonly DiagnosticDescriptor IncorrectDependencyPropertyFieldDeclaration = new( + id: IncorrectDependencyPropertyFieldDeclarationId, + title: "Incorrect dependency property field declaration", + messageFormat: "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should be declared as 'public static readonly', and not be nullable.", + helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); + + /// + /// "The property '{0}' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types)". + /// + public static readonly DiagnosticDescriptor DependencyPropertyFieldDeclaration = new( + id: DependencyPropertyFieldDeclarationId, + title: "Dependency property declared as a property", + messageFormat: "The property '{0}' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types.", + helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)". + /// + public static readonly DiagnosticDescriptor UnnecessaryDependencyPropertyExplicitMetadataType = new( + id: "WCTDPG0022", + title: "Unnecessary dependency property explicit metadata type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' should only do so when the explicit type would not match the declared property type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)". + /// + public static readonly DiagnosticDescriptor IncompatibleDependencyPropertyExplicitMetadataType = new( + id: "WCTDPG0023", + title: "Incompatible dependency property explicit metadata type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' must do so with a type that is compatible with the declared property type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property). + /// + public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNotNullablePropertyDeclaration = new( + id: "WCTDPG0024", + title: "Non-nullable dependency property using [AllowNull] incorrectly", + messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Non-nullable properties annotated with [GeneratedDependencyProperty] using [AllowNull] should have at least one generated getter or setter method (eg. 'OnNameGet', if the property is called 'Name') implemented as null-resilient (by adding [NotNull] on the 'propertyValue' parameter) to ensure assigning null values does not break the nullable annotations on the property.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null). + /// + public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNullablePropertyDeclaration = new( + id: "WCTDPG0025", + title: "Nullable dependency property using [NotNull] incorrectly", + messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated getter method (eg. 'OnNameGet', if the property is called 'Name') with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated setter method (eg. 'OnNameSet', if the property is called 'Name') with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names). + /// + public static readonly DiagnosticDescriptor NoPropertySuffixOnDependencyPropertyField = new( + id: "WCTDPG0026", + title: "No 'Property' suffix on dependency property field name", + messageFormat: "The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should be named following the recommended naming convention (they should use the 'Property' suffix in their names).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix). + /// + public static readonly DiagnosticDescriptor InvalidPropertyNameOnDependencyPropertyField = new( + id: "WCTDPG0027", + title: "Invalid property name in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property names in metadata following the recommended naming convention (they should match the name of the declared fields, minus the 'Property' suffix).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field). + /// + public static readonly DiagnosticDescriptor MismatchedPropertyNameOnDependencyPropertyField = new( + id: "WCTDPG0028", + title: "Mismatched property name in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property names in metadata following the recommended naming convention (they should exactly match the name of the wrapping properties).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration). + /// + public static readonly DiagnosticDescriptor InvalidOwningTypeOnDependencyPropertyField = new( + id: "WCTDPG0029", + title: "Invalid owning type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare owning types in metadata matching their containing types.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property). + /// + public static readonly DiagnosticDescriptor InvalidPropertyTypeOnDependencyPropertyField = new( + id: "WCTDPG0030", + title: "Invalid property type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property types in metadata matching the type of their wrapping properties, or with a valid type conversion between the two.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, setting the property value to a non 'null' value upon object construction, and/or suppressing the diagnostic if this is the intended behavior). + /// + public static readonly DiagnosticDescriptor InvalidDefaultValueNullOnDependencyPropertyField = new( + id: "WCTDPG0031", + title: "Invalid 'null' default value in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, setting the property value to a non 'null' value upon object construction, and/or suppressing the diagnostic if this is the intended behavior)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the property value should be set to a non 'null' value upon object construction.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or suppressing the diagnostic if this is the intended behavior). + /// + public static readonly DiagnosticDescriptor InvalidDefaultValueTypeOnDependencyPropertyField = new( + id: "WCTDPG0032", + title: "Invalid default value type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or suppressing the diagnostic if this is the intended behavior)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs new file mode 100644 index 000000000..9122fc28e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Diagnostics; + +/// +/// A container for all instances for suppressed diagnostics by analyzers in this project. +/// +internal static class SuppressionDescriptors +{ + /// + /// Gets a for a property using [GeneratedDependencyProperty] with an attribute list targeting the 'static' keyword. + /// + public static readonly SuppressionDescriptor StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration = new( + id: "WCTDPSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Properties using [GeneratedDependencyProperty] can use [static:] attribute lists to forward attributes to the generated dependency property fields."); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs new file mode 100644 index 000000000..ce3875685 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs @@ -0,0 +1,64 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.SuppressionDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// +/// A diagnostic suppressor to suppress CS0658 warnings for properties with [GeneratedDependencyProperty] using a [static:] attribute list. +/// +/// +/// That is, this diagnostic suppressor will suppress the following diagnostic: +/// +/// public partial class MyControl : Control +/// { +/// [GeneratedDependencyProperty] +/// [static: JsonIgnore] +/// public partial string? Name { get; set; } +/// } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor : DiagnosticSuppressor +{ + /// + public override ImmutableArray SupportedSuppressions { get; } = [StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration]; + + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) + { + SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + + // Check that the target is effectively [static:] over a property declaration, which is the only case we are interested in + if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: PropertyDeclarationSyntax propertyDeclaration, Identifier: SyntaxToken(SyntaxKind.StaticKeyword) }) + { + SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + + // Get the property symbol from the property declaration + ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken); + + // Check if the property is using [GeneratedDependencyProperty], in which case we should suppress the warning + if (declaredSymbol is IPropertySymbol propertySymbol && propertySymbol.HasAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols)) + { + context.ReportSuppression(Suppression.Create(StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration, diagnostic)); + } + } + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs new file mode 100644 index 000000000..e35abebf6 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs @@ -0,0 +1,36 @@ +// +#pragma warning disable +#nullable enable + +// 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 GENERATED_DEPENDENCY_PROPERTY_EMBEDDED_MODE + +namespace CommunityToolkit.WinUI +{ +#if GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + using DependencyProperty = global::Windows.UI.Xaml.DependencyProperty; +#else + using DependencyProperty = global::Microsoft.UI.Xaml.DependencyProperty; +#endif + + /// + /// Provides constant values that can be used as default values for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("", "")] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class GeneratedDependencyProperty + { + /// + /// + /// This constant is only meant to be used in assignments to (because + /// cannot be used in that context, as it is not a constant, but rather a static field). Using this constant in other scenarios is undefined behavior. + /// + public const object UnsetValue = null!; + } +} + +#endif diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs new file mode 100644 index 000000000..16081e777 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,110 @@ +// +#pragma warning disable +#nullable enable + +// 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 GENERATED_DEPENDENCY_PROPERTY_ATTRIBUTE_EMBEDDED_MODE + +namespace CommunityToolkit.WinUI +{ +#if GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + using DependencyObject = global::Windows.UI.Xaml.DependencyObject; + using DependencyProperty = global::Windows.UI.Xaml.DependencyProperty; + using PropertyMetadata = global::Windows.UI.Xaml.PropertyMetadata; +#else + using DependencyObject = global::Microsoft.UI.Xaml.DependencyObject; + using DependencyProperty = global::Microsoft.UI.Xaml.DependencyProperty; + using PropertyMetadata = global::Microsoft.UI.Xaml.PropertyMetadata; +#endif + + /// + /// An attribute that indicates that a given partial property should generate a backing . + /// In order to use this attribute, the containing type has to inherit from . + /// + /// This attribute can be used as follows: + /// + /// partial class MyClass : DependencyObject + /// { + /// [GeneratedDependencyProperty] + /// public partial string? Name { get; set; } + /// } + /// + /// + /// + /// + /// + /// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# 13 (or 'preview') must be used. + /// + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [global::System.CodeDom.Compiler.GeneratedCode("", "")] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Diagnostics.Conditional("GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_PRESERVE_ATTRIBUTES")] + internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attribute + { + /// + /// Gets or sets a value indicating the default value to set for the property. + /// + /// + /// + /// If not set, the default value will be , for all property types. If there is no callback + /// registered for the generated property, will not be set at all. + /// + /// + /// To set the default value to , use . + /// + /// + /// Using this property is mutually exclusive with . + /// + /// + public object? DefaultValue { get; init; } = null; + + /// + /// Gets or sets the name of the method that will be invoked to produce the default value of the + /// property, for each instance of the containing type. The referenced method needs to return either + /// an , or a value of exactly the property type, and it needs to be parameterless. + /// + /// + /// Using this property is mutually exclusive with . + /// +#if NET8_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DisallowNull] +#endif + public string? DefaultValueCallback { get; init; } = null!; + + /// + /// Gets or sets a value indicating whether or not property values should be cached locally, to improve performance. + /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. + /// + /// + /// Local caching is disabled by default. It should be disabled in scenarios where the values of the dependency + /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. + /// + public bool IsLocalCacheEnabled { get; init; } = false; + + /// + /// Gets or sets the type to use to register the property in metadata. The default value will exactly match the property type. + /// + /// + /// + /// This property allows customizing the property type in metadata, in advanced scenarios. For instance, it can be used to define + /// properties of a type (e.g. ) as just using in metadata. + /// This allows working around some issues primarily around classic (reflection-based) binding in XAML. + /// + /// + /// This property should only be set when actually required (e.g. to ensure a specific scenario can work). The default behavior + /// (i.e. the property type in metadata matching the declared property type) should work correctly in the vast majority of cases. + /// + /// +#if NET8_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DisallowNull] +#endif + public global::System.Type? PropertyType { get; init; } = null!; + } +} + +#endif diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 000000000..4737574d9 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,46 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AnalyzerConfigOptionsExtensions +{ + /// + /// Gets the boolean value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions options, string propertyName, bool defaultValue = false) + { + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) + { + if (bool.TryParse(propertyValue, out bool booleanPropertyValue)) + { + return booleanPropertyValue; + } + } + + return defaultValue; + } + + /// + /// Tries to get a value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The resulting property value. + /// Whether the property value was retrieved.. + public static bool TryGetMSBuildStringPropertyValue(this AnalyzerConfigOptions options, string propertyName, [NotNullWhen(true)] out string? propertyValue) + { + return options.TryGetValue($"build_property.{propertyName}", out propertyValue); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs new file mode 100644 index 000000000..001ca6810 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,149 @@ +// 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AttributeDataExtensions +{ + /// + /// Tries to get the location of the input instance. + /// + /// The input instance to get the location for. + /// The resulting location for , if a syntax reference is available. + public static Location? GetLocation(this AttributeData attributeData) + { + if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) + { + return null; + } + + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + /// + /// Tries to get the location of a named argument in an input instance. + /// + /// The input instance to get the location for. + /// The name of the argument to look for. + /// The cancellation token for the operation. + /// The resulting location for , if a syntax reference is available. + public static Location? GetNamedArgumentOrAttributeLocation(this AttributeData attributeData, string name, CancellationToken token = default) + { + if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) + { + return null; + } + + // If we can recover the syntax node, look for the target named argument + if (syntaxReference.GetSyntax(token) is AttributeSyntax { ArgumentList: { } argumentList }) + { + foreach (AttributeArgumentSyntax argument in argumentList.Arguments) + { + if (argument.NameEquals?.Name.Identifier.Text == name) + { + return argument.GetLocation(); + } + } + } + + // Otherwise, fallback to the location of the whole attribute + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + /// + /// Tries to get a constructor argument at a given index from the input instance. + /// + /// The type of constructor argument to retrieve. + /// The target instance to get the argument from. + /// The index of the argument to try to retrieve. + /// The resulting argument, if it was found. + /// Whether or not an argument of type at position was found. + public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result) + { + if (attributeData.ConstructorArguments.Length > index && + attributeData.ConstructorArguments[index].Value is T argument) + { + result = argument; + + return true; + } + + result = default; + + return false; + } + + /// + /// Tries to get a given named argument value from an instance, or a default value. + /// + /// The type of argument to check. + /// The target instance to check. + /// The name of the argument to check. + /// The default value to return if the argument is not found. + /// The argument value, or . + public static T? GetNamedArgument(this AttributeData attributeData, string name, T? defaultValue = default) + { + if (TryGetNamedArgument(attributeData, name, out T? value)) + { + return value; + } + + return defaultValue; + } + + /// + /// Tries to get a given named argument value from an instance, if present. + /// + /// The type of argument to check. + /// The target instance to check. + /// The name of the argument to check. + /// The resulting argument value, if present. + /// Whether or not contains an argument named with a valid value. + public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) + { + if (TryGetNamedArgument(attributeData, name, out TypedConstant constantValue)) + { + value = (T?)constantValue.Value; + + return true; + } + + value = default; + + return false; + } + + /// + /// Tries to get a given named argument value from an instance, if present. + /// + /// The target instance to check. + /// The name of the argument to check. + /// The resulting argument value, if present. + /// Whether or not contains an argument named with a valid value. + public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out TypedConstant value) + { + foreach (KeyValuePair argument in attributeData.NamedArguments) + { + if (argument.Key == name) + { + value = argument.Value; + + return true; + } + } + + value = default; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs new file mode 100644 index 000000000..f3c256381 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class CompilationExtensions +{ + /// + /// Checks whether a given compilation (assumed to be for C#) is using at least a given language version. + /// + /// The to consider for analysis. + /// The minimum language version to check. + /// Whether is using at least the specified language version. + public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, LanguageVersion languageVersion) + { + return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; + } + + /// + /// Checks whether a given compilation (assumed to be for C#) is using the preview language version. + /// + /// The to consider for analysis. + /// Whether is using the preview language version. + public static bool IsLanguageVersionPreview(this Compilation compilation) + { + return ((CSharpCompilation)compilation).LanguageVersion == LanguageVersion.Preview; + } + + /// + /// Gets whether the current target is a WinRT application (i.e. legacy UWP). + /// + /// The input instance to inspect. + /// Whether the current target is a WinRT application. + public static bool IsWindowsRuntimeApplication(this Compilation compilation) + { + return compilation.Options.OutputKind == OutputKind.WindowsRuntimeApplication; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs new file mode 100644 index 000000000..dbba4c2db --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs @@ -0,0 +1,50 @@ +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class IMethodSymbolExtensions +{ + /// + /// Checks whether or not a given symbol has a return attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// Whether or not has a return attribute with the specified type. + public static bool HasReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray typeSymbols) + { + return TryGetReturnAttributeWithAnyType(symbol, typeSymbols, out _); + } + + /// + /// Tries to get a return attribute with any of the specified types. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The first return attribute of a type matching any type in , if found. + /// Whether or not has a return attribute with the specified type. + public static bool TryGetReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray typeSymbols, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetReturnTypeAttributes()) + { + if (typeSymbols.Contains(attribute.AttributeClass!, SymbolEqualityComparer.Default)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs new file mode 100644 index 000000000..1109c8d53 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs @@ -0,0 +1,74 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class IOperationExtensions +{ + /// + /// Checks whether a given operation represents a default constant value. + /// + /// The input instance. + /// Whether represents a default constant value. + public static bool IsConstantValueDefault(this IOperation operation) + { + if (operation is not { Type: not null, ConstantValue.HasValue: true }) + { + return false; + } + + // Easy check for reference types + if (operation is { Type.IsReferenceType: true, ConstantValue.Value: null }) + { + return true; + } + + // Equivalent check for nullable value types too + if (operation is { Type.SpecialType: SpecialType.System_Nullable_T, ConstantValue.Value: null }) + { + return true; + } + + // Special case enum types as well (enum types only support a subset of primitive types) + if (operation.Type is INamedTypeSymbol { TypeKind: TypeKind.Enum, EnumUnderlyingType: { } underlyingType }) + { + return (underlyingType.SpecialType, operation.ConstantValue.Value) switch + { + (SpecialType.System_Byte, default(byte)) or + (SpecialType.System_SByte, default(sbyte)) or + (SpecialType.System_Int16, default(short)) or + (SpecialType.System_UInt16, default(ushort)) or + (SpecialType.System_Int32, default(int)) or + (SpecialType.System_UInt32, default(uint)) or + (SpecialType.System_Int64, default(long)) or + (SpecialType.System_UInt64, default(ulong)) => true, + _ => false + }; + } + + // Manually match for known primitive types (this should be kept in sync with 'IsWellKnownWinRTProjectedValueType') + return (operation.Type.SpecialType, operation.ConstantValue.Value) switch + { + (SpecialType.System_Byte, default(byte)) or + (SpecialType.System_SByte, default(sbyte)) or + (SpecialType.System_Int16, default(short)) or + (SpecialType.System_UInt16, default(ushort)) or + (SpecialType.System_Char, default(char)) or + (SpecialType.System_Int32, default(int)) or + (SpecialType.System_UInt32, default(uint)) or + (SpecialType.System_Int64, default(long)) or + (SpecialType.System_UInt64, default(ulong)) or + (SpecialType.System_Boolean, default(bool)) => true, + (SpecialType.System_Single, float x) when BitConverter.DoubleToInt64Bits(x) == 0 => true, + (SpecialType.System_Double, double x) when BitConverter.DoubleToInt64Bits(x) == 0 => true, + _ => false + }; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs new file mode 100644 index 000000000..df1e8d75b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs @@ -0,0 +1,132 @@ +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ISymbolExtensions +{ + /// + /// Gets the fully qualified name for a given symbol (without nullability annotations). + /// + /// The input instance. + /// The fully qualified name for . + public static string GetFullyQualifiedName(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + /// + /// Gets the fully qualified name for a given symbol, including nullability annotations + /// + /// The input instance. + /// The fully qualified name for . + public static string GetFullyQualifiedNameWithNullabilityAnnotations(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + } + + /// + /// Checks whether or not a given symbol has an attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// Whether or not has an attribute with the specified type. + public static bool HasAttributeWithAnyType(this ISymbol symbol, ImmutableArray typeSymbols) + { + return TryGetAttributeWithAnyType(symbol, typeSymbols, out _); + } + + /// + /// Tries to get an attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The resulting attribute, if it was found. + /// Whether or not has an attribute with the specified type. + public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } + + /// + /// Tries to get an attribute with any of the specified types. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The first attribute of a type matching any type in , if found. + /// Whether or not has an attribute with the specified type. + public static bool TryGetAttributeWithAnyType(this ISymbol symbol, ImmutableArray typeSymbols, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (typeSymbols.Contains(attribute.AttributeClass!, SymbolEqualityComparer.Default)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } + + /// + /// Calculates the effective accessibility for a given symbol. + /// + /// The instance to check. + /// The effective accessibility for . + public static Accessibility GetEffectiveAccessibility(this ISymbol symbol) + { + // Start by assuming it's visible + Accessibility visibility = Accessibility.Public; + + // Handle special cases + switch (symbol.Kind) + { + case SymbolKind.Alias: return Accessibility.Private; + case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol); + case SymbolKind.TypeParameter: return Accessibility.Private; + } + + // Traverse the symbol hierarchy to determine the effective accessibility + while (symbol is not null && symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + case Accessibility.NotApplicable: + case Accessibility.Private: + return Accessibility.Private; + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + visibility = Accessibility.Internal; + break; + } + + symbol = symbol.ContainingSymbol; + } + + return visibility; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs new file mode 100644 index 000000000..0596f1650 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs @@ -0,0 +1,60 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ITypeParameterSymbolExtensions +{ + /// + /// Checks whether a given type parameter is a reference type. + /// + /// The input instance to check. + /// Whether the input type parameter is a reference type. + public static bool IsReferenceTypeOrIndirectlyConstrainedToReferenceType(this ITypeParameterSymbol symbol) + { + // The type is definitely a reference type (e.g. it has the 'class' constraint) + if (symbol.IsReferenceType) + { + return true; + } + + // The type is definitely a value type (e.g. it has the 'struct' constraint) + if (symbol.IsValueType) + { + return false; + } + + foreach (ITypeSymbol constraintType in symbol.ConstraintTypes) + { + // Recurse on the type parameter first (e. g. we might indirectly be getting a 'class' constraint) + if (constraintType is ITypeParameterSymbol typeParameter && + typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType()) + { + return true; + } + + // Special constraint type that type parameters can derive from. Note that for concrete enum + // types, the 'Enum' constraint isn't sufficient, they'd also have e.g. 'struct', which is + // already checked before. If a type parameter only has 'Enum', then it should be considered + // a reference type. + if (constraintType.SpecialType is SpecialType.System_Delegate or SpecialType.System_Enum) + { + return true; + } + + // Only check for classes (an interface doesn't guarantee the type argument will be a reference type) + if (constraintType.TypeKind is TypeKind.Class) + { + return true; + } + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 000000000..340192841 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,297 @@ +// 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.Diagnostics.CodeAnalysis; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ITypeSymbolExtensions +{ + /// + /// Checks whether a given type has a default value of . + /// + /// The input instance to check. + /// Whether the default value of is . + public static bool IsDefaultValueNull(this ITypeSymbol symbol) + { + // Special case unconstrained type parameters: their default value is not explicitly 'null' for all cases. + // If we do have a type parameter, check that it does have some reference type constraint on it. + if (symbol is ITypeParameterSymbol typeParameter) + { + return typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType(); + } + + return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + + /// + /// Checks whether a given type symbol represents some type. + /// + /// The input instance to check. + /// Whether represents some type. + public static bool IsNullableValueType(this ITypeSymbol symbol) + { + return symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + + /// + /// Checks whether a given type symbol represents a type with a specific underlying type. + /// + /// The input instance to check. + /// The underlyign type to check. + /// Whether represents a type with a specific underlying type. + public static bool IsNullableValueTypeWithUnderlyingType(this ITypeSymbol symbol, ITypeSymbol underlyingType) + { + if (!IsNullableValueType(symbol)) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)symbol).TypeArguments[0], underlyingType); + } + + /// + /// Tries to get the default value of a given enum type. + /// + /// The input instance to check. + /// The resulting default value for , if it was an enum type. + /// Whether was retrieved successfully. + public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNullWhen(true)] out object? value) + { + if (symbol.TypeKind is not TypeKind.Enum) + { + value = null; + + return false; + } + + // The default value of the enum is the value of its first constant field + foreach (ISymbol memberSymbol in symbol.GetMembers()) + { + if (memberSymbol is IFieldSymbol { IsConst: true, ConstantValue: object defaultValue }) + { + value = defaultValue; + + return true; + } + } + + value = null; + + return false; + } + + /// + /// Tries to get the name of the enum field matching a given value. + /// + /// The input instance to check. + /// The value for to try to get the field for. + /// The name of the field with the specified value, if found. + /// Whether was successfully retrieved. + public static bool TryGetEnumFieldName(this ITypeSymbol symbol, object value, [NotNullWhen(true)] out string? fieldName) + { + if (symbol.TypeKind is not TypeKind.Enum) + { + fieldName = null; + + return false; + } + + // The default value of the enum is the value of its first constant field + foreach (ISymbol memberSymbol in symbol.GetMembers()) + { + if (memberSymbol is not IFieldSymbol { IsConst: true, ConstantValue: object fieldValue } fieldSymbol) + { + continue; + } + + if (fieldValue.Equals(value)) + { + fieldName = fieldSymbol.Name; + + return true; + } + } + fieldName = null; + + return false; + } + + /// + /// Checks whether or not a given type symbol has a specified fully qualified metadata name. + /// + /// The input instance to check. + /// The full name to check. + /// Whether has a full name equals to . + public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.WrittenSpan.SequenceEqual(name.AsSpan()); + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The instance to check for inheritance from. + /// Whether or not inherits from . + public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType; + + while (currentBaseTypeSymbol is not null) + { + if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol)) + { + return true; + } + + currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType; + } + + return false; + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The full name of the type to check for inheritance. + /// Whether or not inherits from . + public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + { + INamedTypeSymbol? baseType = typeSymbol.BaseType; + + while (baseType is not null) + { + if (baseType.HasFullyQualifiedMetadataName(name)) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + + /// + /// Gets the fully qualified metadata name for a given instance. + /// + /// The input instance. + /// The fully qualified metadata name for . + public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.ToString(); + } + + /// + /// Appends the fully qualified metadata name for a given symbol to a target builder. + /// + /// The input instance. + /// The target instance. + public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder builder) + { + static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (i.e. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with no namespace just have their metadata name directly written + case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with a containing non-global namespace also append a leading '.' + case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: + BuildFrom(namespaceSymbol, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Nested types append a leading '+' + case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: + BuildFrom(typeSymbol, in builder); + builder.Add('+'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + BuildFrom(symbol, in builder); + } + + /// + /// Checks whether a given type is contained in a namespace with a specified name. + /// + /// The input instance. + /// The namespace to check. + /// Whether is contained within . + public static bool IsContainedInNamespace(this ITypeSymbol symbol, string? namespaceName) + { + static void BuildFrom(INamespaceSymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (i.e. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + // Special case for no containing namespace + if (symbol.ContainingNamespace is not { } containingNamespace) + { + return namespaceName is null; + } + + // Special case if the type is directly in the global namespace + if (containingNamespace.IsGlobalNamespace) + { + return containingNamespace.MetadataName == namespaceName; + } + + using ImmutableArrayBuilder builder = new(); + + BuildFrom(containingNamespace, in builder); + + return builder.WrittenSpan.SequenceEqual(namespaceName.AsSpan()); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs new file mode 100644 index 000000000..7c3169d03 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -0,0 +1,67 @@ +// 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.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// +/// +/// The original value. +/// The original value. +internal readonly struct GeneratorAttributeSyntaxContextWithOptions( + GeneratorAttributeSyntaxContext syntaxContext, + AnalyzerConfigOptions globalOptions) +{ + /// + public SyntaxNode TargetNode { get; } = syntaxContext.TargetNode; + + /// + public ISymbol TargetSymbol { get; } = syntaxContext.TargetSymbol; + + /// + public SemanticModel SemanticModel { get; } = syntaxContext.SemanticModel; + + /// + public ImmutableArray Attributes { get; } = syntaxContext.Attributes; + + /// + public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; +} + +/// +/// Extension methods for . +/// +internal static class IncrementalGeneratorInitializationContextExtensions +{ + /// + public static IncrementalValuesProvider ForAttributeWithMetadataNameAndOptions( + this IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName, + Func predicate, + Func transform) + { + // Invoke 'ForAttributeWithMetadataName' normally, but just return the context directly + IncrementalValuesProvider syntaxContext = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate, + static (context, token) => context); + + // Do the same for the analyzer config options + IncrementalValueProvider configOptions = context.AnalyzerConfigOptionsProvider.Select(static (provider, token) => provider.GlobalOptions); + + // Merge the two and invoke the provided transform on these two values. Neither value + // is equatable, meaning the pipeline will always re-run until this point. This is + // intentional: we don't want any symbols or other expensive objects to be kept alive + // across incremental steps, especially if they could cause entire compilations to be + // rooted, which would significantly increase memory use and introduce more GC pauses. + // In this specific case, flowing non equatable values in a pipeline is therefore fine. + return syntaxContext.Combine(configOptions).Select((input, token) => transform(new GeneratorAttributeSyntaxContextWithOptions(input.Left, input.Right), token)); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalValueProviderExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalValueProviderExtensions.cs new file mode 100644 index 000000000..4dde6f3a7 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalValueProviderExtensions.cs @@ -0,0 +1,71 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for . +/// +internal static class IncrementalValuesProviderExtensions +{ + /// + /// Groups items in a given sequence by a specified key. + /// + /// The type of value that this source provides access to. + /// The type of grouped key elements. + /// The type of projected elements. + /// The type of resulting items. + /// The input instance. + /// The key selection . + /// The element selection . + /// The result selection . + /// An with the grouped results. + public static IncrementalValuesProvider GroupBy( + this IncrementalValuesProvider source, + Func keySelector, + Func elementSelector, + Func<(TKey Key, EquatableArray Values), TResult> resultSelector) + where TValues : IEquatable + where TKey : IEquatable + where TElement : IEquatable + where TResult : IEquatable + { + return source.Collect().SelectMany((item, token) => + { + Dictionary.Builder> map = []; + + foreach (TValues value in item) + { + TKey key = keySelector(value); + TElement element = elementSelector(value); + + if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) + { + builder = ImmutableArray.CreateBuilder(); + + map.Add(key, builder); + } + + builder.Add(element); + } + + token.ThrowIfCancellationRequested(); + + using ImmutableArrayBuilder result = new(); + + foreach (KeyValuePair.Builder> entry in map) + { + result.Add(resultSelector((entry.Key, entry.Value.ToImmutable()))); + } + + return result.ToImmutable(); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs new file mode 100644 index 000000000..01a9a5ec2 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs @@ -0,0 +1,104 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Helpers; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class IndentedTextWriterExtensions +{ + /// + /// Writes the following attributes into a target writer: + /// + /// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + /// [global::System.Diagnostics.DebuggerNonUserCode] + /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + /// + /// + /// The instance to write into. + /// The name of the generator. + /// Whether to use fully qualified type names or not. + /// Whether to also include the attribute for non-user code. + public static void WriteGeneratedAttributes( + this IndentedTextWriter writer, + string generatorName, + bool useFullyQualifiedTypeNames = true, + bool includeNonUserCodeAttributes = true) + { + // We can use this class to get the assembly, as all files for generators are just included + // via shared projects. As such, the assembly will be the same as the generator type itself. + Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version; + + if (useFullyQualifiedTypeNames) + { + writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]"""); + writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"""); + } + } + else + { + writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[DebuggerNonUserCode]"""); + writer.WriteLine($$"""[ExcludeFromCodeCoverage]"""); + } + } + } + + /// + /// Writes a series of members separated by one line between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteLineSeparatedMembers( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + if (i > 0) + { + writer.WriteLine(); + } + + callback(items[i], writer); + } + } + + /// + /// Writes a series of initialization expressions separated by a comma between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteInitializationExpressions( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + callback(items[i], writer); + + if (i < items.Length - 1) + { + writer.WriteLine(","); + } + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs new file mode 100644 index 000000000..fff56a90f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -0,0 +1,48 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolInfoExtensions +{ + /// + /// Tries to get the resolved attribute type symbol from a given value. + /// + /// The value to check. + /// The resulting attribute type symbol, if correctly resolved. + /// Whether is resolved to a symbol. + /// + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just + /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore + /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. + /// + public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) + { + ISymbol? attributeSymbol = symbolInfo.Symbol; + + // If no symbol is selected and there is a single candidate symbol, use that + if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol]) + { + attributeSymbol = candidateSymbol; + } + + // Extract the symbol from either the current one or the containing type + if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol) + { + typeSymbol = null; + + return false; + } + + typeSymbol = resultingSymbol; + + return true; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..6b966edaf --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,39 @@ +// 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.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// A with some extension methods for C# syntax kinds. +/// +internal static partial class SyntaxKindExtensions +{ + /// + /// Converts an of values to one of their underlying type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsUnderlyingType(this ImmutableArray array) + { + ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As, SyntaxKind[]?>(ref array); + + return Unsafe.As>(ref underlyingArray); + } + + /// + /// Converts an of values to one of their real type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsSyntaxKindArray(this ImmutableArray array) + { + SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As, ushort[]?>(ref array); + + return Unsafe.As>(ref typedArray); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxNodeExtensions.cs new file mode 100644 index 000000000..73c835beb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -0,0 +1,76 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// A with some extension methods for C# syntax nodes. +/// +internal static partial class SyntaxNodeExtensions +{ + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } + + /// + public static TNode? FirstAncestor(this SyntaxNode node, Func? predicate = null, bool ascendOutOfTrivia = true) + where TNode : SyntaxNode + { + // Helper method ported from 'SyntaxNode' + static SyntaxNode? GetParent(SyntaxNode node, bool ascendOutOfTrivia) + { + SyntaxNode? parent = node.Parent; + + if (parent is null && ascendOutOfTrivia) + { + if (node is IStructuredTriviaSyntax structuredTrivia) + { + parent = structuredTrivia.ParentTrivia.Token.Parent; + } + } + + return parent; + } + + // Traverse all parents and find the first one of the target type + for (SyntaxNode? parentNode = GetParent(node, ascendOutOfTrivia); + parentNode is not null; + parentNode = GetParent(parentNode, ascendOutOfTrivia)) + { + if (parentNode is TNode candidateNode && predicate?.Invoke(candidateNode) != false) + { + return candidateNode; + } + } + + return null; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs new file mode 100644 index 000000000..0e29a31db --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input value. + /// The resulting value for . + public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind) + { + syntaxKind = syntaxToken.Kind(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTriviaExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTriviaExtensions.cs new file mode 100644 index 000000000..74b8d01e4 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTriviaExtensions.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTriviaExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input value. + /// The resulting value for . + public static void Deconstruct(this SyntaxTrivia syntaxTrivia, out SyntaxKind syntaxKind) + { + syntaxKind = syntaxTrivia.Kind(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs new file mode 100644 index 000000000..56519947c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -0,0 +1,84 @@ +// 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.GeneratedDependencyProperty.Constants; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for WinRT scenarios. +/// +internal static class WinRTExtensions +{ + /// + /// Checks whether a given type is a well known WinRT projected value type (ie. a type that XAML can default). + /// + /// The input instance to check. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether is a well known WinRT projected value type.. + public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, bool useWindowsUIXaml) + { + // Early check: we don't care about type parameters, only about named types + if (symbol is not INamedTypeSymbol) + { + return false; + } + + // This method only cares about non nullable value types + if (symbol.IsDefaultValueNull()) + { + return false; + } + + // There is a special case for this: if the type of the property is a built-in WinRT + // projected enum type or struct type (ie. some projected value type in general, except + // for 'Nullable' values), then we can just use 'null' and bypass creating the property + // metadata. The WinRT runtime will automatically instantiate a default value for us. + if (symbol.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + { + return true; + } + + // Special case for projected numeric types + if (symbol.Name is "Matrix3x2" or "Matrix4x4" or "Plane" or "Quaternion" or "Vector2" or "Vector3" or "Vector4" && + symbol.IsContainedInNamespace("System.Numerics")) + { + return true; + } + + // Special case a few more well known value types that are mapped for WinRT + if (symbol.Name is "Point" or "Rect" or "Size" && + symbol.IsContainedInNamespace("Windows.Foundation")) + { + return true; + } + + // Special case two more system types + if (symbol is { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + { + return true; + } + + // Lastly, special case the well known primitive types + if (symbol.SpecialType is + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_Char or + SpecialType.System_Int32 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Boolean or + SpecialType.System_Single or + SpecialType.System_Double) + { + return true; + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/EquatableArray{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/EquatableArray{T}.cs new file mode 100644 index 000000000..4a1d3605a --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/EquatableArray{T}.cs @@ -0,0 +1,216 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// Extensions for . +/// +internal static class EquatableArray +{ + /// + /// Creates an instance from a given . + /// + /// The type of items in the input array. + /// The input instance. + /// An instance from a given . + public static EquatableArray AsEquatableArray(this ImmutableArray array) + where T : IEquatable + { + return new(array); + } +} + +/// +/// An imutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +/// The input to wrap. +internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array = ImmutableCollectionsMarshal.AsArray(array); + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref AsImmutableArray().ItemRef(index); + } + + /// + /// Gets a value indicating whether the current array is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsEmpty; + } + + /// + /// Gets a value indicating whether the current array is default or empty. + /// + public bool IsDefaultOrEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsDefaultOrEmpty; + } + + /// + /// Gets the length of the current array. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().Length; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override unsafe int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + if (typeof(T) == typeof(byte)) + { + ReadOnlySpan span = array; + ref T r0 = ref MemoryMarshal.GetReference(span); + ref byte r1 = ref Unsafe.As(ref r0); + + fixed (byte* p = &r1) + { + ReadOnlySpan bytes = new(p, span.Length); + + hashCode.AddBytes(bytes); + } + } + else + { + foreach (T item in array) + { + hashCode.Add(item); + } + } + + return hashCode.ToHashCode(); + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return ImmutableCollectionsMarshal.AsImmutableArray(this.array); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + /// + /// Copies the contents of this instance. to a mutable array. + /// + /// The newly instantiated array. + public T[] ToArray() + { + return [.. AsImmutableArray()]; + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs new file mode 100644 index 000000000..9e3728b56 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs @@ -0,0 +1,503 @@ +// 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.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +#pragma warning disable CS0809, IDE0009, IDE1006, IDE0048, CA1065 + +namespace System; + +/// +/// A polyfill type that mirrors some methods from on .7. +/// +internal struct HashCode +{ + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static readonly uint seed = GenerateGlobalSeed(); + + private uint v1, v2, v3, v4; + private uint queue1, queue2, queue3; + private uint length; + + /// + /// Initializes the default seed. + /// + /// A random seed. + private static unsafe uint GenerateGlobalSeed() + { + byte[] bytes = new byte[4]; + + RandomNumberGenerator.Create().GetBytes(bytes); + + return BitConverter.ToUInt32(bytes, 0); + } + + /// + /// Combines a value into a hash code. + /// + /// The type of the value to combine into the hash code. + /// The value to combine into the hash code. + /// The hash code that represents the value. + public static int Combine(T1 value) + { + uint hc1 = (uint)(value?.GetHashCode() ?? 0); + uint hash = MixEmptyState(); + + hash += 4; + hash = QueueRound(hash, hc1); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines two values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hash = MixEmptyState(); + + hash += 8; + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines three values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hash = MixEmptyState(); + + hash += 12; + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines four values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + + hash += 16; + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines five values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + + hash += 20; + hash = QueueRound(hash, hc5); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines six values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + + hash += 24; + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines seven values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + + hash += 28; + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines eight values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The type of the eighth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The eighth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + uint hc8 = (uint)(value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + + hash += 32; + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + /// The instance to use. + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + /// + /// Adds a span of bytes to the hash code. + /// + /// The span. + public void AddBytes(ReadOnlySpan value) + { + ref byte pos = ref MemoryMarshal.GetReference(value); + ref byte end = ref Unsafe.Add(ref pos, value.Length); + + while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) + { + Add(Unsafe.ReadUnaligned(ref pos)); + pos = ref Unsafe.Add(ref pos, sizeof(int)); + } + + while (Unsafe.IsAddressLessThan(ref pos, ref end)) + { + Add((int)pos); + pos = ref Unsafe.Add(ref pos, 1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = seed + Prime1 + Prime2; + v2 = seed + Prime2; + v3 = seed; + v4 = seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixEmptyState() + { + return seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + + return hash; + } + + private void Add(int value) + { + uint val = (uint)value; + uint previousLength = length++; + uint position = previousLength % 4; + + if (position == 0) + { + queue1 = val; + } + else if (position == 1) + { + queue2 = val; + } + else if (position == 2) + { + queue3 = val; + } + else + { + if (previousLength == 3) + { + Initialize(out v1, out v2, out v3, out v4); + } + + v1 = Round(v1, queue1); + v2 = Round(v2, queue2); + v3 = Round(v3, queue3); + v4 = Round(v4, val); + } + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public readonly int ToHashCode() + { + uint length = this.length; + uint position = length % 4; + uint hash = length < 4 ? MixEmptyState() : MixState(v1, v2, v3, v4); + + hash += length * 4; + + if (position > 0) + { + hash = QueueRound(hash, queue1); + + if (position > 1) + { + hash = QueueRound(hash, queue2); + + if (position > 2) + { + hash = QueueRound(hash, queue3); + } + } + } + + hash = MixFinal(hash); + + return (int)hash; + } + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + throw new NotSupportedException(); + } + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + throw new NotSupportedException(); + } + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs new file mode 100644 index 000000000..8238f6b1f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs @@ -0,0 +1,365 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +/// The type of items to create sequences for. +internal struct ImmutableArrayBuilder : IDisposable +{ + /// + /// The shared instance to share objects. + /// + private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); + + /// + /// The rented instance to use. + /// + private Writer? writer; + + /// + /// Creates a new object. + /// + public ImmutableArrayBuilder() + { + this.writer = SharedObjectPool.Allocate(); + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public readonly ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.WrittenSpan; + } + + /// + /// Gets the number of elements currently written in the current instance. + /// + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.Count; + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the builder while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public readonly Span Advance(int requestedSize) + { + return this.writer!.Advance(requestedSize); + } + + /// + public readonly void Add(T item) + { + this.writer!.Add(item); + } + + /// + /// Adds the specified items to the end of the array. + /// + /// The items to add at the end of the array. + public readonly void AddRange(ReadOnlySpan items) + { + this.writer!.AddRange(items); + } + + /// + public readonly void Clear() + { + this.writer!.Clear(); + } + + /// + /// Inserts an item to the builder at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into the current instance. + public readonly void Insert(int index, T item) + { + this.writer!.Insert(index, item); + } + + /// + /// Gets an instance for the current builder. + /// + /// An instance for the current builder. + /// + /// The builder should not be mutated while an enumerator is in use. + /// + public readonly IEnumerable AsEnumerable() + { + return this.writer!; + } + + /// + public readonly ImmutableArray ToImmutable() + { + T[] array = this.writer!.WrittenSpan.ToArray(); + + return ImmutableCollectionsMarshal.AsImmutableArray(array); + } + + /// + public readonly T[] ToArray() + { + return this.writer!.WrittenSpan.ToArray(); + } + + /// + public override readonly string ToString() + { + return this.writer!.WrittenSpan.ToString(); + } + + /// + public void Dispose() + { + Writer? writer = this.writer; + + this.writer = null; + + if (writer is not null) + { + writer.Clear(); + + SharedObjectPool.Free(writer); + } + } + + /// + /// A class handling the actual buffer writing. + /// + private sealed class Writer : IList, IReadOnlyList + { + /// + /// The underlying array. + /// + private T[] array; + + /// + /// The starting offset within . + /// + private int index; + + /// + /// Creates a new instance with the specified parameters. + /// + public Writer() + { + if (typeof(T) == typeof(char)) + { + this.array = new T[1024]; + } + else + { + this.array = new T[8]; + } + + this.index = 0; + } + + /// + public int Count => this.index; + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.array, 0, this.index); + } + + /// + bool ICollection.IsReadOnly => true; + + /// + T IReadOnlyList.this[int index] => WrittenSpan[index]; + + /// + T IList.this[int index] + { + get => WrittenSpan[index]; + set => throw new NotSupportedException(); + } + + /// + public Span Advance(int requestedSize) + { + EnsureCapacity(requestedSize); + + Span span = this.array.AsSpan(this.index, requestedSize); + + this.index += requestedSize; + + return span; + } + + /// + public void Add(T value) + { + EnsureCapacity(1); + + this.array[this.index++] = value; + } + + /// + public void AddRange(ReadOnlySpan items) + { + EnsureCapacity(items.Length); + + items.CopyTo(this.array.AsSpan(this.index)); + + this.index += items.Length; + } + + /// + public void Clear(ReadOnlySpan items) + { + this.index = 0; + } + + /// + public void Insert(int index, T item) + { + if (index < 0 || index > this.index) + { + ImmutableArrayBuilder.ThrowArgumentOutOfRangeExceptionForIndex(); + } + + EnsureCapacity(1); + + if (index < this.index) + { + Array.Copy(this.array, index, this.array, index + 1, this.index - index); + } + + this.array[index] = item; + this.index++; + } + + /// + /// Clears the items in the current writer. + /// + public void Clear() + { + if (typeof(T) != typeof(byte) && + typeof(T) != typeof(char) && + typeof(T) != typeof(int)) + { + this.array.AsSpan(0, this.index).Clear(); + } + + this.index = 0; + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > this.array.Length - this.index) + { + ResizeBuffer(requestedSize); + } + } + + /// + /// Resizes to ensure it can fit the specified number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + int minimumSize = this.index + sizeHint; + int requestedSize = Math.Max(this.array.Length * 2, minimumSize); + + T[] newArray = new T[requestedSize]; + + Array.Copy(this.array, newArray, this.index); + + this.array = newArray; + } + + /// + int IList.IndexOf(T item) + { + return Array.IndexOf(this.array, item, 0, this.index); + } + + /// + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + /// + bool ICollection.Contains(T item) + { + return Array.IndexOf(this.array, item, 0, this.index) >= 0; + } + + /// + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(this.array, 0, array, arrayIndex, this.index); + } + + /// + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + T?[] array = this.array!; + int length = this.index; + + for (int i = 0; i < length; i++) + { + yield return array[i]!; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + } +} + +/// +/// Private helpers for the type. +/// +file static class ImmutableArrayBuilder +{ + /// + /// Throws an for "index". + /// + public static void ThrowArgumentOutOfRangeExceptionForIndex() + { + throw new ArgumentOutOfRangeException("index"); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs new file mode 100644 index 000000000..b244356d6 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs @@ -0,0 +1,515 @@ +// 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.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +#pragma warning disable IDE0290 + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +internal sealed class IndentedTextWriter : IDisposable +{ + /// + /// The default indentation (4 spaces). + /// + private const string DefaultIndentation = " "; + + /// + /// The default new line ('\n'). + /// + private const char DefaultNewLine = '\n'; + + /// + /// The instance that text will be written to. + /// + private ImmutableArrayBuilder builder; + + /// + /// The current indentation level. + /// + private int currentIndentationLevel; + + /// + /// The current indentation, as text. + /// + private string currentIndentation = ""; + + /// + /// The cached array of available indentations, as text. + /// + private string[] availableIndentations; + + /// + /// Creates a new object. + /// + public IndentedTextWriter() + { + this.builder = new ImmutableArrayBuilder(); + this.currentIndentationLevel = 0; + this.currentIndentation = ""; + this.availableIndentations = new string[4]; + this.availableIndentations[0] = ""; + + for (int i = 1, n = this.availableIndentations.Length; i < n; i++) + { + this.availableIndentations[i] = this.availableIndentations[i - 1] + DefaultIndentation; + } + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the writer while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public Span Advance(int requestedSize) + { + // Add the leading whitespace if needed (same as WriteRawText below) + if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) + { + this.builder.AddRange(this.currentIndentation.AsSpan()); + } + + return this.builder.Advance(requestedSize); + } + + /// + /// Increases the current indentation level. + /// + public void IncreaseIndent() + { + this.currentIndentationLevel++; + + if (this.currentIndentationLevel == this.availableIndentations.Length) + { + Array.Resize(ref this.availableIndentations, this.availableIndentations.Length * 2); + } + + // Set both the current indentation and the current position in the indentations + // array to the expected indentation for the incremented level (i.e. one level more). + this.currentIndentation = this.availableIndentations[this.currentIndentationLevel] + ??= this.availableIndentations[this.currentIndentationLevel - 1] + DefaultIndentation; + } + + /// + /// Decreases the current indentation level. + /// + public void DecreaseIndent() + { + this.currentIndentationLevel--; + this.currentIndentation = this.availableIndentations[this.currentIndentationLevel]; + } + + /// + /// Writes a block to the underlying buffer. + /// + /// A value to close the open block with. + public Block WriteBlock() + { + WriteLine("{"); + IncreaseIndent(); + + return new(this); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(string content, bool isMultiline = false) + { + Write(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(ReadOnlySpan content, bool isMultiline = false) + { + if (isMultiline) + { + while (content.Length > 0) + { + int newLineIndex = content.IndexOf(DefaultNewLine); + + if (newLineIndex < 0) + { + // There are no new lines left, so the content can be written as a single line + WriteRawText(content); + + break; + } + else + { + ReadOnlySpan line = content[..newLineIndex]; + + // Write the current line (if it's empty, we can skip writing the text entirely). + // This ensures that raw multiline string literals with blank lines don't have + // extra whitespace at the start of those lines, which would otherwise happen. + WriteIf(!line.IsEmpty, line); + WriteLine(); + + // Move past the new line character (the result could be an empty span) + content = content[(newLineIndex + 1)..]; + } + } + } + else + { + WriteRawText(content); + } + } + + /// + /// Writes content to the underlying buffer. + /// + /// The interpolated string handler with content to write. + public void Write([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + Write(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes a line to the underlying buffer. + /// + /// Indicates whether to skip adding the line if there already is one. + public void WriteLine(bool skipIfPresent = false) + { + if (skipIfPresent && this.builder.WrittenSpan is [.., '\n' or '{', '\n']) + { + return; + } + + this.builder.Add(DefaultNewLine); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(string content, bool isMultiline = false) + { + WriteLine(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(ReadOnlySpan content, bool isMultiline = false) + { + Write(content, isMultiline); + WriteLine(); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The interpolated string handler with content to write. + public void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + WriteLine(); + } + + /// + /// Writes a line to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// Indicates whether to skip adding the line if there already is one. + public void WriteLineIf(bool condition, bool skipIfPresent = false) + { + if (condition) + { + WriteLine(skipIfPresent); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + WriteLine(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + WriteLine(); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + public void WriteLineIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + if (condition) + { + WriteLine(); + } + } + + /// + public override string ToString() + { + return this.builder.WrittenSpan.Trim().ToString(); + } + + /// + public void Dispose() + { + this.builder.Dispose(); + } + + /// + /// Writes raw text to the underlying buffer, adding leading indentation if needed. + /// + /// The raw text to write. + private void WriteRawText(ReadOnlySpan content) + { + if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) + { + this.builder.AddRange(this.currentIndentation.AsSpan()); + } + + this.builder.AddRange(content); + } + + /// + /// A delegate representing a callback to write data into an instance. + /// + /// The type of data to use. + /// The input data to use to write into . + /// The instance to write into. + public delegate void Callback(T value, IndentedTextWriter writer); + + /// + /// Represents an indented block that needs to be closed. + /// + /// The input instance to wrap. + public struct Block(IndentedTextWriter writer) : IDisposable + { + /// + /// The instance to write to. + /// + private IndentedTextWriter? writer = writer; + + /// + public void Dispose() + { + IndentedTextWriter? writer = this.writer; + + this.writer = null; + + if (writer is not null) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + } + + /// + /// Provides a handler used by the language compiler to append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteInterpolatedStringHandler + { + /// The associated to which to append. + private readonly IndentedTextWriter writer; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) + { + this.writer = writer; + } + + /// Writes the specified string to the handler. + /// The string to write. + public void AppendLiteral(string value) + { + this.writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) + { + AppendFormatted(value); + } + + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) + { + this.writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public void AppendFormatted(T value) + { + if (value is not null) + { + this.writer.Write(value.ToString()); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public void AppendFormatted(T value, string? format) + { + if (value is IFormattable) + { + this.writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); + } + else if (value is not null) + { + this.writer.Write(value.ToString()); + } + } + } + + /// + /// Provides a handler used by the language compiler to conditionally append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteIfInterpolatedStringHandler + { + /// The associated to use. + private readonly WriteInterpolatedStringHandler handler; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// The condition to use to decide whether or not to write content. + /// A value indicating whether formatting should proceed. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) + { + if (condition) + { + this.handler = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); + + shouldAppend = true; + } + else + { + this.handler = default; + + shouldAppend = false; + } + } + + /// + public void AppendLiteral(string value) + { + this.handler.AppendLiteral(value); + } + + /// + public void AppendFormatted(string? value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(ReadOnlySpan value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value, string? format) + { + this.handler.AppendFormatted(value, format); + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..44b103abc --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs @@ -0,0 +1,154 @@ +// 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. + +// Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +#pragma warning disable RS1035 + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// +/// Notes: +/// +/// +/// It is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// +/// It is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// +/// +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: if there is no intent for reusing the object, do not use pool - just use "new". +/// +/// +/// The type of objects to pool. +/// The input factory to produce items. +/// +/// The factory is stored for the lifetime of the pool. We will call this only when pool needs to +/// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()". +/// +/// The pool size to use. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + /// + /// The array of cached items. + /// + private readonly Element[] items = new Element[size - 1]; + + /// + /// Storage for the pool objects. The first item is stored in a dedicated field + /// because we expect to be able to satisfy most requests from it. + /// + private T? firstItem; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input factory to produce items. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Produces a instance. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + T? item = this.firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref this.firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + /// + /// Returns a given instance to the pool. + /// + /// The instance to return. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (this.firstItem is null) + { + this.firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Allocates a new item. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref Element element in this.items.AsSpan()) + { + T? instance = element.Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + } + + return factory(); + } + + /// + /// Frees a given item. + /// + /// The item to return to the pool. + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref Element element in this.items.AsSpan()) + { + if (element.Value is null) + { + element.Value = obj; + + break; + } + } + } + + /// + /// A container for a produced item (using a wrapper to avoid covariance checks). + /// + private struct Element + { + /// + /// The value held at the current element. + /// + internal T? Value; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs new file mode 100644 index 000000000..346403db6 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs @@ -0,0 +1,124 @@ +// 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing an attribute declaration. +/// +/// The type name of the attribute. +/// The values for all constructor arguments for the attribute. +/// The values for all named arguments for the attribute. +internal sealed record AttributeInfo( + string TypeName, + EquatableArray<(string? Name, TypedConstantInfo Value)> ConstructorArgumentInfo, + EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) +{ + /// + /// Creates a new instance from a given syntax node. + /// + /// The symbol for the attribute type. + /// The instance for the current run. + /// The sequence of instances to process. + /// The cancellation token for the current operation. + /// The resulting instance, if available + /// Whether a resulting instance could be created. + public static bool TryCreate( + INamedTypeSymbol typeSymbol, + SemanticModel semanticModel, + IEnumerable arguments, + CancellationToken token, + [NotNullWhen(true)] out AttributeInfo? info) + { + string typeName = typeSymbol.GetFullyQualifiedName(); + + using ImmutableArrayBuilder<(string?, TypedConstantInfo)> constructorArguments = new(); + using ImmutableArrayBuilder<(string, TypedConstantInfo)> namedArguments = new(); + + foreach (AttributeArgumentSyntax argument in arguments) + { + // The attribute expression has to have an available operation to extract information from + if (semanticModel.GetOperation(argument.Expression, token) is not IOperation operation) + { + continue; + } + + // Try to get the info for the current argument + if (!TypedConstantInfo.TryCreate(operation, semanticModel, argument.Expression, token, out TypedConstantInfo? argumentInfo)) + { + info = null; + + return false; + } + + // Try to get the identifier name if the current expression is a named argument expression. If it + // isn't, then the expression is a normal attribute constructor argument, so no extra work is needed. + if (argument.NameEquals is { Name.Identifier.ValueText: string nameEqualsName }) + { + namedArguments.Add((nameEqualsName, argumentInfo)); + } + else if (argument.NameColon is { Name.Identifier.ValueText: string nameColonName }) + { + // This special case also handles named constructor parameters (i.e. '[Test(value: 42)]', not '[Test(Value = 42)]') + constructorArguments.Add((nameColonName, argumentInfo)); + } + else + { + constructorArguments.Add((null, argumentInfo)); + } + } + + info = new AttributeInfo( + typeName, + constructorArguments.ToImmutable(), + namedArguments.ToImmutable()); + + return true; + } + + /// + public override string ToString() + { + // Helper to format constructor parameters + static AttributeArgumentSyntax CreateConstructorArgument(string? name, TypedConstantInfo value) + { + AttributeArgumentSyntax argument = AttributeArgument(ParseExpression(value.ToString())); + + // The name color expression is not guaranteed to be present (in fact, it's more common for it to be missing) + if (name is not null) + { + argument = argument.WithNameColon(NameColon(IdentifierName(name))); + } + + return argument; + } + + // Gather the constructor arguments + IEnumerable arguments = + ConstructorArgumentInfo + .Select(static arg => CreateConstructorArgument(arg.Name, arg.Value)); + + // Gather the named arguments + IEnumerable namedArguments = + NamedArgumentInfo.Select(static arg => + AttributeArgument(ParseExpression(arg.Value.ToString())) + .WithNameEquals(NameEquals(IdentifierName(arg.Name)))); + + // Get the attribute to emit + AttributeSyntax attributeDeclaration = Attribute(IdentifierName(TypeName), AttributeArgumentList(SeparatedList(arguments.Concat(namedArguments)))); + + return attributeDeclaration.NormalizeWhitespace(eol: "\n").ToFullString(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs new file mode 100644 index 000000000..108541a2e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -0,0 +1,101 @@ +// 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.GeneratedDependencyProperty.Constants; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing a default value for a dependency property. +/// +internal abstract partial record DependencyPropertyDefaultValue +{ + /// + /// A type representing a value. + /// + public sealed record Null : DependencyPropertyDefaultValue + { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + + /// + public override string ToString() + { + return "null"; + } + } + + /// + /// A type representing an explicit value. + /// + /// This is used in some scenarios with mismatched metadata types. + public sealed record ExplicitNull : DependencyPropertyDefaultValue + { + /// + /// The shared instance (the type is stateless). + /// + public static ExplicitNull Instance { get; } = new(); + + /// + public override string ToString() + { + return "null"; + } + } + + /// + /// A type representing default value for a specific type. + /// + /// The input type name. + /// Indicates whether the type is projected, meaning WinRT can default initialize it automatically if needed. + public sealed record Default(string TypeName, bool IsProjectedType) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return $"default({TypeName})"; + } + } + + /// + /// A type representing the special unset value. + /// + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + public sealed record UnsetValue(bool UseWindowsUIXaml) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return $"global::{WellKnownTypeNames.DependencyProperty(UseWindowsUIXaml)}.UnsetValue"; + } + } + + /// + /// A type representing a constant value. + /// + /// The constant value. + public sealed record Constant(TypedConstantInfo Value) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return Value.ToString(); + } + } + + /// + /// A type representing a callback. + /// + /// The name of the callback method to invoke. + public sealed record Callback(string MethodName) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return MethodName; + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs new file mode 100644 index 000000000..dea504c67 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -0,0 +1,47 @@ +// 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.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing a generated dependency property. +/// +/// The hierarchy info for the containing type. +/// The property name. +/// The list of additional modifiers for the property (they are values). +/// The accessibility of the property, if available. +/// The accessibility of the accessor, if available. +/// The accessibility of the accessor, if available. +/// The type name for the generated property (without nullability annotations). +/// The type name for the generated property, including nullability annotations. +/// The type name for the metadata declaration of the property, if explicitly set (otherwise, will be used). +/// The default value to set the generated property to. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Indicates whether local caching should be used for the property value. +/// Indicates whether the WinRT-based property changed callback is implemented. +/// Indicates whether the WinRT-based shared property changed callback is implemented. +/// Indicates whether additional types can be generated. +/// Whether to use the UWP XAML or WinUI 3 XAML namespaces. +/// The attributes to emit on the generated static field, if any. +internal sealed record DependencyPropertyInfo( + HierarchyInfo Hierarchy, + string PropertyName, + EquatableArray PropertyModifiers, + Accessibility DeclaredAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, + string TypeName, + string TypeNameWithNullabilityAnnotations, + string? MetadataTypeName, + DependencyPropertyDefaultValue DefaultValue, + bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsLocalCachingEnabled, + bool IsPropertyChangedCallbackImplemented, + bool IsSharedPropertyChangedCallbackImplemented, + bool IsAdditionalTypesGenerationSupported, + bool UseWindowsUIXaml, + EquatableArray StaticFieldAttributes); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs new file mode 100644 index 000000000..5bb928710 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs @@ -0,0 +1,140 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model describing the hierarchy info for a specific type. +/// +/// The fully qualified metadata name for the current type. +/// Gets the namespace for the current type. +/// Gets the sequence of type definitions containing the current type. +internal sealed partial record HierarchyInfo(string FullyQualifiedMetadataName, string Namespace, EquatableArray Hierarchy) +{ + /// + /// Creates a new instance from a given . + /// + /// The input instance to gather info for. + /// A instance describing . + public static HierarchyInfo From(INamedTypeSymbol typeSymbol) + { + using ImmutableArrayBuilder hierarchy = new(); + + for (INamedTypeSymbol? parent = typeSymbol; + parent is not null; + parent = parent.ContainingType) + { + hierarchy.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.IsRecord)); + } + + return new( + typeSymbol.GetFullyQualifiedMetadataName(), + typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + hierarchy.ToImmutable()); + } + + /// + /// Writes syntax for the current hierarchy into a target writer. + /// + /// The type of state to pass to callbacks. + /// The input state to pass to callbacks. + /// The target instance to write text to. + /// A list of base types to add to the generated type, if any. + /// The callbacks to use to write members into the declared type. + public void WriteSyntax( + T state, + IndentedTextWriter writer, + ReadOnlySpan baseTypes, + ReadOnlySpan> memberCallbacks) + { + // Write the generated file header + writer.WriteLine("// "); + writer.WriteLine("#pragma warning disable"); + writer.WriteLine("#nullable enable"); + writer.WriteLine(); + + // Declare the namespace, if needed + if (Namespace.Length > 0) + { + writer.WriteLine($"namespace {Namespace}"); + writer.WriteLine("{"); + writer.IncreaseIndent(); + } + + // Declare all the opening types until the inner-most one + for (int i = Hierarchy.Length - 1; i >= 0; i--) + { + writer.WriteLine($$"""/// """); + writer.Write($$"""partial {{Hierarchy[i].GetTypeKeyword()}} {{Hierarchy[i].QualifiedName}}"""); + + // Add any base types, if needed + if (i == 0 && !baseTypes.IsEmpty) + { + writer.Write(" : "); + writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item)); + writer.WriteLine(); + } + else + { + writer.WriteLine(); + } + + writer.WriteLine($$"""{"""); + writer.IncreaseIndent(); + } + + // Generate all nested members + writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer)); + + // Close all scopes and reduce the indentation + for (int i = 0; i < Hierarchy.Length; i++) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Close the namespace scope as well, if needed + if (Namespace.Length > 0) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + + /// + /// Gets the fully qualified type name for the current instance. + /// + /// The fully qualified type name for the current instance. + public string GetFullyQualifiedTypeName() + { + using ImmutableArrayBuilder fullyQualifiedTypeName = new(); + + fullyQualifiedTypeName.AddRange("global::".AsSpan()); + + if (Namespace.Length > 0) + { + fullyQualifiedTypeName.AddRange(Namespace.AsSpan()); + fullyQualifiedTypeName.Add('.'); + } + + fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan()); + + for (int i = Hierarchy.Length - 2; i >= 0; i--) + { + fullyQualifiedTypeName.Add('.'); + fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan()); + } + + return fullyQualifiedTypeName.ToString(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs new file mode 100644 index 000000000..daa0b27cd --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model describing a type info in a type hierarchy. +/// +/// The qualified name for the type. +/// The type of the type in the hierarchy. +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) +{ + /// + /// Gets the keyword for the current type kind. + /// + /// The keyword for the current type kind. + public string GetTypeKeyword() + { + return Kind switch + { + TypeKind.Struct when IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when IsRecord => "record", + _ => "class" + }; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs new file mode 100644 index 000000000..b3c51677b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -0,0 +1,204 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +partial record TypedConstantInfo +{ + /// + /// Creates a new instance from a given value. + /// + /// The input value. + /// A instance representing . + /// Thrown if the input argument is not valid. + public static TypedConstantInfo Create(TypedConstant arg) + { + if (arg.IsNull) + { + return new Null(); + } + + if (arg.Kind == TypedConstantKind.Array) + { + string elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + ImmutableArray items = arg.Values.Select(Create).ToImmutableArray(); + + return new Array(elementTypeName, items); + } + + return (arg.Kind, arg.Value) switch + { + (TypedConstantKind.Primitive, string text) => new Primitive.String(text), + (TypedConstantKind.Primitive, bool flag) => new Primitive.Boolean(flag), + (TypedConstantKind.Primitive, object value) => value switch + { + byte b => new Primitive.Of(b), + char c => new Primitive.Of(c), + double d => new Primitive.Of(d), + float f => new Primitive.Of(f), + int i => new Primitive.Of(i), + long l => new Primitive.Of(l), + sbyte sb => new Primitive.Of(sb), + short sh => new Primitive.Of(sh), + uint ui => new Primitive.Of(ui), + ulong ul => new Primitive.Of(ul), + ushort ush => new Primitive.Of(ush), + _ => throw new ArgumentException("Invalid primitive type") + }, + (TypedConstantKind.Type, ITypeSymbol type) + => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + (TypedConstantKind.Enum, object value) when arg.Type!.TryGetEnumFieldName(value, out string? fieldName) + => new KnownEnum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), fieldName), + (TypedConstantKind.Enum, object value) + => new Enum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), + _ => throw new ArgumentException("Invalid typed constant type"), + }; + } + + /// + /// Creates a new instance from a given instance. + /// + /// The input instance. + /// A instance representing . + /// Thrown if the input argument is not valid. + /// This method only supports constant values. + public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out TypedConstantInfo? result) + { + // Validate that we do have some constant value + if (operation is not { Type: { } operationType, ConstantValue.HasValue: true }) + { + result = null; + + return false; + } + + if (operation.ConstantValue.Value is null) + { + result = new Null(); + + return true; + } + + // Handle all known possible constant values + result = (operationType, operation.ConstantValue.Value) switch + { + ({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text), + ({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag), + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) when operationType.TryGetEnumFieldName(value, out string? fieldName) + => new KnownEnum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), fieldName), + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) + => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), + (_, byte b) => new Primitive.Of(b), + (_, char c) => new Primitive.Of(c), + (_, double d) => new Primitive.Of(d), + (_, float f) => new Primitive.Of(f), + (_, int i) => new Primitive.Of(i), + (_, long l) => new Primitive.Of(l), + (_, sbyte sb) => new Primitive.Of(sb), + (_, short sh) => new Primitive.Of(sh), + (_, uint ui) => new Primitive.Of(ui), + (_, ulong ul) => new Primitive.Of(ul), + (_, ushort ush) => new Primitive.Of(ush), + (_, ITypeSymbol type) => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + _ => throw new ArgumentException("Invalid typed constant type"), + }; + + return true; + } + + /// + /// Creates a new instance from a given instance. + /// + /// The input instance. + /// The that was used to retrieve . + /// The that was retrieved from. + /// The cancellation token for the current operation. + /// The resulting instance, if available. + /// Whether a resulting instance could be created. + /// Thrown if the input argument is not valid. + public static bool TryCreate( + IOperation operation, + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken token, + [NotNullWhen(true)] out TypedConstantInfo? result) + { + if (TryCreate(operation, out result)) + { + return true; + } + + if (operation is ITypeOfOperation typeOfOperation) + { + result = new Type(typeOfOperation.TypeOperand.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + return true; + } + + if (operation is (IArrayCreationOperation or ICollectionExpressionOperation) and { Type: null or IArrayTypeSymbol }) + { + string? elementTypeName = ((IArrayTypeSymbol?)operation.Type)?.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + // If the element type is not available (since the attribute wasn't checked), just default to object + elementTypeName ??= "object"; + + // Handle all possible ways of initializing arrays in attributes + IEnumerable? arrayElementExpressions = expression switch + { + ImplicitArrayCreationExpressionSyntax { Initializer.Expressions: { } expressions } => expressions, + ArrayCreationExpressionSyntax { Initializer.Expressions: { } expressions } => expressions, + CollectionExpressionSyntax { Elements: { } elements } => elements.OfType().Select(static element => element.Expression), + _ => null + }; + + // No element expressions found, just return an empty array + if (arrayElementExpressions is null) + { + result = new Array(elementTypeName, ImmutableArray.Empty); + + return true; + } + + using ImmutableArrayBuilder items = new(); + + // Enumerate all array elements and extract serialized info for them + foreach (ExpressionSyntax elementExpressions in arrayElementExpressions) + { + if (semanticModel.GetOperation(elementExpressions, token) is not IOperation initializationOperation) + { + goto Failure; + } + + if (!TryCreate(initializationOperation, semanticModel, elementExpressions, token, out TypedConstantInfo? elementInfo)) + { + goto Failure; + } + + items.Add(elementInfo); + } + + result = new Array(elementTypeName, items.ToImmutable()); + + return true; + } + + Failure: + result = null; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs new file mode 100644 index 000000000..d42a2c543 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -0,0 +1,305 @@ +// 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.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing a typed constant item. +/// +internal abstract partial record TypedConstantInfo +{ + /// + /// A type representing a value. + /// + public sealed record Null : TypedConstantInfo + { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + + /// + public override string ToString() + { + return "null"; + } + } + + /// + /// A type representing an array. + /// + /// The type name for array elements. + /// The sequence of contained elements. + public sealed record Array(string ElementTypeName, EquatableArray Items) : TypedConstantInfo + { + /// + public override string ToString() + { + ArrayCreationExpressionSyntax arrayCreationExpressionSyntax = + ArrayCreationExpression( + ArrayType(IdentifierName(ElementTypeName)) + .AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))) + .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression) + .AddExpressions(Items.Select(static c => ParseExpression(c.ToString())).ToArray())); + + return arrayCreationExpressionSyntax.NormalizeWhitespace(eol: "\n").ToFullString(); + } + } + + /// + /// A type representing a primitive value. + /// + public abstract record Primitive : TypedConstantInfo + { + /// + /// A type representing a value. + /// + /// The input value. + public sealed record String(string Value) : TypedConstantInfo + { + /// + /// The shared instance for empty strings. + /// + public static String Empty { get; } = new(""); + + /// + public override string ToString() + { + return '"' + Value + '"'; + } + } + + /// + /// A type representing a value. + /// + /// The input value. + public sealed record Boolean(bool Value) : TypedConstantInfo + { + /// + public override string ToString() + { + return Value ? "true" : "false"; + } + } + + /// + /// A type representing a generic primitive value. + /// + /// The primitive type. + /// The input primitive value. + public sealed record Of(T Value) : TypedConstantInfo + where T : unmanaged, IEquatable + { + /// + /// The cached map of constant fields for the type. + /// + private static readonly FrozenDictionary ConstantFields = GetConstantFields(); + + /// + public override string ToString() + { + static ExpressionSyntax GetExpression(T value) + { + // Try to match named constants first + if (TryGetConstantExpression(value, out ExpressionSyntax? expression)) + { + return expression; + } + + // Special logic for doubles + if (value is double d) + { + // Handle 'double.NaN' explicitly, as 'ToString()' won't work on it at all + if (double.IsNaN(d)) + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(SyntaxKind.DoubleKeyword)), IdentifierName("NaN")); + } + + // Handle 0, to avoid matching against positive/negative zeros + if (d == 0) + { + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal("0.0", 0.0)); + } + + string rawLiteral = d.ToString("R", CultureInfo.InvariantCulture); + + // For doubles, we need to manually format it and always add the trailing "D" suffix. + // This ensures that the correct type is produced if the expression was assigned to + // an object (eg. the literal was used in an attribute object parameter/property). + string literal = rawLiteral.Contains(".") ? rawLiteral : $"{rawLiteral}.0"; + + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(literal, d)); + } + + // Same special handling for floats as well + if (value is float f) + { + // Handle 'float.NaN' as above + if (float.IsNaN(f)) + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(SyntaxKind.FloatKeyword)), IdentifierName("NaN")); + } + + // Handle 0, same as above too + if (f == 0) + { + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal("0.0F", 0.0f)); + } + + string rawLiteral = f.ToString("R", CultureInfo.InvariantCulture); + + // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed. + // However, we still format it manually to ensure we consistently add ".0" as suffix. + string literal = rawLiteral.Contains(".") ? $"{rawLiteral}F" : $"{rawLiteral}.0F"; + + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(literal, f)); + } + + // Handle all other supported types as well + return LiteralExpression(SyntaxKind.NumericLiteralExpression, value switch + { + byte b => Literal(b), + char c => Literal(c), + int i => Literal(i), + long l => Literal(l), + sbyte sb => Literal(sb), + short sh => Literal(sh), + uint ui => Literal(ui), + ulong ul => Literal(ul), + ushort ush => Literal(ush), + _ => throw new ArgumentException("Invalid primitive type") + }); + } + + return GetExpression(Value).NormalizeWhitespace(eol: "\n").ToFullString(); + } + + /// + /// Tries to get a constant expression for a given value. + /// + /// The value to try to get an expression for. + /// The resulting expression, if successfully retrieved. + /// The expression for , if available. + /// Thrown if is not of a supported type. + private static bool TryGetConstantExpression(T value, [NotNullWhen(true)] out ExpressionSyntax? expression) + { + if (ConstantFields.TryGetValue(value, out string? name)) + { + SyntaxKind syntaxKind = value switch + { + byte => SyntaxKind.ByteKeyword, + char => SyntaxKind.CharKeyword, + double => SyntaxKind.DoubleKeyword, + float => SyntaxKind.FloatKeyword, + int => SyntaxKind.IntKeyword, + long => SyntaxKind.LongKeyword, + sbyte => SyntaxKind.SByteKeyword, + short => SyntaxKind.ShortKeyword, + uint => SyntaxKind.UIntKeyword, + ulong => SyntaxKind.ULongKeyword, + ushort => SyntaxKind.UShortKeyword, + _ => throw new ArgumentException("Invalid primitive type") + }; + + expression = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(syntaxKind)), IdentifierName(name)); + + return true; + } + + expression = null; + + return false; + } + + /// + /// Gets a mapping of all well known constant fields for the current type. + /// + /// The mapping of all well known constant fields for the current type. + private static FrozenDictionary GetConstantFields() + { + return typeof(T) + .GetFields() + .Where(static info => info.IsLiteral) + .Where(static info => info.FieldType == typeof(T)) + .Select(static info => (Value: (T)info.GetRawConstantValue(), info.Name)) + .Where(static info => !EqualityComparer.Default.Equals(info.Value, default)) + .ToFrozenDictionary( + keySelector: static info => info.Value, + elementSelector: static info => info.Name); + + + } + } + } + + /// + /// A type representing a type. + /// + /// The input type name. + public sealed record Type(string TypeName) : TypedConstantInfo + { + /// + public override string ToString() + { + return $"typeof({TypeName})"; + } + } + + /// + /// A type representing a known enum value. + /// + /// The enum type name. + /// The enum field name. + public sealed record KnownEnum(string TypeName, string FieldName) : TypedConstantInfo + { + /// + public override string ToString() + { + return $"{TypeName}.{FieldName}"; + } + } + + /// + /// A type representing an enum value. + /// + /// The enum type name. + /// The boxed enum value. + public sealed record Enum(string TypeName, object Value) : TypedConstantInfo + { + /// + public override string 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 (UnaryMinusExpression holding the numeric expression). + ExpressionSyntax valueExpression = ParseExpression(Value.ToString()); + + // If the value is negative, we have to put parentheses around them (to avoid CS0075 errors) + if (valueExpression is PrefixUnaryExpressionSyntax unaryExpression && unaryExpression.IsKind(SyntaxKind.UnaryMinusExpression)) + { + valueExpression = ParenthesizedExpression(valueExpression); + } + + // Now we can safely return the cast expression for the target enum type (with optional parentheses if needed) + return $"({TypeName}){valueExpression.NormalizeWhitespace(eol: "\n").ToFullString()}"; + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj new file mode 100644 index 000000000..94514510f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -0,0 +1,46 @@ + + + net8.0-windows10.0.17763.0 + true + false + + + $(NoWarn);NU1903 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs new file mode 100644 index 000000000..071190a59 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -0,0 +1,96 @@ +// 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; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to test. +internal sealed class CSharpAnalyzerTest : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + private CSharpAnalyzerTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, params DiagnosticResult[] expected) + { + CSharpAnalyzerTest test = new(languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + + test.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(CancellationToken.None); + } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, (string PropertyName, object PropertyValue)[] editorconfig) + { + CSharpAnalyzerTest test = new(languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + + // Add any editorconfig properties, if present + if (editorconfig.Length > 0) + { + test.SolutionTransforms.Add((solution, projectId) => + solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId), + "DependencyPropertyGenerator.editorconfig", + SourceText.From($""" + is_global = true + {string.Join(Environment.NewLine, editorconfig.Select(static p => $"build_property.{p.PropertyName} = {p.PropertyValue}"))} + """, + Encoding.UTF8), + filePath: "/DependencyPropertyGenerator.editorconfig")); + } + + return test.RunAsync(CancellationToken.None); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs new file mode 100644 index 000000000..16ba5a5d5 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs @@ -0,0 +1,53 @@ +// 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.WinUI; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to produce diagnostics. +/// The type of code fix to test. +internal sealed class CSharpCodeFixTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFixer : CodeFixProvider, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + public CSharpCodeFixTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + + ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + TestState.AnalyzerConfigFiles.Add(("/.editorconfig", "[*]\nend_of_line = lf")); + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs new file mode 100644 index 000000000..ca28be27b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -0,0 +1,174 @@ +// 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.IO; +using System.Linq; +using Basic.Reference.Assemblies; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A helper type to run source generator tests. +/// +/// The type of generator to test. +internal static class CSharpGeneratorTest + where TGenerator : IIncrementalGenerator, new() +{ + /// + /// Verifies the resulting diagnostics from a source generator. + /// + /// The input source to process. + /// The expected diagnostics ids to be generated. + public static void VerifyDiagnostics(string source, params string[] diagnosticsIds) + { + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics); + + Dictionary diagnosticMap = diagnostics.DistinctBy(diagnostic => diagnostic.Id).ToDictionary(diagnostic => diagnostic.Id); + + // Check that the diagnostics match + Assert.IsTrue(diagnosticMap.Keys.ToHashSet().SetEquals(diagnosticsIds), $"Diagnostics didn't match. {string.Join(", ", diagnosticMap.Values)}"); + + // If the compilation was supposed to succeed, ensure that no further errors were generated + if (diagnosticsIds.Length == 0) + { + // Compute diagnostics for the final compiled output (just include errors) + List outputCompilationDiagnostics = compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); + + Assert.IsTrue(outputCompilationDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", outputCompilationDiagnostics)}"); + } + } + + /// + /// Verifies the resulting sources produced by a source generator. + /// + /// The input source to process. + /// The expected source to be generated. + /// The language version to use to run the test. + public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); + + // Ensure that no diagnostics were generated + CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); + + // Update the assembly version using the version from the assembly of the input generators. + // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. + string expectedText = result.Source.Replace("", $"\"{typeof(TGenerator).Assembly.GetName().Version}\""); + string actualText = compilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == result.Filename).ToString(); + + Assert.AreEqual(expectedText, actualText); + } + + /// + /// Verifies the incremental generator steps for a given source generator. + /// + /// The input source to process. + /// The updated source to process. + /// The reason for the first "Execute" step. + /// The reason for the "Output" step. + /// The reason for the final output source. + /// The language version to use to run the test. + public static void VerifyIncrementalSteps( + string source, + string updatedSource, + IncrementalStepRunReason executeReason, + IncrementalStepRunReason outputReason, + IncrementalStepRunReason sourceReason, + LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + Compilation compilation = CreateCompilation(source, languageVersion); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new TGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + + // Run the generator on the initial sources + driver = driver.RunGenerators(compilation); + + // Update the compilation by replacing the source + compilation = compilation.ReplaceSyntaxTree( + compilation.SyntaxTrees.First(), + CSharpSyntaxTree.ParseText(updatedSource, CSharpParseOptions.Default.WithLanguageVersion(languageVersion))); + + // Run the generators again on the updated source + driver = driver.RunGenerators(compilation); + + GeneratorRunResult result = driver.GetRunResult().Results.Single(); + + // Get the generated sources and validate them + (object Value, IncrementalStepRunReason Reason)[] sourceOuputs = + result.TrackedOutputSteps + .SelectMany(outputStep => outputStep.Value) + .SelectMany(output => output.Outputs) + .ToArray(); + + Assert.AreEqual(1, sourceOuputs.Length); + Assert.AreEqual(sourceReason, sourceOuputs[0].Reason); + Assert.AreEqual(executeReason, result.TrackedSteps["Execute"].Single().Outputs[0].Reason); + Assert.AreEqual(outputReason, result.TrackedSteps["Output"].Single().Outputs[0].Reason); + } + + /// + /// Creates a compilation from a given source. + /// + /// The input source to process. + /// The language version to use to run the test. + /// The resulting object. + private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + // Get all assembly references for the .NET TFM and ComputeSharp + IEnumerable metadataReferences = + [ + .. Net80.References.All, + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + ]; + + // Parse the source text + SyntaxTree sourceTree = CSharpSyntaxTree.ParseText( + source, + CSharpParseOptions.Default.WithLanguageVersion(languageVersion)); + + // Create the original compilation + return CSharpCompilation.Create( + "original", + [sourceTree], + metadataReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + } + + /// + /// Runs a generator and gathers the output results. + /// + /// The input source to process. + /// + /// + /// The language version to use to run the test. + private static void RunGenerator( + string source, + out Compilation compilation, + out ImmutableArray diagnostics, + LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + Compilation originalCompilation = CreateCompilation(source, languageVersion); + + // Create the generator driver with the specified generator + GeneratorDriver driver = CSharpGeneratorDriver.Create(new TGenerator()).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); + + // Run all source generators on the input source code + _ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs new file mode 100644 index 000000000..bae55d74b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs @@ -0,0 +1,160 @@ +// 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.Linq; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.Foundation; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A custom for testing diagnostic suppressors. +/// +/// The type of the suppressor to test. +// Adapted from https://github.com/ImmediatePlatform/Immediate.Validations +public sealed class CSharpSuppressorTest : CSharpAnalyzerTest + where TSuppressor : DiagnosticSuppressor, new() +{ + /// + /// The list of analyzers to run on the input code. + /// + private readonly List _analyzers = []; + + /// + /// Whether to enable unsafe blocks. + /// + private readonly bool _allowUnsafeBlocks; + + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion _languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The source code to analyze. + /// Whether to enable unsafe blocks. + /// The language version to use to run the test. + public CSharpSuppressorTest( + string source, + bool allowUnsafeBlocks = true, + LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + _allowUnsafeBlocks = allowUnsafeBlocks; + _languageVersion = languageVersion; + + TestCode = source; + TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + } + + /// + protected override IEnumerable GetDiagnosticAnalyzers() + { + return base.GetDiagnosticAnalyzers().Concat(_analyzers); + } + + /// + protected override CompilationOptions CreateCompilationOptions() + { + return new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: _allowUnsafeBlocks); + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(_languageVersion, DocumentationMode.Diagnose); + } + + /// + /// Adds a new analyzer to the set of analyzers to run on the input code. + /// + /// The type of analyzer to activate. + /// The current test instance. + public CSharpSuppressorTest WithAnalyzer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string assemblyQualifiedTypeName) + { + _analyzers.Add((DiagnosticAnalyzer)Activator.CreateInstance(Type.GetType(assemblyQualifiedTypeName)!)!); + + return this; + } + + /// + /// Specifies the diagnostics to enable. + /// + /// The set of diagnostics. + /// The current test instance. + public CSharpSuppressorTest WithSpecificDiagnostics(params DiagnosticResult[] diagnostics) + { + ImmutableDictionary diagnosticOptions = diagnostics.ToImmutableDictionary( + descriptor => descriptor.Id, + descriptor => descriptor.Severity.ToReportDiagnostic()); + + // Transform to enable the diagnostics + Solution EnableDiagnostics(Solution solution, ProjectId projectId) + { + CompilationOptions options = + solution.GetProject(projectId)?.CompilationOptions + ?? throw new InvalidOperationException("Compilation options missing."); + + return solution.WithProjectCompilationOptions( + projectId, + options.WithSpecificDiagnosticOptions(diagnosticOptions)); + } + + SolutionTransforms.Clear(); + SolutionTransforms.Add(EnableDiagnostics); + + return this; + } + + /// + /// Specifies the diagnostics that should be produced. + /// + /// The set of diagnostics. + /// The current test instance. + public CSharpSuppressorTest WithExpectedDiagnosticsResults(params DiagnosticResult[] diagnostics) + { + ExpectedDiagnostics.AddRange(diagnostics); + + return this; + } +} + +/// +/// Extensions for . +/// +file static class DiagnosticSeverityExtensions +{ + /// + /// Converts a value into a one. + /// + public static ReportDiagnostic ToReportDiagnostic(this DiagnosticSeverity severity) + { + return severity switch + { + DiagnosticSeverity.Hidden => ReportDiagnostic.Hidden, + DiagnosticSeverity.Info => ReportDiagnostic.Info, + DiagnosticSeverity.Warning => ReportDiagnostic.Warn, + DiagnosticSeverity.Error => ReportDiagnostic.Error, + _ => throw new InvalidEnumArgumentException(nameof(severity), (int)severity, typeof(DiagnosticSeverity)), + }; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs new file mode 100644 index 000000000..d66660615 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -0,0 +1,3922 @@ +// 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.Threading.Tasks; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_Analyzers +{ + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_ValidAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NotPartial_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0001:GeneratedDependencyProperty|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoSetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoGetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_InitOnlySetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; init; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_Static_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0001:GeneratedDependencyProperty|}] + public static partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ValidAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnUnannotatedPartialPropertyWithImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? Name { get; set; } + + public partial string? Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_GeneratedByToolkit_DoesNotWarn() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string Name { get; set; } + + [GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0002:GeneratedDependencyProperty|}] + public partial string Name { get; set; } + + [GeneratedCode("Some.Other.Generator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0002:GeneratedDependencyProperty|}] + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsRef_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0003:GeneratedDependencyProperty|}] + public partial ref int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsRefReadOnly_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0003:GeneratedDependencyProperty|}] + public partial ref readonly int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsByRefLike_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0004:GeneratedDependencyProperty|}] + public partial Span {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsPointerType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public unsafe partial class MyControl : Control + { + [{|WCTDPG0012:GeneratedDependencyProperty|}] + public partial int* {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsFunctionPointerType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public unsafe partial class MyControl : Control + { + [{|WCTDPG0012:GeneratedDependencyProperty|}] + public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_ValidType1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_ValidType2_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_InvalidType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + + namespace MyApp; + + public partial class MyControl + { + [{|WCTDPG0005:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp10); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp13)] + [DataRow(LanguageVersion.Preview)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_ValidLanguageVersion_DoesNotWarn(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp10)] + [DataRow(LanguageVersion.CSharp12)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_RequiresCSharp13_Warns(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0006:GeneratedDependencyProperty|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp10)] + [DataRow(LanguageVersion.CSharp13)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_CSharp10_RequiresPreview_Warns(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0007:GeneratedDependencyProperty(IsLocalCacheEnabled = true)|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_ValidProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("DependencyPropertyChangedEventArgs")] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_InvalidPropertyType_ValidName_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("DependencyPropertyChangedEventArgs")] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_InvalidPropertyType_NamedProperty_Warns(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0008:GeneratedDependencyProperty|}] + public partial {{propertyType}} {|CS9248:Property|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public class MyControl : Control + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("int?")] + [DataRow("string?")] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableOrNotApplicableType_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithMaybeNullAttribute_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [MaybeNull] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_Required_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public required partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValue_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "Bob")] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|CS9248:Name|} { get; set; } + + private static string GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_WithAttribute_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|CS9248:Name|} { get; set; } + + [return: NotNull] + private static string? GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithMaybeNull_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [MaybeNull] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValueCallback_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + + private static string? GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValueCallback_WithAttribute_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + + [return: MaybeNull] + private static string GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Object_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref object? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_Object_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref object? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_WithNoAttribute_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|WCTDPG0009:{|WCTDPG0024:{|CS9248:Name|}|}|} { get; set; } + + partial void {|CS0759:OnNameGet|}(ref string? propertyValue) + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|WCTDPG0009:{|WCTDPG0024:{|CS9248:Name|}|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithoutNotNull_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_Required_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + [DisallowNull] + public required partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_Required_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public required partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_NonNullDefaultValue_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + [NotNull] + [DisallowNull] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_NonNullDefaultValue_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NullableType_NotRequired_WithNotNull_NullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [NotNull] + public partial KeyFrameCollection? {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}([NotNull] ref KeyFrameCollection? propertyValue) + { + propertyValue = new(); + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_NotRequired_WithAllowNull_NullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [AllowNull] + public partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}([NotNull] ref KeyFrameCollection? propertyValue) + { + propertyValue = new(); + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNullableType_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4 {|CS9248:Value|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("T1?")] + [DataRow("T2?")] + [DataRow("T3?")] + [DataRow("T4")] + [DataRow("T4?")] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NullableType_DoesNotWarn(string declaredType) + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial {{declaredType}} {|CS9248:Value|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_NoAttributeOnGetter_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } + + partial void {|CS0759:OnNameSet|}(ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_NotRequired_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + [DisallowNull] + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_NotRequired_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NullableType_NotRequired_WithNotNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [NotNull] + public partial KeyFrameCollection? {|WCTDPG0025:{|CS9248:KeyFrames|}|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) + { + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_NotRequired_WithAllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [AllowNull] + public partial KeyFrameCollection {|WCTDPG0009:{|WCTDPG0024:{|CS9248:KeyFrames|}|}|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) + { + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_Required_WithAllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [AllowNull] + public required partial KeyFrameCollection {|WCTDPG0024:{|CS9248:KeyFrames|}|} { get; set; } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("T1")] + [DataRow("T2")] + [DataRow("T3")] + [DataRow("T5")] + [DataRow("T6")] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNullableType_Warns(string declaredType) + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : DependencyObject + where T6 : T5 + { + [GeneratedDependencyProperty] + public partial {{declaredType}} {|WCTDPG0009:{|CS9248:Value|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoDefaultValue_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string?")] + [DataRow("int")] + [DataRow("int?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_UnsetValue_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = GeneratedDependencyProperty.UnsetValue)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string")] + [DataRow("int?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_Nullable_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "\"test\"")] + [DataRow("int", "42")] + [DataRow("double", "3.14")] + [DataRow("int?", "42")] + [DataRow("double?", "3.14")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_CompatibleType_DoesNotWarn(string propertyType, string defaultValueType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValueType}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("T1")] + [DataRow("T1?")] + [DataRow("T4?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ExplicitNull_DoesNotWarn(string propertyType) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_NonNullable_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] + public partial int {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("T2")] + [DataRow("T2?")] + [DataRow("T3")] + [DataRow("T3?")] + [DataRow("T4")] + [DataRow("T5")] + [DataRow("T5?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ExplicitNull_Warns(string propertyType) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("where T : class", "null")] + [DataRow("where T : class", "default(T)")] + [DataRow("where T : TOther where TOther : class", "null")] + [DataRow("where T : class where TOther : class", "default(TOther)")] + [DataRow("where T : Delegate", "null")] + [DataRow("where T : Enum", "null")] + [DataRow("where T : DependencyObject", "null")] + [DataRow("where T : DependencyObject", "default(T)")] + [DataRow("where T : TOther where TOther : Delegate", "null")] + [DataRow("where T : TOther where TOther : Enum", "null")] + [DataRow("where T : TOther where TOther : DependencyObject", "null")] + [DataRow("where T : DependencyObject where TOther : class", "default(TOther)")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_DoesNotWarn( + string typeConstraints, + string defaultValue) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValue}})] + public partial T {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("where T : struct")] + [DataRow("where T : unmanaged")] + [DataRow("where T : struct, Enum")] + [DataRow("where T : unmanaged, Enum")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_Warns(string typeConstraints) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] + public partial T {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "42")] + [DataRow("string", "3.14")] + [DataRow("int", "\"test\"")] + [DataRow("int?", "\"test\"")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_IncompatibleType_Warns(string propertyType, string defaultValueType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty({|WCTDPG0011:DefaultValue = {{defaultValueType}}|})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoDefaultValueCallback1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoDefaultValueCallback2_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "Bob")] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NullDefaultValueCallback_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = null)] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("string", "object")] + [DataRow("string", "object?")] + [DataRow("string?", "string")] + [DataRow("string?", "string?")] + [DataRow("int", "int")] + [DataRow("int", "object")] + [DataRow("int", "object?")] + [DataRow("int?", "int")] + [DataRow("int?", "int?")] + [DataRow("int?", "object")] + [DataRow("int?", "object?")] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_ValidDefaultValueCallback_DoesNotWarn(string propertyType, string returnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultValue))] + public partial {{propertyType}} {|CS9248:Value|} { get; set; } + + private static {{returnType}} GetDefaultValue() => default!; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_BothDefaultValuePropertiesSet_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0013:GeneratedDependencyProperty(DefaultValue = "Bob", DefaultValueCallback = nameof(GetDefaultName))|}] + public partial string? {|CS9248:Name|} { get; set; } + + private static string? GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_MethodNotFound_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty({|WCTDPG0014:DefaultValueCallback = "MissingMethod"|})] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_InvalidMethod_ExplicitlyImplemented_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control, IGetDefaultValue + { + [GeneratedDependencyProperty({|WCTDPG0014:DefaultValueCallback = "GetDefaultValue"|})] + public partial string? {|CS9248:Name|} { get; set; } + + static string? IGetDefaultValue.GetDefaultValue() => "Bob"; + } + + public interface IGetDefaultValue + { + static abstract string? GetDefaultValue(); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private string? GetDefaultName()")] + [DataRow("private static string? GetDefaultName(int x)")] + [DataRow("private static int GetDefaultName()")] + [DataRow("private static int GetDefaultName(int x)")] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_InvalidMethod_Warns(string methodSignature) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty({|WCTDPG0015:DefaultValueCallback = "GetDefaultName"|})] + public partial string? {|CS9248:Name|} { get; set; } + + {{methodSignature}} => default!; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("Name")] + [DataRow("TestProperty")] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_NoAttribute_DoesNotWarn(string propertyName) + { + string source = $$""" + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:{{propertyName}}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("Name")] + [DataRow("PropertyGroup")] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_ValidName_DoesNotWarn(string propertyName) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:{{propertyName}}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_InvalidName_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDPG0016:GeneratedDependencyProperty|}] + public partial string? {|CS9248:TestProperty|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldNotInitialized_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty; + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldWithDifferentName_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty OtherNameProperty = DependencyProperty.Register( + {|WCTDPG0027:name: "Name"|}, + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(OtherNameProperty); + set => SetValue(OtherNameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_WCTDPG0026_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty {|WCTDPG0026:NameField|} = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameField); + set => SetValue(NameField, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("null", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_NoAdditionalDiagnostic_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: {{name}}, + propertyType: {{propertyType}}, + ownerType: {{ownerType}}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int", "float")] + [DataRow("float", "double")] + [DataRow("int", "object")] + [DataRow("int?", "object")] + [DataRow("string", "object")] + [DataRow("MyControl", "IDisposable")] + [DataRow("MyControl", "Control")] + [DataRow("MyControl", "DependencyObject")] + [DataRow("Control", "DependencyObject")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_DoesNotWarn( + string dependencyPropertyType, + string propertyType) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + {|WCTDPG0030:propertyType: typeof({{dependencyPropertyType}})|}, + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithObjectInitialization_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( + nameof(Margin), + {|WCTDPG0030:typeof(double)|}, + typeof(MyControl), + new PropertyMetadata({|WCTDPG0032:new Thickness(0)|})); + + private Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithExplicitDefaultValueNull_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( + nameof(Margin), + {|WCTDPG0030:typeof(double)|}, + typeof(MyControl), + new PropertyMetadata({|WCTDPG0031:null|})); + + private Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Regression test for a case found in https://github.com/jenius-apps/ambie + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithInvalidPropertyName_DoesNotWarn() + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace AmbientSounds.Controls; + + public sealed partial class PlayerControl : UserControl + { + public static readonly DependencyProperty PlayVisibleProperty = DependencyProperty.Register( + {|WCTDPG0027:nameof(PlayButtonVisible)|}, + {|WCTDPG0030:typeof(bool)|}, + typeof(PlayerControl), + new PropertyMetadata({|WCTDPG0032:Visibility.Visible|})); + + public static readonly DependencyProperty VolumeVisibleProperty = DependencyProperty.Register( + nameof(VolumeVisible), + {|WCTDPG0030:typeof(bool)|}, + typeof(PlayerControl), + new PropertyMetadata({|WCTDPG0032:Visibility.Visible|})); + + public Visibility PlayButtonVisible + { + get => (Visibility)GetValue(PlayVisibleProperty); + set => SetValue(PlayVisibleProperty, value); + } + + public Visibility VolumeVisible + { + get => (Visibility)GetValue(VolumeVisibleProperty); + set => SetValue(VolumeVisibleProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + [DataRow("default(float)")] + [DataRow("1.0F")] + [DataRow("(float)1.0")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithMismatchedNullableUnderlyingType_DoesNotWarn(string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public static readonly DependencyProperty Value1Property = DependencyProperty.Register( + nameof(Value1), + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public static readonly DependencyProperty Value2Property = DependencyProperty.Register( + nameof(Value2), + {|WCTDPG0030:typeof(int?)|}, + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public static readonly DependencyProperty Value3Property = DependencyProperty.Register( + "Value3", + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public int? {|WCTDPG0017:Value1|} + { + get => (int?)GetValue(Value1Property); + set => SetValue(Value1Property, value); + } + + public float Value2 + { + get => (float)GetValue(Value2Property); + set => SetValue(Value2Property, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Same as above, but with property changed callbacks too + [TestMethod] + [DataRow("default(float)")] + [DataRow("1.0F")] + [DataRow("(float)1.0")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithMismatchedNullableUnderlyingType_WithCallbacks_DoesNotWarn(string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public static readonly DependencyProperty Value1Property = DependencyProperty.Register( + nameof(Value1), + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public static readonly DependencyProperty Value2Property = DependencyProperty.Register( + nameof(Value2), + {|WCTDPG0030:typeof(int?)|}, + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public static readonly DependencyProperty Value3Property = DependencyProperty.Register( + "Value3", + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public int? Value1 + { + get => (int?)GetValue(Value1Property); + set => SetValue(Value1Property, value); + } + + public float Value2 + { + get => (float)GetValue(Value2Property); + set => SetValue(Value2Property, value); + } + + private static void ItemSourcePropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0029_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: {{name}}, + propertyType: {{propertyType}}, + {|WCTDPG0029:ownerType: {{ownerType}}|}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0027_WCTDPG0028_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDPG0027:{|WCTDPG0028:name: {{name}}|}|}, + propertyType: {{propertyType}}, + ownerType: {{ownerType}}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingGetter_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingSetter_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("global::System.Numerics.Matrix3x2?", "global::System.Numerics.Matrix3x2?", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4?", "global::System.Numerics.Matrix4x4?", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane?", "global::System.Numerics.Plane?", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion?", "global::System.Numerics.Quaternion?", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2?", "global::System.Numerics.Vector2?", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3?", "global::System.Numerics.Vector3?", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4?", "global::System.Numerics.Vector4?", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point?", "global::Windows.Foundation.Point?", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect?", "global::Windows.Foundation.Rect?", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size?", "global::Windows.Foundation.Size?", "default(global::Windows.Foundation.Size)")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Using 'default(T)' is not a constant (therefore it's not allowed as attribute argument), even if constrained to an enum type. + // Some of these combinations (eg. 'object' property with 'T1?' backing type) are also just flat out invalid and would error out. + [TestMethod] + [DataRow("T1?", "T1?", "new PropertyMetadata(default(T1))")] + [DataRow("object", "T1?", "new PropertyMetadata(default(T1))")] + [DataRow("T2?", "T2?", "new PropertyMetadata(default(T2))")] + [DataRow("object", "T2?", "new PropertyMetadata(default(T2))")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Same as above, but this one also produces some WCTDPG0030 warnings, so we need to split these cases out + [TestMethod] + [DataRow("T1", "object", "new PropertyMetadata(default(T1))")] + [DataRow("T1?", "object", "new PropertyMetadata(default(T1))")] + [DataRow("T2", "object", "new PropertyMetadata(default(T2))")] + [DataRow("T2?", "object", "new PropertyMetadata(default(T2))")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_WithMismatchedType_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + {|WCTDPG0030:propertyType: typeof({{dependencyPropertyType}})|}, + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithInvalidAttribute_DoesNotWarn() + { + const string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [property: Test] + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("object", "object")] + [DataRow("object", "object?")] + [DataRow("int", "int")] + [DataRow("int?", "int?")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")] + [DataRow("global::System.Guid?", "global::System.Guid?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?" )] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns( + string dependencyPropertyType, + string propertyType) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} {|WCTDPG0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("[Test]")] + [DataRow("[field: Test]")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithAttributeOnField_Warns(string attributeDeclaration) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + {{attributeDeclaration}} + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? {|WCTDPG0017:Name|} + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "string", "null")] + [DataRow("string", "string", "default(string)")] + [DataRow("string", "string", "(string)null")] + [DataRow("string", "string", "\"\"")] + [DataRow("string", "string", "\"Hello\"")] + [DataRow("string", "string?", "null")] + [DataRow("object", "object", "null")] + [DataRow("object", "object?", "null")] + [DataRow("int", "int", "0")] + [DataRow("int", "int", "42")] + [DataRow("int", "int", "default(int)")] + [DataRow("int?", "int?", "null")] + [DataRow("int?", "int?", "0")] + [DataRow("int?", "int?", "42")] + [DataRow("int?", "int?", "default(int)")] + [DataRow("int?", "int?", "default(int?)")] + [DataRow("int?", "int?", "null")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility?)")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility?)")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")] + [DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "default(global::System.Collections.Generic.KeyValuePair?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.A")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.B")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum)")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] + [DataRow("object", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.A")] + [DataRow("object", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.B")] + [DataRow("object", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum)")] + [DataRow("object", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_Warns( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} {|WCTDPG0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Using the declared property type as first argument here for clarity when reading all combinations + [TestMethod] + [DataRow("T1?", "T1?", "new PropertyMetadata(default(T1?))")] + [DataRow("T1?", "object", "new PropertyMetadata(default(T1?))")] + [DataRow("T1?", "T1?", "new PropertyMetadata(null)")] + [DataRow("T1?", "object", "new PropertyMetadata(null)")] + [DataRow("T1?", "T1?", "null")] + [DataRow("T1?", "object", "null")] + [DataRow("T2?", "T2?", "new PropertyMetadata(default(T2?))")] + [DataRow("T2?", "object", "new PropertyMetadata(default(T2?))")] + [DataRow("T2?", "T2?", "new PropertyMetadata(null)")] + [DataRow("T2?", "object", "new PropertyMetadata(null)")] + [DataRow("T2?", "T2?", "null")] + [DataRow("T2?", "object", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_Warns( + string propertyType, + string dependencyPropertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} {|WCTDPG0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string?", "object")] + [DataRow("MyControl", "DependencyObject")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitMetadataType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{declaredType}} {|WCTDPG0017:Name|} + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_NoPropertySuffixOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty {|WCTDPG0026:NameField|} = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidPropertyNameOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDPG0027:name: "Text"|}, + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidOwningTypeOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + {|WCTDPG0029:ownerType: typeof(MyOtherObject)|}, + typeMetadata: null); + } + + public class MyOtherObject : DependencyObject; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "null")] + [DataRow("string", "\"Bob\"")] + [DataRow("object", "null")] + [DataRow("object", "\"Bob\"")] + [DataRow("object", "42")] + [DataRow("int?", "null")] + [DataRow("Visibility?", "null")] + [DataRow("string", "DependencyProperty.UnsetValue")] + [DataRow("object", "DependencyProperty.UnsetValue")] + [DataRow("int", "DependencyProperty.UnsetValue")] + [DataRow("int?", "DependencyProperty.UnsetValue")] + [DataRow("Visibility", "DependencyProperty.UnsetValue")] + [DataRow("Visibility?", "DependencyProperty.UnsetValue")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_WithExplicitDefaultValue_DoesNotWarn( + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("global::System.TimeSpan")] + [DataRow("global::Windows.Foundation.Rect")] + [DataRow("Visibility")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_NullDefaultValue_Warns(string propertyType) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int", "3.0")] + [DataRow("int", "3.0F")] + [DataRow("int", "3L")] + [DataRow("int", "\"Bob\"")] + [DataRow("int", "Visibility.Visible")] + [DataRow("int", "default(Visibility)")] + [DataRow("bool", "Visibility.Visible")] + [DataRow("string", "42")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidDefaultValue_Warns(string propertyType, string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // This is an explicit test to ensure upcasts are correctly detected and allowed + [TestMethod] + [DataRow("IDisposable", "MyClass")] + [DataRow("object", "string")] + [DataRow("object", "int")] + [DataRow("object", "int?")] + [DataRow("Control", "MyControl")] + [DataRow("DependencyObject", "MyControl")] + [DataRow("DependencyObject", "Control")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithUpcast_Warns( + string dependencyPropertyType, + string propertyType) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} {|WCTDPG0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyClass : IDisposable + { + public void Dispose() + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + [DataRow("where T : class", "null")] + [DataRow("where T : class", "default(T)")] + [DataRow("where T : TOther where TOther : class", "null")] + [DataRow("where T : class where TOther : class", "default(TOther)")] + [DataRow("where T : Delegate", "null")] + [DataRow("where T : Enum", "null")] + [DataRow("where T : DependencyObject", "null")] + [DataRow("where T : DependencyObject", "default(T)")] + [DataRow("where T : TOther where TOther : Delegate", "null")] + [DataRow("where T : TOther where TOther : Enum", "null")] + [DataRow("where T : TOther where TOther : DependencyObject", "null")] + [DataRow("where T : DependencyObject where TOther : class", "default(TOther)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_Warns( + string typeConstraints, + string defaultValue) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(T), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}})); + + public T {|WCTDPG0017:Name|} + { + get => (T?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithTargetTypedPropertyMetadataNew_Warns() + { + const string source = """ + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty VisibleAreaProperty = DependencyProperty.Register( + nameof(VisibleArea), + typeof(Rect), + typeof(MyObject), + new(default(Rect))); + + public Rect {|WCTDPG0017:VisibleArea|} + { + get => (Rect)GetValue(VisibleAreaProperty); + private set => SetValue(VisibleAreaProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("where T : struct")] + [DataRow("where T : unmanaged")] + [DataRow("where T : struct, Enum")] + [DataRow("where T : unmanaged, Enum")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_WCTDPG0031_DoesNotWarn(string typeConstraints) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(T), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); + + public T {|WCTDPG0017:Name|} + { + get => (T)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private static readonly")] + [DataRow("public static")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_DoesNotWarn(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + {{fieldDeclaration}} DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private static readonly")] + [DataRow("public static")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_EmitsAdditionalDiagnosticsToo_DoesNotWarn(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + {{fieldDeclaration}} DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDPG0027:{|WCTDPG0028:name: "Name2"|}|}, + {|WCTDPG0030:propertyType: typeof(int?)|}, + ownerType: typeof(MyObject), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoForwardedAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_ValidForwardedAttribute_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_TypoInAttributeName_NotTargetingStatic_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [Testt] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, + [ + // /0/Test0.cs(9,6): error CS0246: The type or namespace name 'Testt' could not be found (are you missing a using directive or an assembly reference?) + DiagnosticResult.CompilerError("CS0246").WithSpan(9, 6, 9, 11).WithArguments("Testt"), + + // /0/Test0.cs(9,6): error CS0246: The type or namespace name 'TesttAttribute' could not be found (are you missing a using directive or an assembly reference?) + DiagnosticResult.CompilerError("CS0246").WithSpan(9, 6, 9, 11).WithArguments("TesttAttribute") + ]); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_MissingUsingDirective_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp + { + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDPG0018:Test|}] + public string? Name { get; set; } + } + } + + namespace MyAttributes + { + public class TestAttribute : Attribute; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_TypoInAttributeName_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDPG0018:Testt|}] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/683 + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidExpressionOnFieldAttribute_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDPG0019:Test(TestAttribute.M)|}] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute + { + public static string M => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidExpressionOnFieldAttribute_WithExistingParameter_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDPG0019:Test(TestAttribute.M)|}] + public string? Name { get; set; } + } + + public class TestAttribute(string P) : Attribute + { + public static string M => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationCorrectlyAnalyzer_NotDependencyProperty_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + private static string TestProperty = "Blah"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationCorrectlyAnalyzer_ValidField_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty = DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task UseFieldDeclarationCorrectlyAnalyzer_Warns(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + #nullable enable + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} {|WCTDPG0020:TestProperty|}; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_NotDependencyProperty_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static string TestProperty => "Blah"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_ExplicitInterfaceImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyObject + { + static DependencyProperty IMyObject.TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + + public interface IMyObject + { + static abstract DependencyProperty TestProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_ImplicitInterfaceImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyObject + { + public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + + public interface IMyObject + { + static abstract DependencyProperty TestProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_WinRTComponent_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, editorconfig: [("CsWinRTComponent", true)]); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_UnsupportedModifier_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public abstract class MyObject : MyBase + { + public DependencyProperty Test1Property => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); + public virtual DependencyProperty Test2Property => DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); + public abstract DependencyProperty Test3Property { get; } + public override DependencyProperty BaseProperty => DependencyProperty.Register("Base", typeof(string), typeof(MyObject), null); + } + + public abstract class MyBase : DependencyObject + { + public abstract DependencyProperty BaseProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_WithNoGetter_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty TestProperty + { + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_NormalProperty_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty {|WCTDPG0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); + public static DependencyProperty {|WCTDPG0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); + public static DependencyProperty {|WCTDPG0021:Test3Property|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task ExplicitPropertyMetadataTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "object")] + [DataRow("MyObject", "DependencyObject")] + [DataRow("MyObject", "IMyInterface")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_ValidExplicitType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyInterface + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public {{declaredType}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("string")] + [DataRow("MyObject")] + [DataRow("DependencyObject")] + [DataRow("IMyInterface")] + [DataRow("double?")] + [DataRow("double")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_SameType_Warns(string type) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyInterface + { + [GeneratedDependencyProperty({|WCTDPG0022:PropertyType = typeof({{type}})|})] + public {{type}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object", "string")] + [DataRow("DependencyObject", "MyObject")] + [DataRow("MyObject", "IMyInterface")] + [DataRow("double", "double?")] + [DataRow("double?", "IMyInterface")] + [DataRow("double", "float")] + [DataRow("float", "double")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_IncompatibleType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [GeneratedDependencyProperty({|WCTDPG0023:PropertyType = typeof({{propertyType}})|})] + public {{declaredType}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs new file mode 100644 index 000000000..1baa9750d --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs @@ -0,0 +1,48 @@ +// 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.Reflection; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +partial class Test_DependencyPropertyGenerator +{ + [TestMethod] + [DataRow("GeneratedDependencyProperty")] + [DataRow("GeneratedDependencyPropertyAttribute")] + public void SingleProperty_String_WithNoCaching_PostInitializationSources(string typeName) + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? Name { get; set; } + } + """; + + string fileName = $"{typeName}.g.cs"; + string sourceText; + + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)!) + using (StreamReader reader = new(stream)) + { + sourceText = reader.ReadToEnd(); + } + + string updatedSourceText = sourceText + .Replace("", "CommunityToolkit.WinUI.DependencyPropertyGenerator") + .Replace("", typeof(DependencyPropertyGenerator).Assembly.GetName().Version!.ToString()); + + CSharpGeneratorTest.VerifySources(source, (fileName, updatedSourceText), languageVersion: LanguageVersion.CSharp13); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs new file mode 100644 index 000000000..c1be55365 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -0,0 +1,5812 @@ +// 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.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public partial class Test_DependencyPropertyGenerator +{ + [TestMethod] + public void SingleProperty_Int32_WithLocalCache() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithLocalCache_WithCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + return new(Instance.OnNumberPropertyChanged); + } + + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithLocalCache_WithDefaultValue() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true, DefaultValue = 42)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } = 42; + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_UnsetValue() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = GeneratedDependencyProperty.UnsetValue)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(global::Windows.UI.Xaml.DependencyProperty.UnsetValue)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + return new(Instance.OnNumberPropertyChanged); + } + + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValue() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValue_WithCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: 42, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + return new(Instance.OnNumberPropertyChanged); + } + + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithSharedCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + return new(Instance.OnPropertyChanged); + } + + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithBothCallbacks() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + return new(Instance.OnNumberPropertyChanged); + } + + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithLocalCache() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial string? Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get => field; + set + { + OnNameSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + string? __oldValue = field; + + OnNameChanging(value); + OnNameChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + OnNameChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging(string? newValue); + + /// 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.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_Required() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public required partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public required partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_New() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public class BaseControl : DependencyObject + { + public new string Name { get; set; } + } + + public partial class MyControl : BaseControl + { + [GeneratedDependencyProperty] + public new partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public new partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_Virtual() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public virtual partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public virtual partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("override")] + [DataRow("sealed override")] + public void SingleProperty_String_WithNoCaching_Override(string modifiers) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public class BaseControl : DependencyObject + { + public virtual string Name { get; set; } + } + + public partial class MyControl : BaseControl + { + [GeneratedDependencyProperty] + public {{modifiers}} partial string Name { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public {{modifiers}} partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_CustomAccessibility() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + internal partial string Name { protected get; private set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial string Name + { + protected get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + private set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_CorrectSpacing() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithJustOnePropertyCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithSharedPropertyCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// Shared instance, for all properties only using the shared callback. + private static readonly PropertyChangedCallback SharedPropertyChangedCallback = new(Instance.OnPropertyChanged); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return SharedPropertyChangedCallback; + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return SharedPropertyChangedCallback; + } + + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return new(Instance.OnPropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks2() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnLastNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return new(Instance.OnLastNamePropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + + /// + private void OnLastNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnLastNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnLastNamePropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnLastNamePropertyChanged")] + public static extern void OnLastNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("int")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateNumber))] + public partial int Number { get; set; } + + private static {{returnType}} CreateNumber() => 42; + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("int?")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_NullableOfInt32_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateNumber))] + public partial int? Number { get; set; } + + private static {{returnType}} CreateNumber() => 42; + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int?), + ownerType: typeof(MyControl), + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int? __unboxedValue = (int?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string")] + [DataRow("string?")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_String_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateName))] + public partial string? Name { get; set; } + + private static {{returnType}} CreateName() => "Bob"; + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + + // The 'string' type is special + [DataRow("string", "string", "object", "null")] + [DataRow("string", "string?", "object?", "null")] + + // Well known WinRT primitive types + [DataRow("int", "int", "object", "null")] + [DataRow("byte", "byte", "object", "null")] + [DataRow("sbyte", "sbyte", "object", "null")] + [DataRow("short", "short", "object", "null")] + [DataRow("ushort", "ushort", "object", "null")] + [DataRow("uint", "uint", "object", "null")] + [DataRow("long", "long", "object", "null")] + [DataRow("ulong", "ulong", "object", "null")] + [DataRow("char", "char", "object", "null")] + [DataRow("float", "float", "object", "null")] + [DataRow("double", "double", "object", "null")] + + // Well known WinRT struct types + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "object", "null")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "object", "null")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "object", "null")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "object", "null")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "object", "null")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "object", "null")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "object", "null")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "object", "null")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "object", "null")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "object", "null")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "object", "null")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "object", "null")] + + // Well known WinRT enum types + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "object", "null")] + + // Nullable types, they're always just 'null' + [DataRow("int?", "int?", "object?", "null")] + [DataRow("byte?", "byte?", "object?", "null")] + [DataRow("char?", "char?", "object?", "null")] + [DataRow("long?", "long?", "object?", "null")] + [DataRow("float?", "float?", "object?", "null")] + [DataRow("double?", "double?", "object?", "null")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "object?", "null")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "object?", "null")] + [DataRow("global::System.Guid?", "global::System.Guid?", "object?", "null")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "object?", "null")] + + // Custom struct types + [DataRow("global::MyNamespace.MyStruct", "global::MyNamespace.MyStruct", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyStruct))", "public struct MyStruct { public int X; }")] + [DataRow("global::MyNamespace.MyStruct", "global::MyNamespace.MyStruct", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyStruct))", "public struct MyStruct { public string X { get; set; } }")] + + // Custom enum types + [DataRow("global::MyNamespace.MyEnum", "global::MyNamespace.MyEnum", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyEnum))", "public enum MyEnum { A, B, C }")] + public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( + string dependencyPropertyType, + string propertyType, + string defaultValueDefinition, + string propertyMetadataExpression, + string? typeDefinition = "") + { + string source = $$""" + using System; + using System.Collections.Generic; + using CommunityToolkit.WinUI; + using Windows.Foundation; + using Windows.Foundation.Numerics; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + {{typeDefinition}} + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial {{propertyType}} Name { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{propertyType}} Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + {{propertyType}} __unboxedValue = ({{propertyType}})__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref {{defaultValueDefinition}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{defaultValueDefinition}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged({{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("A", "global::MyNamespace.AAttribute()")] + [DataRow("B(42, 10)", "global::MyNamespace.BAttribute(42, 10)")] + [DataRow("B(X: 42, Y: 10)", "global::MyNamespace.BAttribute(X: 42, Y: 10)")] + [DataRow("B(Y: 42, X: 10)", "global::MyNamespace.BAttribute(Y: 42, X: 10)")] + [DataRow("B(42, Y: 10)", "global::MyNamespace.BAttribute(42, Y: 10)")] + [DataRow("""C(10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(10, X = "Test", Y = 42)""")] + [DataRow("""C(Z: 10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(Z: 10, X = "Test", Y = 42)""")] + [DataRow("D(Foo.B, typeof(string), new[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("D(Foo.B, typeof(string), new int[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("D(Foo.B, typeof(string), [1, 2, 3])", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("""E(42, Y: 10, Z: "Bob", W = 100)""", """global::MyNamespace.EAttribute(42, Y: 10, Z: "Bob", W = 100)""")] + public void SingleProperty_String_WithNoCaching_WithForwardedAttribute( + string attributeDefinition, + string attributeForwarding) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + [static: {{attributeDefinition}}] + public partial string? Name { get; set; } + } + + public class AAttribute : Attribute; + public class BAttribute(int X, int Y) : Attribute; + public class CAttribute(int Z) : Attribute + { + public string X { get; set; } + public int Y { get; set; } + } + public class DAttribute(Foo X, Type Y, int[] Z) : Attribute; + public class EAttribute(int X, int Y, string Z) : Attribute + { + public int W { get; set; } + } + public enum Foo { A, B } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [{{attributeForwarding}}] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Bool_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial bool IsSelected { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof(bool), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial bool IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + bool __unboxedValue = (bool)__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref bool propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref bool propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged(bool newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("bool", "bool", "null", "bool", "object", "null")] + [DataRow("bool", "bool", "bool", "bool", "object", "null")] + [DataRow("bool", "bool", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(bool))")] + [DataRow("bool?", "bool?", "null", "bool?", "object?", "null")] + [DataRow("bool?", "bool?", "bool?", "bool?", "object?", "null")] + [DataRow("bool?", "bool?", "object", "object", "object?", "null")] + [DataRow("bool?", "bool?", "bool", "bool", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(null)")] + [DataRow("string?", "string?", "null", "string", "object?", "null")] + [DataRow("string?", "string?", "string", "string", "object?", "null")] + [DataRow("string?", "string?", "object", "object", "object?", "null")] + [DataRow("Visibility", "global::Windows.UI.Xaml.Visibility", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::Windows.UI.Xaml.Visibility))")] + [DataRow("Visibility?", "global::Windows.UI.Xaml.Visibility?", "object", "object", "object?", "null")] + public void SingleProperty_WithCustomMetadataType_WithNoCaching( + string declaredType, + string generatedDeclaredType, + string propertyType, + string generatedPropertyType, + string boxedType, + string propertyMetadata) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} IsSelected { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof({{generatedPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadata}}); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{generatedDeclaredType}} IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + {{generatedDeclaredType}} __unboxedValue = ({{generatedDeclaredType}})__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{generatedDeclaredType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{generatedDeclaredType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged({{generatedDeclaredType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("T1", "null", "T1", "object", "null")] + [DataRow("T1", "T1", "T1", "object", "null")] + [DataRow("T1", "object", "object", "object", "null")] + [DataRow("T1?", "null", "T1", "object?", "null")] + [DataRow("T1?", "T1", "T1", "object?", "null")] + [DataRow("T1?", "object", "object", "object?", "null")] + [DataRow("T2", "null", "T2", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2", "T2", "T2", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "null", "T2", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "T2", "T2", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "object", "object", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T3", "null", "T3", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T3", "T3", "T3", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T3", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T4", "null", "T4", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4", "T4", "T4", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4?", "null", "T4?", "object?", "null")] + [DataRow("T4?", "T4?", "T4?", "object?", "null")] + [DataRow("T4?", "object", "object", "object?", "null")] + public void SingleProperty_GenericType_WithCustomMetadataType_WithNoCaching( + string declaredType, + string propertyType, + string generatedPropertyType, + string boxedType, + string propertyMetadata) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} IsSelected { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof({{generatedPropertyType}}), + ownerType: typeof(MyObject), + typeMetadata: {{propertyMetadata}}); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{declaredType}} IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + {{declaredType}} __unboxedValue = ({{declaredType}})__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged({{declaredType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => ((MyObject)d).OnNumberPropertyChanged(e))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithSharedCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => ((MyObject)d).OnPropertyChanged(e))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithBothCallbacks() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => + { + MyObject __this = (MyObject)d; + + __this.OnNumberPropertyChanged(e); + __this.OnPropertyChanged(e); + })); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T1_ReferenceType_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T1? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T1), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T1? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T1? __unboxedValue = (T1?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T1? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T1? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T1? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T4_ValueType_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4 Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T4), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T4))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T4 Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T4 __unboxedValue = (T4)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T4 propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T4 propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T4 newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T4_ValueType_Nullable_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T4?), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T4? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T4? __unboxedValue = (T4?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T4? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T4? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T4? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T2_Unconstrained_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T2? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T2), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T2))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T2? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T2? __unboxedValue = (T2?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T2? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T2? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T2? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T2_Unconstrained_WithInterface_WithNoCaching() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T2 : IDisposable + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T2? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T2), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T2))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T2? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T2? __unboxedValue = (T2?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T2? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T2? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T2? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs new file mode 100644 index 000000000..eebe7591c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs @@ -0,0 +1,132 @@ +// 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.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_DependencyPropertyGenerator_Incrementality +{ + [TestMethod] + public void ModifiedOptions_ModifiesOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Modified, + outputReason: IncrementalStepRunReason.Modified, + sourceReason: IncrementalStepRunReason.Modified); + } + + [TestMethod] + public void AddedLeadingTrivia_DoesNotModifyOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + /// + /// This is some property. + /// + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Unchanged, + outputReason: IncrementalStepRunReason.Cached, + sourceReason: IncrementalStepRunReason.Cached); + } + + [TestMethod] + public void AddedOtherMember_DoesNotModifyOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + public void Foo() + { + } + + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Unchanged, + outputReason: IncrementalStepRunReason.Cached, + sourceReason: IncrementalStepRunReason.Cached); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs new file mode 100644 index 000000000..f52ab2a27 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs @@ -0,0 +1,84 @@ +// 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.Threading.Tasks; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_DiagnosticSuppressors +{ + private static readonly DiagnosticResult CS0658 = DiagnosticResult.CompilerWarning("CS0658"); + + [TestMethod] + [DataRow("get")] + [DataRow("with")] + [DataRow("readonly")] + [DataRow("propdp")] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_OtherTarget_NotSuppressed(string target) + { + await new CSharpSuppressorTest( + $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + [{{target}}: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } + + [TestMethod] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_NoTriggerAttribute_NotSuppressed() + { + await new CSharpSuppressorTest( + """ + using System; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } + + [TestMethod] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_ValidUse_Suppressed() + { + await new CSharpSuppressorTest( + """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs new file mode 100644 index 000000000..fc7960744 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs @@ -0,0 +1,198 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCodeFixer>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseFieldDeclarationCodeFixer +{ + [TestMethod] + [DataRow("private static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static new DependencyProperty", "public new static readonly DependencyProperty")] + public async Task SingleProperty(string propertyDeclaration, string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{propertyDeclaration}} [|TestProperty|] { get; } = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + string @fixed = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("private static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static new DependencyProperty", "public new static readonly DependencyProperty")] + public async Task SingleProperty_WithExpressionBody(string propertyDeclaration, string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{propertyDeclaration}} [|TestProperty|] => DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + string @fixed = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithInitializers() + { + const string original = """ + using System; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static DependencyProperty [|Test1Property|] => DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test2Property|] => DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test3Property|] { get; } = DependencyProperty.Register( + "Test3", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test4Property|] { get; } + + [Test] + public static DependencyProperty [|Test5Property|] { get; } + } + + public class TestAttribute : Attribute; + """; + + const string @fixed = """ + using System; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test2Property = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test3Property = DependencyProperty.Register( + "Test3", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test4Property; + + [Test] + public static DependencyProperty Test5Property { get; } + } + + public class TestAttribute : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + test.FixedState.ExpectedDiagnostics.AddRange( + [ + // /0/Test0.cs(29,38): warning WCTDPG0021: The property 'MyApp.MyObject.Test5Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + CSharpCodeFixVerifier.Diagnostic().WithSpan(29, 38, 29, 51).WithArguments("MyApp.MyObject.Test5Property") + ]); + + await test.RunAsync(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs new file mode 100644 index 000000000..d31d3ad70 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs @@ -0,0 +1,238 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyCodeFixer>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseFieldDeclarationCorrectlyCodeFixer +{ + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task SingleField(string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} [|TestProperty|]; + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task SingleField_WithInitializer(string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} [|TestProperty|] = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleFields() + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static readonly DependencyProperty [|Test1Property|]; + public static DependencyProperty [|Test2Property|]; + public static readonly DependencyProperty Test3Property; + public static volatile DependencyProperty [|Test4Property|]; + public static readonly DependencyProperty? [|Test5Property|]; + public static readonly DependencyProperty Test6Property; + public static readonly DependencyProperty Test7Property; + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property; + public static readonly DependencyProperty Test2Property; + public static readonly DependencyProperty Test3Property; + public static readonly DependencyProperty Test4Property; + public static readonly DependencyProperty Test5Property; + public static readonly DependencyProperty Test6Property; + public static readonly DependencyProperty Test7Property; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleFields_WithInitializers() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static readonly DependencyProperty [|Test1Property|] = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + internal static volatile DependencyProperty [|Test2Property|] = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test3Property|]; + public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null); + public static volatile DependencyProperty [|Test5Property|]; + public static readonly DependencyProperty? [|Test6Property|] = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property = DependencyProperty.Register( + "Test8", + typeof(string), + typeof(MyObject), + null); + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test2Property = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test3Property; + public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test5Property; + public static readonly DependencyProperty Test6Property = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property = DependencyProperty.Register( + "Test8", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..33d9474fc --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,2808 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer +{ + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("object", "object")] + [DataRow("object", "object?")] + [DataRow("int", "int")] + [DataRow("byte", "byte")] + [DataRow("sbyte", "sbyte")] + [DataRow("short", "short")] + [DataRow("ushort", "ushort")] + [DataRow("uint", "uint")] + [DataRow("long", "long")] + [DataRow("ulong", "ulong")] + [DataRow("char", "char")] + [DataRow("float", "float")] + [DataRow("double", "double")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility")] + [DataRow("int?", "int?")] + [DataRow("byte?", "byte?")] + [DataRow("char?", "char?")] + [DataRow("long?", "long?")] + [DataRow("float?", "float?")] + [DataRow("double?", "double?")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] + [DataRow("global::System.Guid?", "global::System.Guid?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass?")] + public async Task SimpleProperty(string dependencyPropertyType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + // These are custom value types, on properties where the metadata was set to 'null'. In this case, the + // default value would just be 'null', as XAML can't default initialize them. To preserve behavior, + // we must include an explicit default value. This will warn when the code is recompiled, but that + // is expected, because this specific scenario was (1) niche, and (2) kinda busted already anyway. + [TestMethod] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + public async Task SimpleProperty_ExplicitNull(string dependencyPropertyType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string", "string", "null")] + [DataRow("string", "string", "default(string)")] + [DataRow("string", "string", "(string)null")] + [DataRow("string", "string?", "null")] + [DataRow("object", "object", "null")] + [DataRow("object", "object?", "null")] + [DataRow("int", "int", "0")] + [DataRow("int", "int", "default(int)")] + [DataRow("int?", "int?", "null")] + [DataRow("int?", "int?", "default(int?)")] + [DataRow("int?", "int?", "null")] + [DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "null")] + [DataRow("global::System.Guid?", "global::System.Guid?", "null")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] + public async Task SimpleProperty_WithExplicitValue_DefaultValue( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string", "string", "\"\"")] + [DataRow("string", "string", "\"Hello\"")] + [DataRow("int", "int", "42")] + [DataRow("int?", "int?", "0")] + [DataRow("int?", "int?", "42")] + [DataRow("Visibility", "Visibility", "Visibility.Collapsed")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)5")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)(-5)")] + public async Task SimpleProperty_WithExplicitValue_NotDefault( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValueExpression}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_EmptyString() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(string.Empty)); + + public string [|Name|] + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NestedEnumType() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(MyContainingType.MyEnum), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(MyContainingType.MyEnum.B)); + + public MyContainingType.MyEnum [|Name|] + { + get => (MyContainingType.MyEnum)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = MyContainingType.MyEnum.B)] + public partial MyContainingType.MyEnum {|CS9248:Name|} { get; set; } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NestedEnumType_WithUsingStatic() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + using static MyApp.MyContainingType; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(MyEnum), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(MyEnum.B)); + + public MyEnum [|Name|] + { + get => (MyEnum)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + using static MyApp.MyContainingType; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = MyEnum.B)] + public partial MyEnum {|CS9248:Name|} { get; set; } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(Windows.UI.Xaml.Automation.AnnotationType), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(Windows.UI.Xaml.Automation.AnnotationType.TrackChanges)); + + public Windows.UI.Xaml.Automation.AnnotationType [|Name|] + { + get => (Windows.UI.Xaml.Automation.AnnotationType)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Automation; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = AnnotationType.TrackChanges)] + public partial Windows.UI.Xaml.Automation.AnnotationType {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("int", "MyContainer1.X")] + [DataRow("float", "MyContainer1.Y")] + [DataRow("string", "MyContainer1.S")] + [DataRow("MyContainer1.MyContainer2.NestedEnum", "MyContainer1.E1")] + [DataRow("MyContainer1.MyEnum1", "MyContainer1.E2")] + [DataRow("MyEnum3", "MyContainer1.E3")] + [DataRow("int", "MyContainer1.MyContainer2.X")] + [DataRow("float", "MyContainer1.MyContainer2.Y")] + [DataRow("string", "MyContainer1.MyContainer2.S")] + [DataRow("MyContainer1.MyContainer2.NestedEnum", "MyContainer1.MyContainer2.E1")] + [DataRow("MyContainer1.MyEnum1", "MyContainer1.MyContainer2.E2")] + [DataRow("MyEnum3", "MyContainer1.MyContainer2.E3")] + public async Task SimpleProperty_WithExplicitValue_NamedConstant(string propertyType, string defaultValue) + { + const string types = """ + public class MyContainer1 + { + public const int X = 42; + public const float Y = 3.14f; + public const string S = "Test"; + public const MyContainer2.NestedEnum E1 = MyContainer2.NestedEnum.B; + public const MyEnum1 E2 = MyEnum1.B; + public const MyEnum3 E3 = MyEnum3.B; + + public enum MyEnum1 { A, B }; + + public struct MyContainer2 + { + public const int X = 42; + public const float Y = 3.14f; + public const string S = "Test"; + public const NestedEnum E1 = NestedEnum.B; + public const MyEnum1 E2 = MyEnum1.B; + public const MyEnum3 E3 = MyEnum3.B; + + public enum NestedEnum { A, B }; + } + } + + public enum MyEnum3 { A, B } + """; + + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + {{types}} + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValue}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + + {{types}} + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("[A]", "[static: A]")] + [DataRow("""[Test(42, "Hello")]""", """[static: Test(42, "Hello")]""")] + [DataRow("""[field: Test(42, "Hello")]""", """[static: Test(42, "Hello")]""")] + [DataRow("""[A, Test(42, "Hello")]""", """[static: A, Test(42, "Hello")]""")] + [DataRow(""" + [A] + [Test(42, "Hello")] + """, """ + [static: A] + [static: Test(42, "Hello")] + """)] + public async Task SimpleProperty_WithForwardedAttributes( + string attributeDefinition, + string attributeForwarding) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + {{attributeDefinition}} + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class AAttribute : Attribute; + public class TestAttribute(int X, string Y) : Attribute; + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + {{attributeForwarding}} + public partial string? {|CS9248:Name|} { get; set; } + } + + public class AAttribute : Attribute; + public class TestAttribute(int X, string Y) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? [|Name1|] + { + get => (string?)GetValue(Name1Property); + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name1|} { get; set; } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithXmlDocs_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + public static readonly DependencyProperty TargetObjectProperty = DependencyProperty.Register( + nameof(TargetObject), + typeof(TElement?), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( + nameof(Value), + typeof(TValue?), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public TValue? [|Value|] + { + get => (TValue?)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + /// + /// Blah. + /// + public TElement? [|TargetObject|] + { + get => (TElement?)GetValue(TargetObjectProperty); + set => SetValue(TargetObjectProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + [GeneratedDependencyProperty(DefaultValue = null)] + public partial TValue? {|CS9248:Value|} { get; set; } + + /// + /// Blah. + /// + [GeneratedDependencyProperty(DefaultValue = null)] + public partial TElement? {|CS9248:TargetObject|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithInterspersedMembers_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// This is another member + public int Blah => 42; + + public string? [|Name1|] + { + get => (string?)GetValue(Name1Property); + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + /// This is another member + public int Blah => 42; + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name1|} { get; set; } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithLeadingPersistentMembers_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithLeadingPersistentMembers_WithXmlDocs_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + /// Blah + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + /// Blah + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + /// Blah + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + /// Blah + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("float", "0.0F", "1.0F", "0.123F")] + [DataRow("double", "0.0", "4.0", "0.123")] + public async Task MultipleProperties_HandlesWellKnownLiterals(string propertyType, string zeroExpression, string literalExpression, string decimalLiteralExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty P1Property = DependencyProperty.Register( + name: "P1", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{zeroExpression}})); + + public static readonly DependencyProperty P2Property = DependencyProperty.Register( + name: "P2", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.MinValue)); + + public static readonly DependencyProperty P3Property = DependencyProperty.Register( + name: "P3", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.NaN)); + + public static readonly DependencyProperty P4Property = DependencyProperty.Register( + name: "P4", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.Pi)); + + public static readonly DependencyProperty P5Property = DependencyProperty.Register( + name: "P5", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{literalExpression}})); + + public static readonly DependencyProperty P6Property = DependencyProperty.Register( + name: "P6", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{decimalLiteralExpression}})); + + public {{propertyType}} [|P1|] + { + get => ({{propertyType}})GetValue(P1Property); + set => SetValue(P1Property, value); + } + + public {{propertyType}} [|P2|] + { + get => ({{propertyType}})GetValue(P2Property); + set => SetValue(P2Property, value); + } + + public {{propertyType}} [|P3|] + { + get => ({{propertyType}})GetValue(P3Property); + set => SetValue(P3Property, value); + } + + public {{propertyType}} [|P4|] + { + get => ({{propertyType}})GetValue(P4Property); + set => SetValue(P4Property, value); + } + + public {{propertyType}} [|P5|] + { + get => ({{propertyType}})GetValue(P5Property); + set => SetValue(P5Property, value); + } + + public {{propertyType}} [|P6|] + { + get => ({{propertyType}})GetValue(P6Property); + set => SetValue(P6Property, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:P1|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.MinValue)] + public partial {{propertyType}} {|CS9248:P2|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.NaN)] + public partial {{propertyType}} {|CS9248:P3|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.Pi)] + public partial {{propertyType}} {|CS9248:P4|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{literalExpression}})] + public partial {{propertyType}} {|CS9248:P5|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{decimalLiteralExpression}})] + public partial {{propertyType}} {|CS9248:P6|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithXmlDocs_WithForwardedAttributes_TrimsAttributTrivia() + { + const string original = """ + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ExpressionProperty = DependencyProperty.Register( + nameof(Expression), + typeof(string), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + [Test(42, "Test")] + public static readonly DependencyProperty InputProperty = DependencyProperty.Register( + nameof(Input), + typeof(object), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public string? [|Expression|] + { + get => (string?)GetValue(ExpressionProperty); + set => SetValue(ExpressionProperty, value); + } + + /// + /// Blah. + /// + public object? [|Input|] + { + get => (object?)GetValue(InputProperty); + set => SetValue(InputProperty, value); + } + } + + public class TestAttribute(int X, string Y) : Attribute; + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + [GeneratedDependencyProperty] + public partial string? {|CS9248:Expression|} { get; set; } + + /// + /// Blah. + /// + [GeneratedDependencyProperty] + [static: Test(42, "Test")] + public partial object? {|CS9248:Input|} { get; set; } + } + + public class TestAttribute(int X, string Y) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithInterspersedNonFixableProprty_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DisableAnimationProperty = DependencyProperty.Register( + nameof(DisableAnimation), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(float), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsHorizontalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsHorizontalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsVerticalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsVerticalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetScrollViewerProperty = DependencyProperty.Register( + nameof(TargetScrollViewer), + typeof(ScrollViewer), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(float), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + public bool [|DisableAnimation|] + { + get => (bool)GetValue(DisableAnimationProperty); + set => SetValue(DisableAnimationProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double HorizontalOffset + { + get => (double)(float)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + public bool [|IsHorizontalOffsetRelative|] + { + get => (bool)GetValue(IsHorizontalOffsetRelativeProperty); + set => SetValue(IsHorizontalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + public bool [|IsVerticalOffsetRelative|] + { + get => (bool)GetValue(IsVerticalOffsetRelativeProperty); + set => SetValue(IsVerticalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets the target . + /// + public ScrollViewer? [|TargetScrollViewer|] + { + get => (ScrollViewer?)GetValue(TargetScrollViewerProperty); + set => SetValue(TargetScrollViewerProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double VerticalOffset + { + get => (double)(float)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(float), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(float), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:DisableAnimation|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double HorizontalOffset + { + get => (double)(float)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsHorizontalOffsetRelative|} { get; set; } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsVerticalOffsetRelative|} { get; set; } + + /// + /// Gets or sets the target . + /// + [GeneratedDependencyProperty] + public partial ScrollViewer? {|CS9248:TargetScrollViewer|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double VerticalOffset + { + get => (double)(float)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + // Using 'object' for dependency properties is sometimes needed to work around an 'IReference' issue in some binding scenarios + [TestMethod] + public async Task MultipleProperties_WithWorkaroundPropertiesForReflectionBindings_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DisableAnimationProperty = DependencyProperty.Register( + nameof(DisableAnimation), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsHorizontalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsHorizontalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsVerticalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsVerticalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetScrollViewerProperty = DependencyProperty.Register( + nameof(TargetScrollViewer), + typeof(ScrollViewer), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + public bool [|DisableAnimation|] + { + get => (bool)GetValue(DisableAnimationProperty); + set => SetValue(DisableAnimationProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double? [|HorizontalOffset|] + { + get => (double?)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + public bool [|IsHorizontalOffsetRelative|] + { + get => (bool)GetValue(IsHorizontalOffsetRelativeProperty); + set => SetValue(IsHorizontalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + public bool [|IsVerticalOffsetRelative|] + { + get => (bool)GetValue(IsVerticalOffsetRelativeProperty); + set => SetValue(IsVerticalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets the target . + /// + public ScrollViewer? [|TargetScrollViewer|] + { + get => (ScrollViewer?)GetValue(TargetScrollViewerProperty); + set => SetValue(TargetScrollViewerProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double? [|VerticalOffset|] + { + get => (double?)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:DisableAnimation|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial double? {|CS9248:HorizontalOffset|} { get; set; } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsHorizontalOffsetRelative|} { get; set; } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsVerticalOffsetRelative|} { get; set; } + + /// + /// Gets or sets the target . + /// + [GeneratedDependencyProperty] + public partial ScrollViewer? {|CS9248:TargetScrollViewer|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial double? {|CS9248:VerticalOffset|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_NestedType_AddsAllRequiredPartialModifiers() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: null); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_ExplicitNullCallbackArgument_IsHandledCorrectly1() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: new PropertyMetadata(null, null)); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_ExplicitNullCallbackArgument_IsHandledCorrectly2() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: new PropertyMetadata("", null)); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = "")] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "object")] + [DataRow("MyObject", "DependencyObject")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task SimpleProperty_ExplicitMetadataType_IsHandledCorrectly(string declaredType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: null); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "object", "\"\"")] + [DataRow("double?", "object", "1.0")] + [DataRow("double?", "double", "1.0")] + public async Task SimpleProperty_ExplicitMetadataType_WithDefaultValue_IsHandledCorrectly( + string declaredType, + string propertyType, + string defaultValue) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}}, null)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}), DefaultValue = {{defaultValue}})] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithinGenericType_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty P1Property = DependencyProperty.Register( + nameof(P1), + typeof(T1), + typeof(MyObject), + new PropertyMetadata(default(T1))); + + public static readonly DependencyProperty P2Property = DependencyProperty.Register( + nameof(P2), + typeof(T1), + typeof(MyObject), + null); + + public static readonly DependencyProperty P3Property = DependencyProperty.Register( + nameof(P3), + typeof(T2), + typeof(MyObject), + new PropertyMetadata(default(T2))); + + public static readonly DependencyProperty P4Property = DependencyProperty.Register( + nameof(P4), + typeof(T2), + typeof(MyObject), + null); + + public static readonly DependencyProperty P5Property = DependencyProperty.Register( + nameof(P5), + typeof(T3), + typeof(MyObject), + new PropertyMetadata(default(T3))); + + public static readonly DependencyProperty P6Property = DependencyProperty.Register( + nameof(P6), + typeof(T3), + typeof(MyObject), + null); + + public static readonly DependencyProperty P7Property = DependencyProperty.Register( + nameof(P7), + typeof(T4), + typeof(MyObject), + new PropertyMetadata(default(T4))); + + public static readonly DependencyProperty P8Property = DependencyProperty.Register( + nameof(P8), + typeof(T4), + typeof(MyObject), + null); + + public static readonly DependencyProperty P9Property = DependencyProperty.Register( + nameof(P9), + typeof(T4?), + typeof(MyObject), + new PropertyMetadata(default(T4?))); + + public static readonly DependencyProperty P10Property = DependencyProperty.Register( + nameof(P10), + typeof(T4?), + typeof(MyObject), + null); + + public static readonly DependencyProperty P11Property = DependencyProperty.Register( + nameof(P11), + typeof(T5), + typeof(MyObject), + new PropertyMetadata(default(T5))); + + public static readonly DependencyProperty P12Property = DependencyProperty.Register( + nameof(P12), + typeof(T5), + typeof(MyObject), + null); + + // Constrained to 'class', with default value matching 'null' (redundant) + public T1? [|P1|] + { + get => (T1?)GetValue(P1Property); + set => SetValue(P1Property, value); + } + + // Constrained to 'class', no default value + public T1? [|P2|] + { + get => (T1?)GetValue(P2Property); + set => SetValue(P2Property, value); + } + + // Unconstrained, with explicit default value + public T2? [|P3|] + { + get => (T2?)GetValue(P3Property); + set => SetValue(P3Property, value); + } + + // Unconstrained, with no metadata + public T2? [|P4|] + { + get => (T2?)GetValue(P4Property); + set => SetValue(P4Property, value); + } + + // Unconstrained, with explicit default value + public T3? [|P5|] + { + get => (T3?)GetValue(P5Property); + set => SetValue(P5Property, value); + } + + // Unconstrained, with no metadata + public T3? [|P6|] + { + get => (T3?)GetValue(P6Property); + set => SetValue(P6Property, value); + } + + // Constrained to value type, with explicit default value + public T4 [|P7|] + { + get => (T4)GetValue(P7Property); + set => SetValue(P7Property, value); + } + + // Constrained to value type, with no metadata + public T4 [|P8|] + { + get => (T4)GetValue(P8Property); + set => SetValue(P8Property, value); + } + + // Constrained to value type, nullable, with explicit default value + public T4? [|P9|] + { + get => (T4?)GetValue(P9Property); + set => SetValue(P9Property, value); + } + + // Constrained to value type, nullable, with no metadata + public T4? [|P10|] + { + get => (T4?)GetValue(P10Property); + set => SetValue(P10Property, value); + } + + // Constrained to just interface, with explicit default value + public T5? [|P11|] + { + get => (T5?)GetValue(P11Property); + set => SetValue(P11Property, value); + } + + // Constrained to just interface, with no metadata + public T5? [|P12|] + { + get => (T5?)GetValue(P12Property); + set => SetValue(P12Property, value); + } + } + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + // Constrained to 'class', with default value matching 'null' (redundant) + [GeneratedDependencyProperty] + public partial T1? {|CS9248:P1|} { get; set; } + + // Constrained to 'class', no default value + [GeneratedDependencyProperty] + public partial T1? {|CS9248:P2|} { get; set; } + + // Unconstrained, with explicit default value + [GeneratedDependencyProperty] + public partial T2? {|CS9248:P3|} { get; set; } + + // Unconstrained, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T2? {|CS9248:P4|} { get; set; } + + // Unconstrained, with explicit default value + [GeneratedDependencyProperty] + public partial T3? {|CS9248:P5|} { get; set; } + + // Unconstrained, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T3? {|CS9248:P6|} { get; set; } + + // Constrained to value type, with explicit default value + [GeneratedDependencyProperty] + public partial T4 {|CS9248:P7|} { get; set; } + + // Constrained to value type, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T4 {|CS9248:P8|} { get; set; } + + // Constrained to value type, nullable, with explicit default value + [GeneratedDependencyProperty] + public partial T4? {|CS9248:P9|} { get; set; } + + // Constrained to value type, nullable, with no metadata + [GeneratedDependencyProperty] + public partial T4? {|CS9248:P10|} { get; set; } + + // Constrained to just interface, with explicit default value + [GeneratedDependencyProperty] + public partial T5? {|CS9248:P11|} { get; set; } + + // Constrained to just interface, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T5? {|CS9248:P12|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "string", "")] + [DataRow("int", "int", "")] + [DataRow("int?", "int?", "")] + [DataRow("T1?", "T1", "")] + [DataRow("T2", "T2", "(DefaultValue = null)")] + [DataRow("T2?", "T2", "(DefaultValue = null)")] + [DataRow("T4", "T4", "(DefaultValue = null)")] + [DataRow("T4?", "T4?", "")] + [DataRow("T5", "T5", "(DefaultValue = null)")] + [DataRow("T5?", "T5", "(DefaultValue = null)")] + public async Task SimpleProperty_WithinGenericType_WithNullMetadata( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + null); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "string", "")] + [DataRow("int?", "int?", "")] + [DataRow("T1?", "T1", "")] + [DataRow("T4?", "T4?", "")] + public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + new PropertyMetadata(null)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("T2", "T2", "(DefaultValue = null)")] + [DataRow("T2?", "T2", "(DefaultValue = null)")] + [DataRow("T4", "T4", "(DefaultValue = null)")] + [DataRow("T5", "T5", "(DefaultValue = null)")] + [DataRow("T5?", "T5", "(DefaultValue = null)")] + public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue_FixesButAlsoWarnsInOriginalCode( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0031:null|})); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + // Hit this false negative in the Microsoft Store + [TestMethod] + public async Task SingleProperty_WithinGenericType_WithExplicitDefaultValue_TypeParameterToObject() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract class KeyFrame : DependencyObject + where TKeyFrame : unmanaged + { + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( + nameof(Value), + typeof(object), + typeof(KeyFrame), + new PropertyMetadata(default(TValue?))); + + public TValue? [|Value|] + { + get => (TValue?)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class KeyFrame : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial TValue? {|CS9248:Value|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + // Some mixed scenarios with enum types, nullable types, and different property metadata types. + // Not all of these are a 1:1 match with the code fixer (some aren't even fully valid) just yet. + [TestMethod] + [DataRow("int?", "int?", "null", "")] + [DataRow("int?", "int?", "new PropertyMetadata(null)", "")] + [DataRow("int?", "int?", "new PropertyMetadata(default(int?))", "")] + [DataRow("int?", "int?", "new PropertyMetadata(0)", "(DefaultValue = 0)")] + [DataRow("int?", "int?", "new PropertyMetadata(default(int))", "(DefaultValue = 0)")] + [DataRow("int?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(default(int?))", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(0)", "(PropertyType = typeof(object), DefaultValue = 0)")] + [DataRow("int?", "object", "new PropertyMetadata(default(int))", "(PropertyType = typeof(object), DefaultValue = 0)")] + [DataRow("Visibility", "Visibility", "null", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(default(Visibility))", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Visible)", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] + [DataRow("Visibility", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] + [DataRow("Visibility", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object))")] + [DataRow("Visibility", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] + [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] + [DataRow("Visibility?", "Visibility?", "null", "")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(null)", "")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(default(Visibility))", "(DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Visible)", "(DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] + [DataRow("Visibility?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("Visibility?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + [DataRow("Visibility?", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] + [DataRow("MyEnum", "MyEnum", "null", "(DefaultValue = null)")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.A)", "")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] + [DataRow("MyEnum", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("MyEnum", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("MyEnum", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object))")] + [DataRow("MyEnum", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object))")] + [DataRow("MyEnum", "object", "new PropertyMetadata(MyEnum.B)", "(PropertyType = typeof(object), DefaultValue = MyEnum.B)")] + [DataRow("MyEnum?", "MyEnum?", "null", "")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(null)", "")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(default(MyEnum))", "(DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.A)", "(DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] + [DataRow("MyEnum?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.B)", "(PropertyType = typeof(object), DefaultValue = MyEnum.B)")] + public async Task SimpleProperty_MixedEnumAndNullable_WithPropertyType_HandlesAllScenariosCorrectly( + string declaredType, + string propertyType, + string propertyMetadataExpression, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: {{propertyMetadataExpression}}); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("Visibility", "Visibility", "(DefaultValue = null)")] + [DataRow("MyEnum", "MyEnum", "(DefaultValue = null)")] + public async Task SimpleProperty_MixedEnumAndNullable_WithPropertyType_WithInvalidDefaultValueNull_HandlesAllScenariosCorrectly( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("int", "int", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("int?", "int?", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("int?", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("string", "string", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("string", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility", "Visibility", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility?", "Visibility?", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility?", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + public async Task SimpleProperty_WithDefaultValue_UnsetValue( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata(DependencyProperty.UnsetValue)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task SimpleProperty_WithTargetTypedPropertyMetadataNew() + { + const string original = """ + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty VisibleAreaProperty = DependencyProperty.Register( + nameof(VisibleArea), + typeof(Rect), + typeof(MyObject), + new(default(Rect))); + + public Rect {|WCTDPG0017:VisibleArea|} + { + get => (Rect)GetValue(VisibleAreaProperty); + private set => SetValue(VisibleAreaProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial Rect {|CS9248:VisibleArea|} { get; private set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } +} diff --git a/components/DependencyPropertyGenerator/OpenSolution.bat b/components/DependencyPropertyGenerator/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/DependencyPropertyGenerator/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/samples/Assets/icon.png b/components/DependencyPropertyGenerator/samples/Assets/icon.png new file mode 100644 index 000000000..8435bcaa9 Binary files /dev/null and b/components/DependencyPropertyGenerator/samples/Assets/icon.png differ diff --git a/components/DependencyPropertyGenerator/samples/Dependencies.props b/components/DependencyPropertyGenerator/samples/Dependencies.props new file mode 100644 index 000000000..0b0c230ab --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/Dependencies.props @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj new file mode 100644 index 000000000..c7af9907c --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj @@ -0,0 +1,10 @@ + + + + + DependencyPropertyGenerator + + + + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj new file mode 100644 index 000000000..de4d74d91 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -0,0 +1,58 @@ + + + + + DependencyPropertyGenerator + This package contains DependencyPropertyGenerator. + + + CommunityToolkit.WinUI.DependencyPropertyGeneratorRns + false + false + + + false + false + + + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; + + + + + + + + + + + + + System.Runtime.CompilerServices.IsExternalInit; + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets new file mode 100644 index 000000000..00a35e6a2 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -0,0 +1,86 @@ + + + + + true + true + false + + + + + false + false + + + + + + + + + + + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_ATTRIBUTE_EMBEDDED_MODE + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_EMBEDDED_MODE + + + + + + + + + + false + true + true + + + + + + + + + + + + + + + + + + @(CommunityToolkitGeneratedDependencyPropertyCurrentCompilerAssemblyIdentity->'%(Version)') + + + true + + + + + + + diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs new file mode 100644 index 000000000..0db11876a --- /dev/null +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs @@ -0,0 +1,24 @@ +// 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 WINAPPSDK +using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; +#else +using DependencyProperty = Windows.UI.Xaml.DependencyProperty; +#endif + +namespace CommunityToolkit.WinUI; + +/// +/// Provides constant values that can be used as default values for . +/// +public sealed class GeneratedDependencyProperty +{ + /// + /// + /// This constant is only meant to be used in assignments to (because + /// cannot be used in that context, as it is not a constant, but rather a static field). Using this constant in other scenarios is undefined behavior. + /// + public const object UnsetValue = null!; +} diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs new file mode 100644 index 000000000..2291e55ed --- /dev/null +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,101 @@ +// 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; +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +#if WINDOWS_UWP || HAS_UNO +using DependencyObject = Windows.UI.Xaml.DependencyObject; +using DependencyProperty = Windows.UI.Xaml.DependencyProperty; +using PropertyMetadata = Windows.UI.Xaml.PropertyMetadata; +#else +using DependencyObject = Microsoft.UI.Xaml.DependencyObject; +using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; +using PropertyMetadata = Microsoft.UI.Xaml.PropertyMetadata; +#endif + +namespace CommunityToolkit.WinUI; + +/// +/// An attribute that indicates that a given partial property should generate a backing . +/// In order to use this attribute, the containing type has to inherit from . +/// +/// This attribute can be used as follows: +/// +/// partial class MyClass : DependencyObject +/// { +/// [GeneratedDependencyProperty] +/// public partial string? Name { get; set; } +/// } +/// +/// +/// +/// +/// +/// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# 13 (or 'preview') must be used. +/// +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class GeneratedDependencyPropertyAttribute : Attribute +{ + /// + /// Gets or sets a value indicating the default value to set for the property. + /// + /// + /// + /// If not set, the default value will be , for all property types. If there is no callback + /// registered for the generated property, will not be set at all. + /// + /// + /// To set the default value to , use . + /// + /// + /// Using this property is mutually exclusive with . + /// + /// + public object? DefaultValue { get; init; } = null; + + /// + /// Gets or sets the name of the method that will be invoked to produce the default value of the + /// property, for each instance of the containing type. The referenced method needs to return either + /// an , or a value of exactly the property type, and it needs to be parameterless. + /// + /// + /// Using this property is mutually exclusive with . + /// +#if NET8_0_OR_GREATER + [DisallowNull] +#endif + public string? DefaultValueCallback { get; init; } = null!; + + /// + /// Gets or sets a value indicating whether or not property values should be cached locally, to improve performance. + /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. + /// + /// + /// Local caching is disabled by default. It should be disabled in scenarios where the values of the dependency + /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. + /// + public bool IsLocalCacheEnabled { get; init; } = false; + + /// + /// Gets or sets the type to use to register the property in metadata. The default value will exactly match the property type. + /// + /// + /// + /// This property allows customizing the property type in metadata, in advanced scenarios. For instance, it can be used to define + /// properties of a type (e.g. ) as just using in metadata. + /// This allows working around some issues primarily around classic (reflection-based) binding in XAML. + /// + /// + /// This property should only be set when actually required (e.g. to ensure a specific scenario can work). The default behavior + /// (i.e. the property type in metadata matching the declared property type) should work correctly in the vast majority of cases. + /// + /// +#if NET8_0_OR_GREATER + [DisallowNull] +#endif + public Type? PropertyType { get; init; } = null!; +} diff --git a/components/DependencyPropertyGenerator/src/MultiTarget.props b/components/DependencyPropertyGenerator/src/MultiTarget.props new file mode 100644 index 000000000..6a4600344 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk; + + \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems new file mode 100644 index 000000000..f30608823 --- /dev/null +++ b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 22BE50B3-9810-4304-899E-6D7AF9D3147A + + + DependencyPropertyGeneratorExperiment.Tests + + \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj new file mode 100644 index 000000000..65d342261 --- /dev/null +++ b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj @@ -0,0 +1,13 @@ + + + + 22BE50B3-9810-4304-899E-6D7AF9D3147A + 14.0 + + + + + + + + diff --git a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj index c339fb59f..34c3bb7e4 100644 --- a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj +++ b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj @@ -30,7 +30,7 @@ - + diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index bc175d9ac..208bb3c6e 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -15,8 +15,10 @@ disable false false + false - + + false uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0;