Skip to content

Commit 09b80fa

Browse files
matt-goldmanCopilotTheCodeTraveler
authored
Add compile-time property initializer for default values in BindableProperty source generaor (#3204)
* Add compile-time property initializer for default value sin BindableProperty source generaor * Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address copilot feedback about properties and invocations * Update test to accomodate literal expression change * Update timespan tests for new resolver pattern * Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address copilot feedback * Use Try Pattern * Update formatting * Update BindablePropertyModelTests.cs * Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs" This reverts commit 375870b. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com>
1 parent 6c4f8c4 commit 09b80fa

10 files changed

Lines changed: 417 additions & 165 deletions

File tree

.github/instructions/codestyle.instructions.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,16 @@ Within a class, struct, or interface, elements should be positioned in the follo
566566
11. Records
567567
12. Structs
568568
13. Classes
569+
570+
### Try Pattern
571+
572+
When a method name begins with `Try` and returns a variable, ensure it adheres to the Try Pattern.
573+
574+
Here Key Components of the Try Pattern:
575+
1. Method Name: Begins with Try (e.g., TryParse, TryGetValue).
576+
2. Return Type: bool (indicating success or failure).
577+
3. Out Parameter: An out parameter to return the result if successful.
578+
4. Null Analysis Attribute: [NotNullWhen(true)] (or [MaybeNullWhen(false)]) informs the compiler that if the method returns true, the out parameter is guaranteed to be non-null.
579+
580+
This pattern allows for high-performance retrieval or parsing without throwing exceptions for expected failures. It also allows cleaner call sites by eliminating the need for null-checking the output variable within the if block, as seen in Dictionary<TKey, TValue>.TryGetValue.
581+
```

src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -598,8 +598,8 @@ public partial class {{defaultTestClassName}}
598598
/// <summary>
599599
/// BindableProperty for the <see cref = "Text"/> property.
600600
/// </summary>
601-
public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultText);
602-
public partial string Text { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
601+
public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), "Initial Value", (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null);
602+
public partial string Text { get => false ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
603603
604604
/// <summary>
605605
/// BindableProperty for the <see cref = "CustomDuration"/> property.
@@ -610,20 +610,11 @@ public partial class {{defaultTestClassName}}
610610
611611
file static class __{{defaultTestClassName}}BindablePropertyInitHelpers
612612
{
613-
public static volatile bool IsInitializingText = false;
614-
public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
615-
{
616-
IsInitializingText = true;
617-
var defaultValue = ((TestView)bindable).Text;
618-
IsInitializingText = false;
619-
return defaultValue;
620-
}
621-
622613
public static volatile bool IsInitializingCustomDuration = false;
623614
public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
624615
{
625616
IsInitializingCustomDuration = true;
626-
var defaultValue = ((TestView)bindable).CustomDuration;
617+
var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration;
627618
IsInitializingCustomDuration = false;
628619
return defaultValue;
629620
}

src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs

Lines changed: 24 additions & 120 deletions
Large diffs are not rendered by default.

src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,21 +162,8 @@ public partial class {{defaultTestClassName}}<T, U>
162162
/// <summary>
163163
/// BindableProperty for the <see cref = "Value"/> property.
164164
/// </summary>
165-
public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}<T, U>), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultValue);
166-
public partial T? Value { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
167-
168-
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
169-
private static class __{{defaultTestClassName}}BindablePropertyInitHelpers
170-
{
171-
public static volatile bool IsInitializingValue = false;
172-
public static object CreateDefaultValue(global::Microsoft.Maui.Controls.BindableObject bindable)
173-
{
174-
IsInitializingValue = true;
175-
var defaultValue = (({{defaultTestClassName}}<T, U>)bindable).Value;
176-
IsInitializingValue = false;
177-
return defaultValue;
178-
}
179-
}
165+
public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}<T, U>), default(T), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null);
166+
public partial T? Value { get => false ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
180167
}
181168
""";
182169

src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyModelTests.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public void BindablePropertyName_ReturnsCorrectPropertyName()
3030
true, // IsReadOnlyBindableProperty
3131
string.Empty, // SetterAccessibility
3232
false,
33-
"null"
33+
"null",
34+
null
3435
);
3536

3637
// Act
@@ -74,7 +75,8 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
7475
true, // IsReadOnlyBindableProperty
7576
string.Empty, // SetterAccessibility
7677
hasInitializer,
77-
propertyAccessibility
78+
propertyAccessibility,
79+
null
7880
);
7981

8082
// Assert
@@ -135,7 +137,8 @@ public void SemanticValues_WithClassInfoAndProperties_StoresCorrectValues()
135137
true, // IsReadOnlyBindableProperty
136138
string.Empty, // SetterAccessibilityText
137139
false,
138-
"public"
140+
"public",
141+
null
139142
);
140143

141144
var bindableProperties = new[] { bindableProperty }.ToImmutableArray();
@@ -194,7 +197,8 @@ public class TestClass { public string TestProperty { get; set; } = "Initial Val
194197
true,
195198
string.Empty,
196199
hasInitializer,
197-
"public"
200+
"public",
201+
null
198202
);
199203

200204
// Act
@@ -234,7 +238,8 @@ public class TestClass { public string TestProperty { get; set; } = "Initial Val
234238
true,
235239
string.Empty,
236240
hasInitializer,
237-
"public"
241+
"public",
242+
null
238243
);
239244

240245
// Act

src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper
237237
.Append(GetFormattedReturnType(nonNullableReturnType))
238238
.Append("), typeof(")
239239
.Append(info.DeclaringType)
240-
.Append("), null, ")
240+
.Append("), ")
241+
.Append(info.ResolvedInitializerExpression ?? "null")
242+
.Append(", ")
241243
.Append(info.DefaultBindingMode)
242244
.Append(", ")
243245
.Append(info.ValidateValueMethodName)
@@ -304,7 +306,9 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
304306
.Append(GetFormattedReturnType(nonNullableReturnType))
305307
.Append("), typeof(")
306308
.Append(info.DeclaringType)
307-
.Append("), null, ")
309+
.Append("), ")
310+
.Append(info.ResolvedInitializerExpression ?? "null")
311+
.Append(", ")
308312
.Append(info.DefaultBindingMode)
309313
.Append(", ")
310314
.Append(info.ValidateValueMethodName)
@@ -408,8 +412,15 @@ static BindablePropertySemanticValues SemanticTransform(GeneratorAttributeSyntax
408412

409413
var propertyAccessibility = GetPropertyAccessibility(propertySymbol);
410414

415+
// Resolve initializer expression to a fully qualified string for use as defaultValue
416+
string? resolvedInitializer = null;
417+
if (hasInitializer && propertyDeclarationSyntax.Initializer is not null)
418+
{
419+
InitializerExpressionResolver.TryResolve(propertyDeclarationSyntax.Initializer.Value, semanticModel, out resolvedInitializer);
420+
}
421+
411422
var attributeData = context.Attributes[0];
412-
bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility, hasInitializer, propertyAccessibility);
423+
bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility, hasInitializer, propertyAccessibility, resolvedInitializer);
413424

414425
return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels));
415426
}
@@ -517,7 +528,7 @@ static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol)
517528
return sb.ToString();
518529
}
519530

520-
static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility, in bool hasInitializer, in string? propertyAccessibility)
531+
static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility, in bool hasInitializer, in string? propertyAccessibility, in string? resolvedInitializerExpression)
521532
{
522533
if (attributeData.AttributeClass is null)
523534
{
@@ -532,7 +543,10 @@ static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attrib
532543
var validateValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));
533544
var newKeywordText = doesContainNewKeyword ? "new " : string.Empty;
534545

535-
return new BindablePropertyModel(propertyName, returnType, declaringType, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility, hasInitializer, propertyAccessibility);
546+
// Only use the resolved initializer when there's no explicit DefaultValueCreatorMethodName
547+
var effectiveResolvedInitializer = defaultValueCreatorMethodName is "null" ? resolvedInitializerExpression : null;
548+
549+
return new BindablePropertyModel(propertyName, returnType, declaringType, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility, hasInitializer, propertyAccessibility, effectiveResolvedInitializer);
536550
}
537551

538552
[MethodImpl(MethodImplOptions.AggressiveInlining)]

0 commit comments

Comments
 (0)