From 21c10482e28365f43f8047a423fbc000bef0a2e9 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Saenz Date: Sun, 26 Jan 2025 09:15:14 -0500 Subject: [PATCH 1/5] [RGen] Update the transformer generator to access the attr data. Add new code that will generate a 2 methods for each BindingAttribute marked AttributeName. A method that will allow to check if the attribute data is present and a method that allows to retrieve the data. The new generated code will look liek this https://gist.github.com/mandel-macaque/7ffb4b215cb5856d91fd391393564170 --- .../AttributeType.cs | 10 + .../Attributes/BindingAttributeData.cs | 3 +- .../Attributes/BindingFlagData.cs | 2 +- .../XamarinBindingAPIGenerator.cs | 191 ++++++++++++------ 4 files changed, 147 insertions(+), 59 deletions(-) create mode 100644 src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/AttributeType.cs diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/AttributeType.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/AttributeType.cs new file mode 100644 index 000000000000..9548f874b006 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/AttributeType.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Macios.Transformer.Generator; + +public enum AttributeType { + None, + Flag, + Data, +} diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingAttributeData.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingAttributeData.cs index f89f5a63d28f..c2c5ea00819a 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingAttributeData.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingAttributeData.cs @@ -9,6 +9,7 @@ namespace Microsoft.Macios.Transformer.Generator.Attributes; readonly struct BindingAttributeData { + public static string Name = "BindingAttributeAttribute"; public static string Source = @"// using System; @@ -54,7 +55,7 @@ public static bool TryParse (AttributeData attributeData, break; case 2: baseType = ((INamedTypeSymbol) attributeData.ConstructorArguments [0].Value!).ToDisplayString (); - target = (AttributeTargets) attributeData.ConstructorArguments [0].Value!; + target = (AttributeTargets) attributeData.ConstructorArguments [1].Value!; break; default: // 0 should not be an option.. diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingFlagData.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingFlagData.cs index 061f3d7d8bfc..8419ac643398 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingFlagData.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/Attributes/BindingFlagData.cs @@ -11,7 +11,7 @@ namespace Microsoft.Macios.Transformer.Generator.Attributes; /// Struct representing the data of a Binding attribute. /// readonly struct BindingFlagData { - + public static string Name = "BindingFlagAttribute"; public static string Source = @"// using System; diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs index 0ee7cdc7a87b..49091179d949 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs @@ -20,19 +20,6 @@ namespace Microsoft.Macios.Transformer.Generator; [Generator] public class XamarinBindingAPIGenerator : IIncrementalGenerator { const string Namespace = "Microsoft.Macios.Transformer.Generator"; - const string BindingFlagAttributeName = "BindingFlagAttribute"; - - const string BindingAttributeAttributeSourceCode = @"// -using System; - -namespace Microsoft.Macios.Transformer.Generator; - -[AttributeUsage(AttributeTargets.Field)] -public class BindingAttributeAttribute(Type dataModelType, AttributeTargets target = AttributeTargets.All) : System.Attribute { - public AttributeTargets Target { get; } = target; - public Type DataModelType { get; } = dataModelType; -} -"; public void Initialize (IncrementalGeneratorInitializationContext context) { @@ -50,35 +37,37 @@ public void Initialize (IncrementalGeneratorInitializationContext context) .CreateSyntaxProvider ( (s, _) => s is FieldDeclarationSyntax, (ctx, _) => GetClassDeclarationForSourceGen (ctx)) - .Where (t => t.reportAttributeFound) - .Select ((t, _) => t.Item1); + .Where (t => t.attrType != AttributeType.None); // Generate the source code. context.RegisterSourceOutput (context.CompilationProvider.Combine (provider.Collect ()), ((ctx, t) => GenerateCode (ctx, t.Left, t.Right))); } - static (FieldDeclarationSyntax, bool reportAttributeFound) GetClassDeclarationForSourceGen ( + static (FieldDeclarationSyntax, AttributeType attrType) GetClassDeclarationForSourceGen ( GeneratorSyntaxContext context) { var declarationSyntax = Unsafe.As (context.Node); // Go through all attributes of the field - foreach (AttributeListSyntax attributeListSyntax in declarationSyntax.AttributeLists) + foreach (AttributeListSyntax attributeListSyntax in declarationSyntax.AttributeLists) { foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) { if (context.SemanticModel.GetSymbolInfo (attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) continue; // if we can't get the symbol, ignore it string attributeName = attributeSymbol.ContainingType.ToDisplayString (); - if (attributeName == $"{Namespace}.{BindingFlagAttributeName}") - return (declarationSyntax, true); + if (attributeName == $"{Namespace}.{BindingFlagData.Name}") + return (declarationSyntax, AttributeType.Flag); + if (attributeName == $"{Namespace}.{BindingAttributeData.Name}") + return (declarationSyntax, AttributeType.Data); } + } - return (declarationSyntax, false); + return (declarationSyntax, AttributeType.None); } - static string [] GetFlagsForTarget (Dictionary flags, + static string [] GetFlagsForTarget (Dictionary flags, AttributeTargets targets) => flags.Where (kv => kv.Value.Targets.HasFlag (targets)) .Select (kv => kv.Key) @@ -117,7 +106,9 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s // property to store the dictionary modelBlock.AppendLine (); modelBlock.AppendLine ("readonly Dictionary>? _attributesDictionary = null;"); - using (var dictionaryPropertyBlock = modelBlock.CreateBlock ("public Dictionary>? AttributesDictionary", block: true)) { + using (var dictionaryPropertyBlock = + modelBlock.CreateBlock ("public Dictionary>? AttributesDictionary", + block: true)) { dictionaryPropertyBlock.AppendLine ("get => _attributesDictionary;"); using (var initBlock = dictionaryPropertyBlock.CreateBlock ("private init", block: true)) { initBlock.AppendLine ("_attributesDictionary = value;"); @@ -132,7 +123,7 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s } static void GenerateModelExtension (TabbedStringBuilder sb, string dataModel, - Dictionary flags, AttributeTargets targets, + Dictionary flags, AttributeTargets targets, SourceProductionContext context) { var methodFlags = GetFlagsForTarget (flags, targets); @@ -146,37 +137,81 @@ static AttributeTargets GetTarget (ISymbol symbol) var attrData = symbol.GetAttributes (); // loop over attrs, if we find the BindingFlagAttribute, return the target foreach (var attr in attrData) { - if (attr.AttributeClass?.Name == BindingFlagAttributeName - && BindingFlagData.TryParse (attr, out var data)) { + if (attr.AttributeClass?.Name == BindingFlagData.Name + && BindingFlagData.TryParse (attr, out var data)) { return data.Value.Target; } } + return AttributeTargets.All; } + static BindingAttributeData? GetAttributeData (ISymbol symbol) + { + var attrData = symbol.GetAttributes (); + // loop over attrs, if we find the BindingFlagAttribute, return the target + foreach (var attr in attrData) { + if (attr.AttributeClass?.Name == BindingAttributeData.Name + && BindingAttributeData.TryParse (attr, out var data)) { + return data; + } + } + + return null; + } + void GenerateCode (SourceProductionContext context, Compilation compilation, - ImmutableArray declarations) + ImmutableArray<(FieldDeclarationSyntax Field, AttributeType Type)> declarations) { - // Go through all the fields that have the [BindingFlagAttribute] attribute, get the class name - // and field name. - var flags = new Dictionary (); - foreach (var fieldDeclarationSyntax in declarations) { + // loop over the fields and create two dictionaries, one for flags and one for data + var flags = new Dictionary (); + var dataAttributes = + new Dictionary (); + + foreach (var (fieldDeclarationSyntax, attrType) in declarations) { // We need to get semantic model of the class to retrieve metadata. var semanticModel = compilation.GetSemanticModel (fieldDeclarationSyntax.SyntaxTree); - foreach (var variableSyntax in fieldDeclarationSyntax.Declaration.Variables) { - // get the symbol to retrieve the data - if (semanticModel.GetDeclaredSymbol (variableSyntax) is not IFieldSymbol symbol) - continue; - var flagName = $"Has{symbol.Name.Replace ("Attribute", "Flag")}"; - var attrName = symbol.ToDisplayString ().Trim (); - var target = GetTarget (symbol); - flags [flagName] = (attrName, target); + switch (attrType) { + // the attr type will let use know what data can be retrieved from the field + case AttributeType.Flag: + GetFlagsFromField (fieldDeclarationSyntax, semanticModel, flags); + break; + case AttributeType.Data: + GetAttributesFromField (fieldDeclarationSyntax, semanticModel, dataAttributes); + break; } } // all flags are collected, generate the code - var sb = new TabbedStringBuilder (new ()); + var sb = new TabbedStringBuilder (new()); + GenerateDictionaryExtension (sb, flags, dataAttributes); + + // Add the source code to the compilation. + context.AddSource ("AttributeDataDictionaryExtensions.g.cs", + SourceText.From (sb.ToString (), Encoding.UTF8)); + +#pragma warning disable format + // generate the extra methods for the data model, group the fields by the model type based on the target + var models = new (string Model, AttributeTargets Target) [] { + ("EnumMember", AttributeTargets.Field), + ("Parameter", AttributeTargets.Parameter), + ("Property", AttributeTargets.Property), + ("Method", AttributeTargets.Method), + ("Binding", AttributeTargets.Interface), + ("TypeInfo", AttributeTargets.Parameter) + }; +#pragma warning restore format + + foreach (var (model, target) in models) { + GenerateModelExtension (sb, model, flags, target, context); + } + } + + static void GenerateDictionaryExtension (TabbedStringBuilder sb, + Dictionary flags, + Dictionary dataAttributes) + { sb.AppendLine ("// "); sb.AppendLine ("#nullable enable"); sb.AppendLine ("using System;"); @@ -189,12 +224,41 @@ void GenerateCode (SourceProductionContext context, Compilation compilation, // loop over the flags and generate a helper static method to retrieve it from a attribute data dict foreach (var (methodName, attributeName) in flags) { using (var methodBlock = classBlock.CreateBlock ( - $"public static bool {methodName} (this Dictionary> self)", - block: true)) { - methodBlock.AppendLine ($"return self.ContainsKey ({attributeName.AttributeName});"); + $"public static bool {methodName} (this Dictionary> self)", + block: true)) { + methodBlock.AppendLine ($"return self.ContainsKey ({attributeName.AttributeFullName});"); + } + + classBlock.AppendLine (); + } + + // same operation over the data attributes + foreach (var (methodName, attributeInfo) in dataAttributes) { + // property to check if the attribute is present + using (var methodBlock = classBlock.CreateBlock ( + $"public static bool {methodName} (this Dictionary> self)", + block: true)) { + methodBlock.AppendLine ($"return self.ContainsKey ({attributeInfo.AttributeFullName});"); + } + + classBlock.AppendLine (); + // property to access the attribute + using (var methodBlock = classBlock.CreateBlock ( + $"public static {attributeInfo.Data.DataModelType}? Get{attributeInfo.AttributeName} (this Dictionary> self)", + block: true)) { + methodBlock.AppendRaw ( +$@"if (self.{methodName} ()) {{ + var data = self.GetAttribute<{attributeInfo.Data.DataModelType}> ({attributeInfo.AttributeFullName}, {attributeInfo.Data.DataModelType}.TryParse); + return data; +}} else {{ + return null; +}} +"); } + classBlock.AppendLine (); } + // add a generic method that will allow use to retrieve an attribute type classBlock.AppendRaw ( @"public delegate bool TryParseDelegate (AttributeData attributeData, [NotNullWhen (true)] out T? data) where T : struct; @@ -206,29 +270,42 @@ void GenerateCode (SourceProductionContext context, Compilation compilation, foreach (var attr in attrs) { if (tryParse (attr, out var data)) - return data; + return data.Value; } return null; } "); } + } - // Add the source code to the compilation. - context.AddSource ("AttributeDataDictionaryExtensions.g.cs", - SourceText.From (sb.ToString (), Encoding.UTF8)); + static void GetAttributesFromField (FieldDeclarationSyntax fieldDeclarationSyntax, SemanticModel semanticModel, + Dictionary dataAttributes) + { + foreach (var variableSyntax in fieldDeclarationSyntax.Declaration.Variables) { + // get the symbol to retrieve the data + if (semanticModel.GetDeclaredSymbol (variableSyntax) is not IFieldSymbol symbol) + continue; + var flagName = $"Has{symbol.Name}"; + var attributeFullName = symbol.ToDisplayString ().Trim (); + var attrData = GetAttributeData (symbol); + if (attrData is not null) { + dataAttributes [flagName] = (attributeFullName, symbol.Name, attrData.Value); + } + } + } - // generate the extra methods for the data model, group the fields by the model type based on the target - var models = new (string Model, AttributeTargets Target) [] { - ("EnumMember", AttributeTargets.Field), - ("Parameter", AttributeTargets.Parameter), - ("Property", AttributeTargets.Property), - ("Method", AttributeTargets.Method), - ("Binding", AttributeTargets.Interface), - ("TypeInfo", AttributeTargets.Parameter) - }; - foreach (var (model, target) in models) { - GenerateModelExtension (sb, model, flags, target, context); + static void GetFlagsFromField (FieldDeclarationSyntax fieldDeclarationSyntax, SemanticModel semanticModel, + Dictionary flags) + { + foreach (var variableSyntax in fieldDeclarationSyntax.Declaration.Variables) { + // get the symbol to retrieve the data + if (semanticModel.GetDeclaredSymbol (variableSyntax) is not IFieldSymbol symbol) + continue; + var flagName = $"Has{symbol.Name.Replace ("Attribute", "Flag")}"; + var attrName = symbol.ToDisplayString ().Trim (); + var target = GetTarget (symbol); + flags [flagName] = (attrName, target); } } } From a083d732aebeca42a2b2f50448a590c87d11faa4 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Saenz Date: Sun, 26 Jan 2025 12:43:56 -0500 Subject: [PATCH 2/5] [RGen] Add the code generation for the data attributes. This change adds two more properties to the data models when they are compile. One to check if the data attr is present, and a second one with the attribute details. The boolean property is decorated acordingly to help with the nullability analisys. An example of the generated code can be found here: https://gist.github.com/mandel-macaque/04185ea481ca30ef9ff2ecf6dbf51ea0 --- .../XamarinBindingAPIGenerator.cs | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs index 49091179d949..a042699c671a 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs @@ -73,6 +73,14 @@ static string [] GetFlagsForTarget (Dictionary kv.Key) .ToArray (); + static (string AttributeFullName, string AttributeName, BindingAttributeData Data)[] GetAttributesForTarget ( + Dictionary dataAttribute, + AttributeTargets targets) + => dataAttribute.Where (kv => kv.Value.Data.Target.HasFlag (targets)) + .Select (kv => kv.Value) + .ToArray (); + + static void WriteFlagProperty (TabbedStringBuilder sb, string flagName) { // write the backing field @@ -83,13 +91,34 @@ static void WriteFlagProperty (TabbedStringBuilder sb, string flagName) } } - static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, string [] flags) + static void WriteAttributeProperty (TabbedStringBuilder sb, + (string AttributeFullName, string AttributeName, BindingAttributeData Data) attrData) + { + // add a property that will state if we have the attr, this will help with nullability + sb.AppendLine($"readonly bool _has{attrData.AttributeName} = false;"); + sb.AppendLine ($"[MemberNotNullWhen (true, nameof ({attrData.AttributeName}))]"); + using (var flagPropertyBlock = sb.CreateBlock ($"public bool Has{attrData.AttributeName}", block: true)) { + flagPropertyBlock.AppendLine ($"get => _has{attrData.AttributeName};"); + flagPropertyBlock.AppendLine ($"private init => _has{attrData.AttributeName} = value;"); + } + sb.AppendLine (); + sb.AppendLine($"readonly {attrData.Data.DataModelType}? _{attrData.AttributeName} = null;"); + // decorate to help with nullability + using (var attributePropertyBlock = sb.CreateBlock ($"public {attrData.Data.DataModelType}? {attrData.AttributeName}", block: true)) { + attributePropertyBlock.AppendLine ($"get => _{attrData.AttributeName};"); + attributePropertyBlock.AppendLine ($"private init => _{attrData.AttributeName} = value;"); + } + } + + static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, string [] flags, + (string AttributeFullName, string AttributeName, BindingAttributeData Data)[] attributes) { sb.Clear (); sb.AppendLine ("// "); sb.AppendLine ("#nullable enable"); sb.AppendLine (""); sb.AppendLine ("using System;"); + sb.AppendLine ("using System.Diagnostics.CodeAnalysis;"); sb.AppendLine ("using Microsoft.CodeAnalysis;"); sb.AppendLine ("using Microsoft.Macios.Transformer.Extensions;"); sb.AppendLine (); @@ -103,6 +132,11 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s WriteFlagProperty (modelBlock, flag); } + foreach (var attrData in attributes) { + modelBlock.AppendLine (); + WriteAttributeProperty (modelBlock, attrData); + } + // property to store the dictionary modelBlock.AppendLine (); modelBlock.AppendLine ("readonly Dictionary>? _attributesDictionary = null;"); @@ -114,7 +148,15 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s initBlock.AppendLine ("_attributesDictionary = value;"); using (var ifBlock = initBlock.CreateBlock ("if (_attributesDictionary is not null)", block: true)) { foreach (var flag in flags) { - ifBlock.AppendLine ($"_{flag} = _attributesDictionary.{flag} ();"); + ifBlock.AppendLine ($"{flag} = _attributesDictionary.{flag} ();"); + } + + foreach (var attributeData in attributes) { + // check if the attribute is present, if it is, set the value + ifBlock.AppendLine($"Has{attributeData.AttributeName} = _attributesDictionary.Has{attributeData.AttributeName} ();"); + using (var attrIfBlock = ifBlock.CreateBlock ($"if (Has{attributeData.AttributeName})", block: true)) { + attrIfBlock.AppendLine ($"{attributeData.AttributeName} = _attributesDictionary.Get{attributeData.AttributeName} ();"); + } } } } @@ -123,11 +165,14 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s } static void GenerateModelExtension (TabbedStringBuilder sb, string dataModel, - Dictionary flags, AttributeTargets targets, + Dictionary flags, + Dictionary attributes, + AttributeTargets targets, SourceProductionContext context) { var methodFlags = GetFlagsForTarget (flags, targets); - WriteDataModelExtension (sb, dataModel, methodFlags); + var methodAttributes = GetAttributesForTarget (attributes, targets); + WriteDataModelExtension (sb, dataModel, methodFlags, methodAttributes); context.AddSource ($"{dataModel}.Transformer.g.cs", SourceText.From (sb.ToString (), Encoding.UTF8)); } @@ -204,7 +249,7 @@ void GenerateCode (SourceProductionContext context, Compilation compilation, #pragma warning restore format foreach (var (model, target) in models) { - GenerateModelExtension (sb, model, flags, target, context); + GenerateModelExtension (sb, model, flags, dataAttributes, target, context); } } From 010103d3bdfbc81dc9da97308e7a670ca458fbcf Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Saenz Date: Sun, 26 Jan 2025 18:31:48 -0500 Subject: [PATCH 3/5] [Rgen] Fix a bug in how the target flags are managed. We cannot use an or a & we need to provide a list, that leater is tested against a flag that has been either | or &. --- .../XamarinBindingAPIGenerator.cs | 27 ++++++++++--------- .../AttributesNames.cs | 2 ++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs index a042699c671a..95a7cb6f37ba 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs @@ -68,15 +68,16 @@ public void Initialize (IncrementalGeneratorInitializationContext context) } static string [] GetFlagsForTarget (Dictionary flags, - AttributeTargets targets) - => flags.Where (kv => kv.Value.Targets.HasFlag (targets)) + AttributeTargets[] targets) + => flags.Where (kv => targets.Any (t => kv.Value.Targets.HasFlag (t))) .Select (kv => kv.Key) .ToArray (); - static (string AttributeFullName, string AttributeName, BindingAttributeData Data)[] GetAttributesForTarget ( + static (string AttributeFullName, string AttributeName, BindingAttributeData Data) [] GetAttributesForTarget ( Dictionary dataAttribute, - AttributeTargets targets) - => dataAttribute.Where (kv => kv.Value.Data.Target.HasFlag (targets)) + AttributeTargets [] targets) + // return all the attributes that have at least one of the targets + => dataAttribute.Where (kv => targets.Any (t => kv.Value.Data.Target.HasFlag (t))) .Select (kv => kv.Value) .ToArray (); @@ -167,7 +168,7 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s static void GenerateModelExtension (TabbedStringBuilder sb, string dataModel, Dictionary flags, Dictionary attributes, - AttributeTargets targets, + AttributeTargets[] targets, SourceProductionContext context) { var methodFlags = GetFlagsForTarget (flags, targets); @@ -238,13 +239,13 @@ void GenerateCode (SourceProductionContext context, Compilation compilation, #pragma warning disable format // generate the extra methods for the data model, group the fields by the model type based on the target - var models = new (string Model, AttributeTargets Target) [] { - ("EnumMember", AttributeTargets.Field), - ("Parameter", AttributeTargets.Parameter), - ("Property", AttributeTargets.Property), - ("Method", AttributeTargets.Method), - ("Binding", AttributeTargets.Interface), - ("TypeInfo", AttributeTargets.Parameter) + var models = new (string Model, AttributeTargets[] Targets) [] { + ("EnumMember", [AttributeTargets.Field]), + ("Parameter", [AttributeTargets.Parameter]), + ("Property", [AttributeTargets.Property]), + ("Method", [AttributeTargets.Method]), + ("Binding", [AttributeTargets.Interface, AttributeTargets.Class, AttributeTargets.Enum, AttributeTargets.Struct]), + ("TypeInfo", [AttributeTargets.Parameter]) }; #pragma warning restore format diff --git a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs index a3ca39d41f3c..1d8a5186630b 100644 --- a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs +++ b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs @@ -145,7 +145,9 @@ static class AttributesNames { /// [BindingFlag (AttributeTargets.Class | AttributeTargets.Interface)] public const string ModelAttribute = "Foundation.ModelAttribute"; + [BindingAttribute(typeof(NativeData), AttributeTargets.Enum)] public const string NativeAttribute = "ObjCRuntime.NativeAttribute"; + [BindingAttribute(typeof(NativeData), AttributeTargets.Enum | AttributeTargets.Struct)] public const string NativeNameAttribute = "ObjCRuntime.NativeNameAttribute"; /// From 71ed12c8c22aa8047142373a84f245648ee608d6 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Saenz Date: Sun, 26 Jan 2025 19:54:32 -0500 Subject: [PATCH 4/5] [Rgen] Add parsing of the ThreadSafe attr to the transformer. --- .../Attributes/ThreadSafeData.cs | 68 ++++++++++++++ .../AttributesNames.cs | 5 + .../Attributes/ThreadSafeDataTests.cs | 92 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs create mode 100644 tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs diff --git a/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs b/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs new file mode 100644 index 000000000000..3c42045a7c08 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Transformer.Attributes; + +readonly struct ThreadSafeData : IEquatable { + + public bool Safe { get; } = true; + + public ThreadSafeData () : this (true) {} + + public ThreadSafeData (bool safe) + { + Safe = safe; + } + + public static bool TryParse (AttributeData attributeData, + [NotNullWhen (true)] out ThreadSafeData? data) + { + data = null; + var count = attributeData.ConstructorArguments.Length; + if (count == 0) { + data = new(); + return true; + } + bool safe = true; + switch (count) { + case 1: + safe = (bool) attributeData.ConstructorArguments [0].Value!; + break; + default: + // 0 should not be an option.. + return false; + } + + data = new ThreadSafeData (safe); + return true; + } + + public bool Equals (ThreadSafeData other) + => Safe == other.Safe; + + /// + public override bool Equals (object? obj) + { + return obj is ThreadSafeData other && Equals (other); + } + + /// + public override int GetHashCode () + => HashCode.Combine (Safe); + + public static bool operator == (ThreadSafeData x, ThreadSafeData y) + { + return x.Equals (y); + } + + public static bool operator != (ThreadSafeData x, ThreadSafeData y) + { + return !(x == y); + } + + public override string ToString () + => $"{{ ThreadSafe: {Safe} }}"; +} diff --git a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs index 1d8a5186630b..0fb39f6624b1 100644 --- a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs +++ b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs @@ -279,6 +279,11 @@ static class AttributesNames { /// [BindingFlag (AttributeTargets.Class | AttributeTargets.Interface)] public const string TargetAttribute = "TargetAttribute"; + + /// + /// Flags the object as being thread safe. + /// + [BindingAttribute(typeof(BackingFieldTypeData), AttributeTargets.All)] public const string ThreadSafeAttribute = "ThreadSafeAttribute"; /// diff --git a/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs new file mode 100644 index 000000000000..768d5d1865a9 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Extensions; +using Microsoft.Macios.Transformer.Attributes; +using Xamarin.Tests; +using Xamarin.Utils; + +namespace Microsoft.Macios.Transformer.Tests.Attributes; + +public class ThreadSafeDataTests : BaseTransformerTestClass { + + class TestDataTryCreate : IEnumerable { + public IEnumerator GetEnumerator () + { + var path = "/some/random/path.cs"; + + const string threadSageMethod = @" +using System; +using Foundation; +using ObjCRuntime; +using UIKit; + +namespace Test; + +[NoTV] +[MacCatalyst (13, 1)] +[DisableDefaultCtor] +[Abstract] +[BaseType (typeof (NSObject))] +interface UIFeedbackGenerator : UIInteraction { + + [Export (""prepare""), ThreadSafe] + void Prepare (); +} +"; + + yield return [(Source: threadSageMethod, Path: path), new ThreadSafeData()]; + + const string notThreadSafeMethod = @" +using System; +using Foundation; +using ObjCRuntime; +using UIKit; + +namespace Test; + +[NoTV] +[MacCatalyst (13, 1)] +[DisableDefaultCtor] +[Abstract] +[BaseType (typeof (NSObject))] +interface UIFeedbackGenerator : UIInteraction { + + [Export (""prepare""), ThreadSafe (false)] + void Prepare (); +} +"; + + yield return [(Source: notThreadSafeMethod, Path: path), new ThreadSafeData(false)]; + + } + + IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); + } + + [Theory] + [AllSupportedPlatformsClassData] + void TryCreateTests (ApplePlatform platform, (string Source, string Path) source, ThreadSafeData expectedData) + { + // create a compilation used to create the transformer + var compilation = CreateCompilation (platform, sources: source); + var syntaxTree = compilation.SyntaxTrees.ForSource (source); + Assert.NotNull (syntaxTree); + + var semanticModel = compilation.GetSemanticModel (syntaxTree); + Assert.NotNull (semanticModel); + + var declaration = syntaxTree.GetRoot () + .DescendantNodes ().OfType () + .FirstOrDefault (); + Assert.NotNull (declaration); + + var symbol = semanticModel.GetDeclaredSymbol (declaration); + Assert.NotNull (symbol); + var attribute = symbol.GetAttribute (AttributesNames.ThreadSafeAttribute, ThreadSafeData.TryParse); + Assert.Equal (expectedData, attribute); + } +} From 624397dfb3843f9e7d5dd9185d32d08cfe91aba1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Mon, 27 Jan 2025 00:59:42 +0000 Subject: [PATCH 5/5] Auto-format source code --- .../XamarinBindingAPIGenerator.cs | 44 +++++++++---------- .../Attributes/ThreadSafeData.cs | 10 ++--- .../Attributes/ThreadSafeDataTests.cs | 10 ++--- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs index 95a7cb6f37ba..72cff4800382 100644 --- a/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs +++ b/src/rgen/Microsoft.Macios.Transformer.Generator/Microsoft.Macios.Transformer.Generator/XamarinBindingAPIGenerator.cs @@ -68,7 +68,7 @@ public void Initialize (IncrementalGeneratorInitializationContext context) } static string [] GetFlagsForTarget (Dictionary flags, - AttributeTargets[] targets) + AttributeTargets [] targets) => flags.Where (kv => targets.Any (t => kv.Value.Targets.HasFlag (t))) .Select (kv => kv.Key) .ToArray (); @@ -80,7 +80,7 @@ static string [] GetFlagsForTarget (Dictionary dataAttribute.Where (kv => targets.Any (t => kv.Value.Data.Target.HasFlag (t))) .Select (kv => kv.Value) .ToArray (); - + static void WriteFlagProperty (TabbedStringBuilder sb, string flagName) { @@ -96,14 +96,14 @@ static void WriteAttributeProperty (TabbedStringBuilder sb, (string AttributeFullName, string AttributeName, BindingAttributeData Data) attrData) { // add a property that will state if we have the attr, this will help with nullability - sb.AppendLine($"readonly bool _has{attrData.AttributeName} = false;"); + sb.AppendLine ($"readonly bool _has{attrData.AttributeName} = false;"); sb.AppendLine ($"[MemberNotNullWhen (true, nameof ({attrData.AttributeName}))]"); using (var flagPropertyBlock = sb.CreateBlock ($"public bool Has{attrData.AttributeName}", block: true)) { flagPropertyBlock.AppendLine ($"get => _has{attrData.AttributeName};"); flagPropertyBlock.AppendLine ($"private init => _has{attrData.AttributeName} = value;"); } - sb.AppendLine (); - sb.AppendLine($"readonly {attrData.Data.DataModelType}? _{attrData.AttributeName} = null;"); + sb.AppendLine (); + sb.AppendLine ($"readonly {attrData.Data.DataModelType}? _{attrData.AttributeName} = null;"); // decorate to help with nullability using (var attributePropertyBlock = sb.CreateBlock ($"public {attrData.Data.DataModelType}? {attrData.AttributeName}", block: true)) { attributePropertyBlock.AppendLine ($"get => _{attrData.AttributeName};"); @@ -111,8 +111,8 @@ static void WriteAttributeProperty (TabbedStringBuilder sb, } } - static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, string [] flags, - (string AttributeFullName, string AttributeName, BindingAttributeData Data)[] attributes) + static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, string [] flags, + (string AttributeFullName, string AttributeName, BindingAttributeData Data) [] attributes) { sb.Clear (); sb.AppendLine ("// "); @@ -142,8 +142,8 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s modelBlock.AppendLine (); modelBlock.AppendLine ("readonly Dictionary>? _attributesDictionary = null;"); using (var dictionaryPropertyBlock = - modelBlock.CreateBlock ("public Dictionary>? AttributesDictionary", - block: true)) { + modelBlock.CreateBlock ("public Dictionary>? AttributesDictionary", + block: true)) { dictionaryPropertyBlock.AppendLine ("get => _attributesDictionary;"); using (var initBlock = dictionaryPropertyBlock.CreateBlock ("private init", block: true)) { initBlock.AppendLine ("_attributesDictionary = value;"); @@ -154,7 +154,7 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s foreach (var attributeData in attributes) { // check if the attribute is present, if it is, set the value - ifBlock.AppendLine($"Has{attributeData.AttributeName} = _attributesDictionary.Has{attributeData.AttributeName} ();"); + ifBlock.AppendLine ($"Has{attributeData.AttributeName} = _attributesDictionary.Has{attributeData.AttributeName} ();"); using (var attrIfBlock = ifBlock.CreateBlock ($"if (Has{attributeData.AttributeName})", block: true)) { attrIfBlock.AppendLine ($"{attributeData.AttributeName} = _attributesDictionary.Get{attributeData.AttributeName} ();"); } @@ -166,9 +166,9 @@ static void WriteDataModelExtension (TabbedStringBuilder sb, string dataModel, s } static void GenerateModelExtension (TabbedStringBuilder sb, string dataModel, - Dictionary flags, - Dictionary attributes, - AttributeTargets[] targets, + Dictionary flags, + Dictionary attributes, + AttributeTargets [] targets, SourceProductionContext context) { var methodFlags = GetFlagsForTarget (flags, targets); @@ -184,7 +184,7 @@ static AttributeTargets GetTarget (ISymbol symbol) // loop over attrs, if we find the BindingFlagAttribute, return the target foreach (var attr in attrData) { if (attr.AttributeClass?.Name == BindingFlagData.Name - && BindingFlagData.TryParse (attr, out var data)) { + && BindingFlagData.TryParse (attr, out var data)) { return data.Value.Target; } } @@ -198,7 +198,7 @@ static AttributeTargets GetTarget (ISymbol symbol) // loop over attrs, if we find the BindingFlagAttribute, return the target foreach (var attr in attrData) { if (attr.AttributeClass?.Name == BindingAttributeData.Name - && BindingAttributeData.TryParse (attr, out var data)) { + && BindingAttributeData.TryParse (attr, out var data)) { return data; } } @@ -230,7 +230,7 @@ void GenerateCode (SourceProductionContext context, Compilation compilation, } // all flags are collected, generate the code - var sb = new TabbedStringBuilder (new()); + var sb = new TabbedStringBuilder (new ()); GenerateDictionaryExtension (sb, flags, dataAttributes); // Add the source code to the compilation. @@ -270,8 +270,8 @@ static void GenerateDictionaryExtension (TabbedStringBuilder sb, // loop over the flags and generate a helper static method to retrieve it from a attribute data dict foreach (var (methodName, attributeName) in flags) { using (var methodBlock = classBlock.CreateBlock ( - $"public static bool {methodName} (this Dictionary> self)", - block: true)) { + $"public static bool {methodName} (this Dictionary> self)", + block: true)) { methodBlock.AppendLine ($"return self.ContainsKey ({attributeName.AttributeFullName});"); } @@ -282,16 +282,16 @@ static void GenerateDictionaryExtension (TabbedStringBuilder sb, foreach (var (methodName, attributeInfo) in dataAttributes) { // property to check if the attribute is present using (var methodBlock = classBlock.CreateBlock ( - $"public static bool {methodName} (this Dictionary> self)", - block: true)) { + $"public static bool {methodName} (this Dictionary> self)", + block: true)) { methodBlock.AppendLine ($"return self.ContainsKey ({attributeInfo.AttributeFullName});"); } classBlock.AppendLine (); // property to access the attribute using (var methodBlock = classBlock.CreateBlock ( - $"public static {attributeInfo.Data.DataModelType}? Get{attributeInfo.AttributeName} (this Dictionary> self)", - block: true)) { + $"public static {attributeInfo.Data.DataModelType}? Get{attributeInfo.AttributeName} (this Dictionary> self)", + block: true)) { methodBlock.AppendRaw ( $@"if (self.{methodName} ()) {{ var data = self.GetAttribute<{attributeInfo.Data.DataModelType}> ({attributeInfo.AttributeFullName}, {attributeInfo.Data.DataModelType}.TryParse); diff --git a/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs b/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs index 3c42045a7c08..4f7fdeb37e56 100644 --- a/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs +++ b/src/rgen/Microsoft.Macios.Transformer/Attributes/ThreadSafeData.cs @@ -7,11 +7,11 @@ namespace Microsoft.Macios.Transformer.Attributes; readonly struct ThreadSafeData : IEquatable { - + public bool Safe { get; } = true; - - public ThreadSafeData () : this (true) {} - + + public ThreadSafeData () : this (true) { } + public ThreadSafeData (bool safe) { Safe = safe; @@ -23,7 +23,7 @@ public static bool TryParse (AttributeData attributeData, data = null; var count = attributeData.ConstructorArguments.Length; if (count == 0) { - data = new(); + data = new (); return true; } bool safe = true; diff --git a/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs index 768d5d1865a9..87d9b004a77c 100644 --- a/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs +++ b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/ThreadSafeDataTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.Macios.Transformer.Tests.Attributes; public class ThreadSafeDataTests : BaseTransformerTestClass { - + class TestDataTryCreate : IEnumerable { public IEnumerator GetEnumerator () { @@ -37,8 +37,8 @@ interface UIFeedbackGenerator : UIInteraction { void Prepare (); } "; - - yield return [(Source: threadSageMethod, Path: path), new ThreadSafeData()]; + + yield return [(Source: threadSageMethod, Path: path), new ThreadSafeData ()]; const string notThreadSafeMethod = @" using System; @@ -59,8 +59,8 @@ interface UIFeedbackGenerator : UIInteraction { void Prepare (); } "; - - yield return [(Source: notThreadSafeMethod, Path: path), new ThreadSafeData(false)]; + + yield return [(Source: notThreadSafeMethod, Path: path), new ThreadSafeData (false)]; }