Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ICustomPropertyProvider AOT safe #1677

Merged
merged 12 commits into from
Jul 23, 2024
258 changes: 258 additions & 0 deletions src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
GenerateCCWForGenericInstantiation);

context.RegisterImplementationSourceOutput(vtablesToAddOnLookupTable.Collect().Combine(properties), GenerateVtableLookupTable);

var bindableCustomPropertyAttributes = context.SyntaxProvider.CreateSyntaxProvider(
manodasanW marked this conversation as resolved.
Show resolved Hide resolved
static (n, _) => NeedCustomPropertyImplementation(n),
static (n, _) => n)
.Select((data, _) => GetBindableCustomProperties(data))
.Where(static bindableCustomProperties => bindableCustomProperties != default)
.Collect()
.Combine(properties);
context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties);
}

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

private static bool NeedCustomPropertyImplementation(SyntaxNode node)
{
return node is ClassDeclarationSyntax declaration &&
!declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword)) &&
declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) &&
GeneratorHelper.HasBindableCustomPropertyAttribute(declaration);
}

private static (VtableAttribute, EquatableArray<VtableAttribute>) GetVtableAttributeToAdd(
GeneratorSyntaxContext context,
TypeMapper typeMapper,
Expand Down Expand Up @@ -224,6 +241,117 @@ private static (VtableAttribute, VtableAttribute) GetVtableAttributesForTaskAdap
return default;
}

#nullable enable
private static BindableCustomProperties GetBindableCustomProperties(GeneratorSyntaxContext context)
{
var symbol = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node)!;
INamedTypeSymbol bindableCustomPropertyAttributeSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("WinRT.BindableCustomPropertyAttribute")!;

if (bindableCustomPropertyAttributeSymbol is null ||
!symbol.TryGetAttributeWithType(bindableCustomPropertyAttributeSymbol, out AttributeData? attributeData))
{
return default;
}

List<BindableCustomProperty> bindableCustomProperties = new();

// Make all public properties in the class bindable including ones in base type.
if (attributeData.ConstructorArguments.Length == 0)
{
for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType)
{
foreach (var propertySymbol in curSymbol.GetMembers().
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we should also make sure this works correctly with partial properties. We can handle this in a follow up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using symbol data to get this data, so I believe it should. But we can confirm after.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more worried about the fact you might see two different property symbols for the same member (one being the definition, one being the implementation part), and that the fact they'd be "duplicate" might cause problems.

Where(m => m.Kind == SymbolKind.Property &&
m.DeclaredAccessibility == Accessibility.Public))
{
AddProperty(propertySymbol);
}
}
}
// Make specified public properties in the class bindable including ones in base type.
else if (attributeData.ConstructorArguments is
[
{ Kind: TypedConstantKind.Array, Values: [..] propertyNames },
{ Kind: TypedConstantKind.Array, Values: [..] propertyIndexerTypes }
])
{
for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType)
{
foreach (var member in curSymbol.GetMembers())
{
if (member is IPropertySymbol propertySymbol &&
member.DeclaredAccessibility == Accessibility.Public)
{
if (!propertySymbol.IsIndexer &&
propertyNames.Any(p => p.Value is string value && value == propertySymbol.Name))
{
AddProperty(propertySymbol);
}
else if (propertySymbol.IsIndexer &&
// ICustomProperty only supports single indexer parameter.
propertySymbol.Parameters.Length == 1 &&
propertyIndexerTypes.Any(p => p.Value is ISymbol typeSymbol && typeSymbol.Equals(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default)))
{
AddProperty(propertySymbol);
}
}
}
}
}

var typeName = ToFullyQualifiedString(symbol);
bool isGlobalNamespace = symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace;
var @namespace = symbol.ContainingNamespace?.ToDisplayString();
if (!isGlobalNamespace)
{
typeName = typeName[(@namespace!.Length + 1)..];
}

EquatableArray<TypeInfo> classHierarchy = ImmutableArray<TypeInfo>.Empty;

// Gather the type hierarchy, only if the type is nested (as an optimization)
if (symbol.ContainingType is not null)
{
List<TypeInfo> hierarchyList = new();

for (ITypeSymbol parent = symbol; parent is not null; parent = parent.ContainingType)
{
hierarchyList.Add(new TypeInfo(
parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
parent.TypeKind,
parent.IsRecord));
}

classHierarchy = ImmutableArray.CreateRange(hierarchyList);
}

return new BindableCustomProperties(
@namespace,
isGlobalNamespace,
typeName,
classHierarchy,
ToFullyQualifiedString(symbol),
bindableCustomProperties.ToImmutableArray());

void AddProperty(ISymbol symbol)
{
if (symbol is IPropertySymbol propertySymbol)
{
bindableCustomProperties.Add(new BindableCustomProperty(
propertySymbol.MetadataName,
ToFullyQualifiedString(propertySymbol.Type),
// Make sure the property accessors are also public even if property itself is public.
propertySymbol.GetMethod != null && propertySymbol.GetMethod.DeclaredAccessibility == Accessibility.Public,
propertySymbol.SetMethod != null && propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public,
propertySymbol.IsIndexer,
propertySymbol.IsIndexer ? ToFullyQualifiedString(propertySymbol.Parameters[0].Type) : "",
propertySymbol.IsStatic
));
}
}
}
#nullable disable

private static string ToFullyQualifiedString(ISymbol symbol)
{
// Used to ensure class names within generics are fully qualified to avoid
Expand Down Expand Up @@ -1278,6 +1406,119 @@ private static string LookupRuntimeClassName(Type type)
addSource($"WinRT{classPrefix}GlobalVtableLookup.g.cs", source.ToString());
}
}

private static void GenerateBindableCustomProperties(
SourceProductionContext sourceProductionContext,
(ImmutableArray<BindableCustomProperties> bindableCustomProperties, (bool isCsWinRTAotOptimizerEnabled, bool isCsWinRTComponent, bool isCsWinRTCcwLookupTableGeneratorEnabled) properties) value)
{
if (!value.properties.isCsWinRTAotOptimizerEnabled || value.bindableCustomProperties.Length == 0)
{
return;
}

StringBuilder source = new();

foreach (var bindableCustomProperties in value.bindableCustomProperties)
{
if (!bindableCustomProperties.IsGlobalNamespace)
{
source.AppendLine($$"""
namespace {{bindableCustomProperties.Namespace}}
{
""");
}

var escapedClassName = GeneratorHelper.EscapeTypeNameForIdentifier(bindableCustomProperties.ClassName);

ReadOnlySpan<TypeInfo> classHierarchy = bindableCustomProperties.ClassHierarchy.AsSpan();
// If the type is nested, correctly nest the type definition
for (int i = classHierarchy.Length - 1; i > 0; i--)
{
source.AppendLine($$"""
partial {{classHierarchy[i].GetTypeKeyword()}} {{classHierarchy[i].QualifiedName}}
{
""");
}

source.AppendLine($$"""
partial class {{(classHierarchy.IsEmpty ? bindableCustomProperties.ClassName : classHierarchy[0].QualifiedName)}} : global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation
{
global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(string name)
{
""");

foreach (var property in bindableCustomProperties.Properties.Where(p => !p.IsIndexer))
{
var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)""";

source.AppendLine($$"""
if (name == "{{property.Name}}")
{
return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty(
{{GetBoolAsString(property.CanRead)}},
{{GetBoolAsString(property.CanWrite)}},
"{{property.Name}}",
typeof({{property.Type}}),
{{ (property.CanRead ? $$"""static (instance) => {{instanceAccessor}}.{{property.Name}}""" : "null") }},
{{ (property.CanWrite ? $$"""static (instance, value) => {{instanceAccessor}}.{{property.Name}} = ({{property.Type}})value""" : "null") }},
null,
null);
}
""");
}

source.AppendLine($$"""
return default;
}

global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(global::System.Type indexParameterType)
{
""");

foreach (var property in bindableCustomProperties.Properties.Where(p => p.IsIndexer))
{
var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)""";

source.AppendLine($$"""
if (indexParameterType == typeof({{property.IndexerType}}))
{
return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty(
{{GetBoolAsString(property.CanRead)}},
{{GetBoolAsString(property.CanWrite)}},
"{{property.Name}}",
typeof({{property.Type}}),
null,
null,
{{ (property.CanRead ? $$"""static (instance, index) => {{instanceAccessor}}[({{property.IndexerType}})index]""" : "null") }},
{{ (property.CanWrite ? $$"""static (instance, value, index) => {{instanceAccessor}}[({{property.IndexerType}})index] = ({{property.Type}})value""" : "null") }});
}
""");
}

source.AppendLine($$"""
return default;
}
}
""");

// Close all brackets
for (int i = classHierarchy.Length - 1; i > 0; i--)
{
source.AppendLine("}");
}

if (!bindableCustomProperties.IsGlobalNamespace)
{
source.AppendLine($@"}}");
}

source.AppendLine();
}

sourceProductionContext.AddSource("WinRTCustomBindableProperties.g.cs", source.ToString());

static string GetBoolAsString(bool value) => value ? "true" : "false";
}
}

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

internal readonly record struct BindableCustomProperty(
string Name,
string Type,
bool CanRead,
bool CanWrite,
bool IsIndexer,
string IndexerType,
bool IsStatic);

internal readonly record struct BindableCustomProperties(
string Namespace,
bool IsGlobalNamespace,
string ClassName,
EquatableArray<TypeInfo> ClassHierarchy,
string QualifiedClassName,
EquatableArray<BindableCustomProperty> Properties);

/// <summary>
/// A model describing a type info in a type hierarchy.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Authoring/WinRT.SourceGenerator/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,27 @@ public static bool IsWinRTType(MemberDeclarationSyntax node)
return isProjectedType;
}

public static bool HasBindableCustomPropertyAttribute(MemberDeclarationSyntax node)
{
return node.AttributeLists.SelectMany(list => list.Attributes).Any(IsBindableCustomPropertyAttribute);

// Check based on identifier name if this is the BindableCustomProperty attribute.
// Technically this can be a different namespace, but we will confirm later once
// we have access to the semantic model.
static bool IsBindableCustomPropertyAttribute(AttributeSyntax attribute)
{
var nameSyntax = attribute.Name;
if (nameSyntax is QualifiedNameSyntax qualifiedName)
{
// Right would have the attribute while left is the namespace.
nameSyntax = qualifiedName.Right;
}

return nameSyntax is IdentifierNameSyntax name &&
name.Identifier.ValueText == "BindableCustomProperty";
}
}

/// <summary>
/// Checks whether or not a given symbol has an attribute with the specified type.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Tests/AuthoringTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry
public string Value => "CsWinRT";
}

[BindableCustomProperty]
public sealed partial class CustomProperty
{
public int Number { get; } = 4;
public string Value => "CsWinRT";
}

[Version(3u)]
public interface IDouble
{
Expand Down
Loading
Loading