diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9db0a33a8..0897a88f0 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/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index cf4e183b4..ef7579bf9 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -6,7 +6,12 @@ true CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) + false false + false + + + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; @@ -29,7 +34,7 @@ - + Windows Desktop Extensions for the UWP 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/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..5197300bb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,481 @@ +// 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 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; + + // We can only possibly fix diagnostics with an additional location + if (diagnostic.AdditionalLocations is not [{ } fieldLocation]) + { + return; + } + + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + + 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, + defaultValue), + 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 expression for the default value of the property, if present + /// The updated attribute syntax. + private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList( + Document document, + SemanticModel semanticModel, + AttributeListSyntax generatedDependencyPropertyAttributeList, + string? defaultValueExpression) + { + // 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) + { + ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); + + // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed' + if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + { + string fullyQualifiedTypeName = expressionSyntax.ToFullString(); + + // Ensure we strip the global prefix, if present (it should always be present) + if (fullyQualifiedTypeName.StartsWith("global::")) + { + fullyQualifiedTypeName = fullyQualifiedTypeName["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(fullyQualifiedTypeName) is INamedTypeSymbol enumTypeSymbol) + { + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // 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 to insert + SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax); + + // Actually add the argument to the existing attribute syntax + return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + } + } + + // Otherwise, just add the new default value normally + return + AttributeList(SingletonSeparatedList( + generatedDependencyPropertyAttributeList.Attributes[0] + .AddArgumentListArguments( + AttributeArgument(ParseExpression(defaultValueExpression)) + .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + } + + // If we have no value expression, we can just reuse the attribute with no changes + return generatedDependencyPropertyAttributeList; + } + + /// + /// 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 expression for the default value of the property, if present + /// 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, + string? defaultValueExpression) + { + 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, + propertyDeclaration, + fieldDeclaration, + generatedDependencyPropertyAttributeList, + syntaxEditor, + defaultValueExpression); + + 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 for the property being updated. + /// The for the declared property to remove. + /// The with the attribute to add. + /// The instance to use. + /// The expression for the default value of the property, if present + /// An updated document with the applied code fix, and being replaced with a partial property. + private static void ConvertToPartialProperty( + Document document, + SemanticModel semanticModel, + PropertyDeclarationSyntax propertyDeclaration, + FieldDeclarationSyntax fieldDeclaration, + AttributeListSyntax generatedDependencyPropertyAttributeList, + SyntaxEditor syntaxEditor, + string? defaultValueExpression) + { + // 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, + generatedDependencyPropertyAttributeList, + defaultValueExpression); + + // 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 rmeoved. 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))); + } + } + + /// + /// 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; + } + + // Also check that we can find the target field to remove + if (diagnostic.AdditionalLocations is not [{ } fieldLocation] || + root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) + { + continue; + } + + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + + ConvertToPartialProperty( + document, + semanticModel, + propertyDeclaration, + fieldDeclaration, + generatedDependencyPropertyAttributeList, + syntaxEditor, + defaultValue); + + 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..55f8ed3fc --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,28 @@ +; 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 +--------|----------|----------|------- +WCTDP0001 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0002 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0003 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0004 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0005 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0006 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0007 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0008 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0009 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0010 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0011 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0012 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0013 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | +WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | +WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | 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..7d88ca86a --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +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); +} 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..1773b993f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -0,0 +1,1047 @@ +// 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; + } + + /// + /// Gets the default value to use to initialize the generated property, if explicitly specified. + /// + /// The input that triggered the annotation. + /// The input instance. + /// 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, + 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()) + { + // For non nullable types, we return 'default(T)', unless we can optimize for projected types + return new DependencyPropertyDefaultValue.Default( + TypeName: propertySymbol.Type.GetFullyQualifiedName(), + IsProjectedType: propertySymbol.Type.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); + } + + // 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 + { IsNet8OrGreater: 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}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).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}]"); + } + + writer.Write($$""" + public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( + name: "{{propertyInfo.PropertyName}}", + propertyType: typeof({{propertyInfo.TypeName}}), + 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) + { + // If the target is not .NET 8, we never need additional types (as '[UnsafeAccessor]' is not available) + if (!propertyInfos[0].IsNet8OrGreater) + { + 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..4abd428e5 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.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 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(); + + string typeName = propertySymbol.Type.GetFullyQualifiedName(); + string typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + token.ThrowIfCancellationRequested(); + + bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); + bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); + bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); + + 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, + 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, + DefaultValue: defaultValue, + IsReferenceTypeOrUnconstraindTypeParameter: isReferenceTypeOrUnconstraindTypeParameter, + IsLocalCachingEnabled: isLocalCachingEnabled, + IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, + IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, + IsNet8OrGreater: isNet8OrGreater, + 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/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..b15719f1f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -0,0 +1,161 @@ +// 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.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 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.GetLocation(), + propertySymbol, + defaultValueCallback)); + } + else if (!IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + // Emit a diagnostic if the candidate method is not valid + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod, + attributeData.GetLocation(), + 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; + } + + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + + // Otherwise, try to see if the return is the type argument of a nullable value type + if (isNullableValueType && + methodSymbol.ReturnType.TypeKind is TypeKind.Struct && + SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) + { + return true; + } + + return false; + } +} 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..485819482 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -0,0 +1,128 @@ +// 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.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.RegisterOperationAction(context => + { + // We only care about attributes on properties + if (context.ContainingSymbol is not IPropertySymbol 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; + } + + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; + + // 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 (!isNullableType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueNull, + attributeData.GetLocation(), + propertySymbol, + propertySymbol.Type)); + } + } + else + { + // Get the target type with a special case for 'Nullable' + ITypeSymbol propertyTypeSymbol = 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.GetLocation(), + propertySymbol, + propertySymbol.Type, + defaultValue.Value, + defaultValue.Type)); + } + } + }, 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..0824c140e --- /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.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.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/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs new file mode 100644 index 000000000..4d729b2e0 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.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.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 warning when a property with [GeneratedDependencyProperty] would generate a nullability annotations violation. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyNonNullableDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced]; + + /// + 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); + + // Attempt to also get the '[MaybeNull]' symbols (there might be multiples, due to polyfills) + ImmutableArray maybeNullAttributeSymbol = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + + context.RegisterSymbolAction(context => + { + // Validate that we have a property that is of some type that can be explicitly nullable. + // We're intentionally ignoring 'Nullable' values here, since those are by defintiion nullable. + // Additionally, we only care about properties that are explicitly marked as not nullable. + // Lastly, we can skip required properties, since for those it's completely fine to be non-nullable. + if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: NullableAnnotation.NotAnnotated, IsRequired: false } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If the property has '[MaybeNull]', we never need to emit a diagnostic + if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbol)) + { + return; + } + + // Emit a diagnostic if there is no default value, or if it's 'null' + if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue) || defaultValue.IsNull) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} 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..6052a8beb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs @@ -0,0 +1,55 @@ +// 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.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; + + // 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..c05b15809 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -0,0 +1,79 @@ +// 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 => + { + // 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; + } + + 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/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs new file mode 100644 index 000000000..ee855ca3b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -0,0 +1,644 @@ +// 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 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"; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; + + /// + 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; + } + + // 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 + { + DeclaredAccessibility: Accessibility.Public, + IsStatic: true, + IsReadOnly: true, + IsFixedSizeBuffer: false, + IsRequired: false, + Type.IsReferenceType: true, + IsVolatile: false + } fieldSymbol) + { + // We only care about fields that are 'DependencyProperty' + 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; + } + + // The field must follow the expected naming pattern. We can check this just here in the getter. + // If this is valid, the whole property will be skipped anyway, so no need to do it twice. + if (fieldSymbol.Name != $"{propertySymbol.Name}Property") + { + 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) + if (context.Operation is not IFieldInitializerOperation { InitializedFields: [{ } fieldSymbol], Value: IInvocationOperation invocationOperation }) + { + return; + } + + // Check that the field is one of the ones we expect to encounter + if (!fieldMap.TryGetValue(fieldSymbol, out FieldFlags? fieldFlags)) + { + return; + } + + // Validate that we are calling 'DependencyProperty.Register' + if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) + { + return; + } + + // Next, make sure we have the arguments we expect + if (invocationOperation.Arguments is not [{ } nameArgument, { } propertyTypeArgument, { } ownerTypeArgument, { } propertyMetadataArgument]) + { + return; + } + + // 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 not { HasValue: true, Value: string propertyName }) + { + return; + } + + // Extract the property type, we can validate it later + if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) + { + return; + } + + // Extract the owning type, we can validate it right now + if (ownerTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } owningTypeSymbol }) + { + return; + } + + // The owning type always has to be exactly the same as the containing type + if (!SymbolEqualityComparer.Default.Equals(owningTypeSymbol, typeSymbol)) + { + return; + } + + // 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)) + { + 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) + { + return; + } + + // Make sure the object being created is actually 'PropertyMetadata' + if (!SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, propertyMetadataSymbol)) + { + return; + } + + // The argument should be a conversion operation (boxing) + if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + { + return; + } + + bool isNullableValueType = propertyTypeSymbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + + // 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 (!conversionOperation.Operand.IsConstantValueDefault() || isNullableValueType) + { + // The value is just 'null' with no type, special case this one and skip the other checks below + if (conversionOperation.Operand 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 (!propertyTypeSymbol.IsReferenceType && !isNullableValueType) + { + return; + } + } + else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) + { + // We have found a valid constant. As an optimization, we check whether the constant was the value + // 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. + if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType && + operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + { + // 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) && + conversionOperation.Operand.ConstantValue.Value == defaultValue) + { + fieldFlags.DefaultValue = null; + } + } + } + else + { + // If we don't have a constant, check if it's some constant value we can forward. In this case, we + // did not retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. + if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) + { + return; + } + + // Also make sure the 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 (!SymbolEqualityComparer.Default.Equals(defaultValueExpressionType, propertyTypeSymbol)) + { + 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.PropertyName = propertyName; + fieldFlags.PropertyType = propertyTypeSymbol; + 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; + } + + // 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. + if (fieldFlags.PropertyName != pair.Key.Name) + { + continue; + } + + // Make sure that the 'propertyType' value matches the actual type of the property. + // We are intentionally not handling combinations of nullable value types here. + if (!SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type)) + { + continue; + } + + // Finally, check whether the field was valid (if so, we will have a valid location) + if (fieldFlags.FieldLocation is Location fieldLocation) + { + context.ReportDiagnostic(Diagnostic.Create( + UseGeneratedDependencyPropertyForManualProperty, + pair.Key.Locations.FirstOrDefault(), + [fieldLocation], + ImmutableDictionary.Create().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.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.PropertyName = null; + fieldFlags.PropertyType = null; + fieldFlags.DefaultValue = null; + fieldFlags.FieldLocation = null; + + 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 name of the property. + /// + public string? PropertyName; + + /// + /// The type of the property (as in, of values that can be assigned to it). + /// + public ITypeSymbol? PropertyType; + + /// + /// The default value to use (not present if it does not need to be set explicitly). + /// + public TypedConstantInfo? DefaultValue; + + /// + /// The location of the target field being initialized. + /// + public Location? FieldLocation; + } +} 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..d86c378e5 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,265 @@ +// 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 diagnostic id for . + /// + public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDP0017"; + + /// + /// "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: "WCTDP0001", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0002", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0003", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0004", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0005", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0006", + 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: "WCTDP0007", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0008", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0009", + 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: typeof(DependencyPropertyGenerator).FullName, + 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 fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( + id: "WCTDP0010", + 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 fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + category: typeof(DependencyPropertyGenerator).FullName, + 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 'Get(ref object)' method 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 'Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueType = new( + id: "WCTDP0011", + 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 'Get(ref object)' partial method to handle the type mismatch)", + category: typeof(DependencyPropertyGenerator).FullName, + 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 'Get(ref object)' method 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: "WCTDP0012", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0013", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0014", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0015", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0016", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0018", + 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: typeof(DependencyPropertyGenerator).FullName, + 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: "WCTDP0019", + 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: typeof(DependencyPropertyGenerator).FullName, + 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"); +} 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..0e7c0fafe --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,91 @@ +// +#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 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 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; + } +} + +#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..ef945b78b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,117 @@ +// 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 Microsoft.CodeAnalysis; + +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 { } syntaxReference) + { + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + return null; + } + + /// + /// 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/IOperationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs new file mode 100644 index 000000000..a6d7ba672 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +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; + } + + // Manually match for known primitive types + return (operation.Type.SpecialType, operation.ConstantValue.Value) switch + { + (SpecialType.System_Byte, default(byte)) or + (SpecialType.System_Char, default(char)) 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)) 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..c49e99c1c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.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.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; + } +} 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..c7306082b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,264 @@ +// 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) + { + return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + + /// + /// 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..738cfac37 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -0,0 +1,77 @@ +// 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) + { + // 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 INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + { + return true; + } + + // Lastly, special case the well known primitive types + if (symbol.SpecialType is + SpecialType.System_Int32 or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char 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..8ac269e7c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs @@ -0,0 +1,105 @@ +// 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 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 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 argumentName }) + { + namedArguments.Add((argumentName, argumentInfo)); + } + else + { + constructorArguments.Add(argumentInfo); + } + } + + info = new AttributeInfo( + typeName, + constructorArguments.ToImmutable(), + namedArguments.ToImmutable()); + + return true; + } + + /// + public override string ToString() + { + // Gather the constructor arguments + IEnumerable arguments = + ConstructorArgumentInfo + .Select(static arg => AttributeArgument(ParseExpression(arg.ToString()))); + + // 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..993c9d717 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -0,0 +1,83 @@ +// 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 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..a53c35f6f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.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 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 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 the current target is .NET 8 or greater. +/// 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, + DependencyPropertyDefaultValue DefaultValue, + bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsLocalCachingEnabled, + bool IsPropertyChangedCallbackImplemented, + bool IsSharedPropertyChangedCallbackImplemented, + bool IsNet8OrGreater, + 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..4ffb03a31 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -0,0 +1,300 @@ +// 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 + { + /// + 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..441d35d82 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -0,0 +1,62 @@ +// 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; +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; +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); + } +} 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..30b0a2fb2 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -0,0 +1,1820 @@ +// 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 + { + [{|WCTDP0001: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 + { + [{|WCTDP0001: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 + { + [{|WCTDP0001: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 + { + [{|WCTDP0001: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 + { + [{|WCTDP0001: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 + { + [{|WCTDP0002: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 + { + [{|WCTDP0002: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 + { + [{|WCTDP0003: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 + { + [{|WCTDP0003: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 + { + [{|WCTDP0004: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 + { + [{|WCTDP0012: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 + { + [{|WCTDP0012: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 + { + [{|WCTDP0005: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 + { + [{|WCTDP0006: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 + { + [{|WCTDP0007: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 + { + [{|WCTDP0008:GeneratedDependencyProperty|}] + public partial {{propertyType}} {|CS9248:Property|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_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 InvalidPropertyNonNullableDeclarationAnalyzer_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 InvalidPropertyNonNullableDeclarationAnalyzer_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 InvalidPropertyNonNullableDeclarationAnalyzer_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 InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + 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 InvalidPropertyNonNullableDeclarationAnalyzer_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 required partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty|}] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty(DefaultValue = null)|}] + public partial string {|CS9248:Name|} { 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] + 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 + { + [{|WCTDP0010:GeneratedDependencyProperty(DefaultValue = null)|}] + public partial int {|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 + { + [{|WCTDP0011:GeneratedDependencyProperty(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 + { + [{|WCTDP0013: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 + { + [{|WCTDP0014:GeneratedDependencyProperty(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 + { + [{|WCTDP0014:GeneratedDependencyProperty(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 + { + [{|WCTDP0015:GeneratedDependencyProperty(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 + { + [{|WCTDP0016: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( + 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] + [DataRow("null", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(int)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(MyControl)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(object)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_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] + 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.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); + } + + [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}} {|WCTDP0017: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? {|WCTDP0017: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?", "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::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?", "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}} {|WCTDP0017: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] + 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: {|WCTDP0018: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: {|WCTDP0018: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: {|WCTDP0019: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: {|WCTDP0019:Test(TestAttribute.M)|}] + public string? Name { get; set; } + } + + public class TestAttribute(string P) : Attribute + { + public static string M => ""; + } + """; + + 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..054bb8966 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -0,0 +1,4285 @@ +// 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("""C(10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(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 })")] + 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 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); + } +} 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_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..b18a8db7c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,1313 @@ +// 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.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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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] + public partial TValue? {|CS9248:Value|} { get; set; } + + /// + /// Blah. + /// + [GeneratedDependencyProperty] + public partial TElement? {|CS9248:TargetObject|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + 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_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(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 + { + /// + /// 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 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 . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:DisableAnimation|} { get; set; } + + /// + /// 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 . + /// + [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?)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + 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 + }; + + 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..612e2bd54 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -0,0 +1,85 @@ + + + + + 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..368fe5fc1 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,82 @@ +// 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 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 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; +} 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..6db92daf4 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -15,8 +15,9 @@ disable false false + false - + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/tooling b/tooling index b121eb57c..3e178bc8f 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit b121eb57cc0fdca03206a9e1a08960d7e3cd824c +Subproject commit 3e178bc8f3d0eceb4ef9e955542ea5ce3892ca8a