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