Skip to content

Commit f8cb6a0

Browse files
authored
Make ICustomPropertyProvider AOT safe (#1677)
* Add AOT friendly support for ICutomPropertyProvider * Add missing file and fix attribute * Change approach to work with generic types and add more tests * Add authoring test * Fix check to handle namespaces * Fix comments * Fix warnings * PR feedback * PR feedback * Indenting
1 parent aa59310 commit f8cb6a0

File tree

11 files changed

+794
-39
lines changed

11 files changed

+794
-39
lines changed

src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
101101
GenerateCCWForGenericInstantiation);
102102

103103
context.RegisterImplementationSourceOutput(vtablesToAddOnLookupTable.Collect().Combine(properties), GenerateVtableLookupTable);
104+
105+
var bindableCustomPropertyAttributes = context.SyntaxProvider.CreateSyntaxProvider(
106+
static (n, _) => NeedCustomPropertyImplementation(n),
107+
static (n, _) => n)
108+
.Select((data, _) => GetBindableCustomProperties(data))
109+
.Where(static bindableCustomProperties => bindableCustomProperties != default)
110+
.Collect()
111+
.Combine(properties);
112+
context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties);
104113
}
105114

106115
// Restrict to non-projected classes which can be instantiated
@@ -123,6 +132,14 @@ private static bool IsComponentType(SyntaxNode node)
123132
!GeneratorHelper.IsWinRTType(declaration); // Making sure it isn't an RCW we are projecting.
124133
}
125134

135+
private static bool NeedCustomPropertyImplementation(SyntaxNode node)
136+
{
137+
return node is ClassDeclarationSyntax declaration &&
138+
!declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword)) &&
139+
declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) &&
140+
GeneratorHelper.HasBindableCustomPropertyAttribute(declaration);
141+
}
142+
126143
private static (VtableAttribute, EquatableArray<VtableAttribute>) GetVtableAttributeToAdd(
127144
GeneratorSyntaxContext context,
128145
TypeMapper typeMapper,
@@ -224,6 +241,117 @@ private static (VtableAttribute, VtableAttribute) GetVtableAttributesForTaskAdap
224241
return default;
225242
}
226243

244+
#nullable enable
245+
private static BindableCustomProperties GetBindableCustomProperties(GeneratorSyntaxContext context)
246+
{
247+
var symbol = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node)!;
248+
INamedTypeSymbol bindableCustomPropertyAttributeSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("WinRT.BindableCustomPropertyAttribute")!;
249+
250+
if (bindableCustomPropertyAttributeSymbol is null ||
251+
!symbol.TryGetAttributeWithType(bindableCustomPropertyAttributeSymbol, out AttributeData? attributeData))
252+
{
253+
return default;
254+
}
255+
256+
List<BindableCustomProperty> bindableCustomProperties = new();
257+
258+
// Make all public properties in the class bindable including ones in base type.
259+
if (attributeData.ConstructorArguments.Length == 0)
260+
{
261+
for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType)
262+
{
263+
foreach (var propertySymbol in curSymbol.GetMembers().
264+
Where(m => m.Kind == SymbolKind.Property &&
265+
m.DeclaredAccessibility == Accessibility.Public))
266+
{
267+
AddProperty(propertySymbol);
268+
}
269+
}
270+
}
271+
// Make specified public properties in the class bindable including ones in base type.
272+
else if (attributeData.ConstructorArguments is
273+
[
274+
{ Kind: TypedConstantKind.Array, Values: [..] propertyNames },
275+
{ Kind: TypedConstantKind.Array, Values: [..] propertyIndexerTypes }
276+
])
277+
{
278+
for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType)
279+
{
280+
foreach (var member in curSymbol.GetMembers())
281+
{
282+
if (member is IPropertySymbol propertySymbol &&
283+
member.DeclaredAccessibility == Accessibility.Public)
284+
{
285+
if (!propertySymbol.IsIndexer &&
286+
propertyNames.Any(p => p.Value is string value && value == propertySymbol.Name))
287+
{
288+
AddProperty(propertySymbol);
289+
}
290+
else if (propertySymbol.IsIndexer &&
291+
// ICustomProperty only supports single indexer parameter.
292+
propertySymbol.Parameters.Length == 1 &&
293+
propertyIndexerTypes.Any(p => p.Value is ISymbol typeSymbol && typeSymbol.Equals(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default)))
294+
{
295+
AddProperty(propertySymbol);
296+
}
297+
}
298+
}
299+
}
300+
}
301+
302+
var typeName = ToFullyQualifiedString(symbol);
303+
bool isGlobalNamespace = symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace;
304+
var @namespace = symbol.ContainingNamespace?.ToDisplayString();
305+
if (!isGlobalNamespace)
306+
{
307+
typeName = typeName[(@namespace!.Length + 1)..];
308+
}
309+
310+
EquatableArray<TypeInfo> classHierarchy = ImmutableArray<TypeInfo>.Empty;
311+
312+
// Gather the type hierarchy, only if the type is nested (as an optimization)
313+
if (symbol.ContainingType is not null)
314+
{
315+
List<TypeInfo> hierarchyList = new();
316+
317+
for (ITypeSymbol parent = symbol; parent is not null; parent = parent.ContainingType)
318+
{
319+
hierarchyList.Add(new TypeInfo(
320+
parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
321+
parent.TypeKind,
322+
parent.IsRecord));
323+
}
324+
325+
classHierarchy = ImmutableArray.CreateRange(hierarchyList);
326+
}
327+
328+
return new BindableCustomProperties(
329+
@namespace,
330+
isGlobalNamespace,
331+
typeName,
332+
classHierarchy,
333+
ToFullyQualifiedString(symbol),
334+
bindableCustomProperties.ToImmutableArray());
335+
336+
void AddProperty(ISymbol symbol)
337+
{
338+
if (symbol is IPropertySymbol propertySymbol)
339+
{
340+
bindableCustomProperties.Add(new BindableCustomProperty(
341+
propertySymbol.MetadataName,
342+
ToFullyQualifiedString(propertySymbol.Type),
343+
// Make sure the property accessors are also public even if property itself is public.
344+
propertySymbol.GetMethod != null && propertySymbol.GetMethod.DeclaredAccessibility == Accessibility.Public,
345+
propertySymbol.SetMethod != null && propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public,
346+
propertySymbol.IsIndexer,
347+
propertySymbol.IsIndexer ? ToFullyQualifiedString(propertySymbol.Parameters[0].Type) : "",
348+
propertySymbol.IsStatic
349+
));
350+
}
351+
}
352+
}
353+
#nullable disable
354+
227355
private static string ToFullyQualifiedString(ISymbol symbol)
228356
{
229357
// Used to ensure class names within generics are fully qualified to avoid
@@ -1278,6 +1406,119 @@ private static string LookupRuntimeClassName(Type type)
12781406
addSource($"WinRT{classPrefix}GlobalVtableLookup.g.cs", source.ToString());
12791407
}
12801408
}
1409+
1410+
private static void GenerateBindableCustomProperties(
1411+
SourceProductionContext sourceProductionContext,
1412+
(ImmutableArray<BindableCustomProperties> bindableCustomProperties, (bool isCsWinRTAotOptimizerEnabled, bool isCsWinRTComponent, bool isCsWinRTCcwLookupTableGeneratorEnabled) properties) value)
1413+
{
1414+
if (!value.properties.isCsWinRTAotOptimizerEnabled || value.bindableCustomProperties.Length == 0)
1415+
{
1416+
return;
1417+
}
1418+
1419+
StringBuilder source = new();
1420+
1421+
foreach (var bindableCustomProperties in value.bindableCustomProperties)
1422+
{
1423+
if (!bindableCustomProperties.IsGlobalNamespace)
1424+
{
1425+
source.AppendLine($$"""
1426+
namespace {{bindableCustomProperties.Namespace}}
1427+
{
1428+
""");
1429+
}
1430+
1431+
var escapedClassName = GeneratorHelper.EscapeTypeNameForIdentifier(bindableCustomProperties.ClassName);
1432+
1433+
ReadOnlySpan<TypeInfo> classHierarchy = bindableCustomProperties.ClassHierarchy.AsSpan();
1434+
// If the type is nested, correctly nest the type definition
1435+
for (int i = classHierarchy.Length - 1; i > 0; i--)
1436+
{
1437+
source.AppendLine($$"""
1438+
partial {{classHierarchy[i].GetTypeKeyword()}} {{classHierarchy[i].QualifiedName}}
1439+
{
1440+
""");
1441+
}
1442+
1443+
source.AppendLine($$"""
1444+
partial class {{(classHierarchy.IsEmpty ? bindableCustomProperties.ClassName : classHierarchy[0].QualifiedName)}} : global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation
1445+
{
1446+
global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(string name)
1447+
{
1448+
""");
1449+
1450+
foreach (var property in bindableCustomProperties.Properties.Where(p => !p.IsIndexer))
1451+
{
1452+
var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)""";
1453+
1454+
source.AppendLine($$"""
1455+
if (name == "{{property.Name}}")
1456+
{
1457+
return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty(
1458+
{{GetBoolAsString(property.CanRead)}},
1459+
{{GetBoolAsString(property.CanWrite)}},
1460+
"{{property.Name}}",
1461+
typeof({{property.Type}}),
1462+
{{ (property.CanRead ? $$"""static (instance) => {{instanceAccessor}}.{{property.Name}}""" : "null") }},
1463+
{{ (property.CanWrite ? $$"""static (instance, value) => {{instanceAccessor}}.{{property.Name}} = ({{property.Type}})value""" : "null") }},
1464+
null,
1465+
null);
1466+
}
1467+
""");
1468+
}
1469+
1470+
source.AppendLine($$"""
1471+
return default;
1472+
}
1473+
1474+
global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(global::System.Type indexParameterType)
1475+
{
1476+
""");
1477+
1478+
foreach (var property in bindableCustomProperties.Properties.Where(p => p.IsIndexer))
1479+
{
1480+
var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)""";
1481+
1482+
source.AppendLine($$"""
1483+
if (indexParameterType == typeof({{property.IndexerType}}))
1484+
{
1485+
return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty(
1486+
{{GetBoolAsString(property.CanRead)}},
1487+
{{GetBoolAsString(property.CanWrite)}},
1488+
"{{property.Name}}",
1489+
typeof({{property.Type}}),
1490+
null,
1491+
null,
1492+
{{ (property.CanRead ? $$"""static (instance, index) => {{instanceAccessor}}[({{property.IndexerType}})index]""" : "null") }},
1493+
{{ (property.CanWrite ? $$"""static (instance, value, index) => {{instanceAccessor}}[({{property.IndexerType}})index] = ({{property.Type}})value""" : "null") }});
1494+
}
1495+
""");
1496+
}
1497+
1498+
source.AppendLine($$"""
1499+
return default;
1500+
}
1501+
}
1502+
""");
1503+
1504+
// Close all brackets
1505+
for (int i = classHierarchy.Length - 1; i > 0; i--)
1506+
{
1507+
source.AppendLine("}");
1508+
}
1509+
1510+
if (!bindableCustomProperties.IsGlobalNamespace)
1511+
{
1512+
source.AppendLine($@"}}");
1513+
}
1514+
1515+
source.AppendLine();
1516+
}
1517+
1518+
sourceProductionContext.AddSource("WinRTCustomBindableProperties.g.cs", source.ToString());
1519+
1520+
static string GetBoolAsString(bool value) => value ? "true" : "false";
1521+
}
12811522
}
12821523

12831524
internal readonly record struct GenericParameter(
@@ -1303,6 +1544,23 @@ internal sealed record VtableAttribute(
13031544
bool IsPublic,
13041545
string RuntimeClassName = default);
13051546

1547+
internal readonly record struct BindableCustomProperty(
1548+
string Name,
1549+
string Type,
1550+
bool CanRead,
1551+
bool CanWrite,
1552+
bool IsIndexer,
1553+
string IndexerType,
1554+
bool IsStatic);
1555+
1556+
internal readonly record struct BindableCustomProperties(
1557+
string Namespace,
1558+
bool IsGlobalNamespace,
1559+
string ClassName,
1560+
EquatableArray<TypeInfo> ClassHierarchy,
1561+
string QualifiedClassName,
1562+
EquatableArray<BindableCustomProperty> Properties);
1563+
13061564
/// <summary>
13071565
/// A model describing a type info in a type hierarchy.
13081566
/// </summary>

src/Authoring/WinRT.SourceGenerator/Helper.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,27 @@ public static bool IsWinRTType(MemberDeclarationSyntax node)
450450
return isProjectedType;
451451
}
452452

453+
public static bool HasBindableCustomPropertyAttribute(MemberDeclarationSyntax node)
454+
{
455+
return node.AttributeLists.SelectMany(list => list.Attributes).Any(IsBindableCustomPropertyAttribute);
456+
457+
// Check based on identifier name if this is the BindableCustomProperty attribute.
458+
// Technically this can be a different namespace, but we will confirm later once
459+
// we have access to the semantic model.
460+
static bool IsBindableCustomPropertyAttribute(AttributeSyntax attribute)
461+
{
462+
var nameSyntax = attribute.Name;
463+
if (nameSyntax is QualifiedNameSyntax qualifiedName)
464+
{
465+
// Right would have the attribute while left is the namespace.
466+
nameSyntax = qualifiedName.Right;
467+
}
468+
469+
return nameSyntax is IdentifierNameSyntax name &&
470+
name.Identifier.ValueText == "BindableCustomProperty";
471+
}
472+
}
473+
453474
/// <summary>
454475
/// Checks whether or not a given symbol has an attribute with the specified type.
455476
/// </summary>

src/Tests/AuthoringTest/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry
199199
public string Value => "CsWinRT";
200200
}
201201

202+
[BindableCustomProperty]
203+
public sealed partial class CustomProperty
204+
{
205+
public int Number { get; } = 4;
206+
public string Value => "CsWinRT";
207+
}
208+
202209
[Version(3u)]
203210
public interface IDouble
204211
{

0 commit comments

Comments
 (0)