From a6332d49feec2a7bab4470ddcc060d9f7b389404 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Saenz Date: Tue, 28 Jan 2025 11:28:58 -0500 Subject: [PATCH] [Rgen] Add Async attr parsing to the transformer. --- .../Attributes/AsyncData.cs | 155 +++++++++++++++++ .../AttributesNames.cs | 3 + .../Attributes/AsyncDataTests.cs | 164 ++++++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 src/rgen/Microsoft.Macios.Transformer/Attributes/AsyncData.cs create mode 100644 tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/AsyncDataTests.cs diff --git a/src/rgen/Microsoft.Macios.Transformer/Attributes/AsyncData.cs b/src/rgen/Microsoft.Macios.Transformer/Attributes/AsyncData.cs new file mode 100644 index 00000000000..8c677bb727c --- /dev/null +++ b/src/rgen/Microsoft.Macios.Transformer/Attributes/AsyncData.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Transformer.Attributes; + +readonly struct AsyncData : IEquatable { + /// + /// Diff the constructor used in the bindings. + /// + internal enum ConstructorType { + ResultType, + MethodName + } + + public string? ResultType { get; init; } // this in the attr is a type, but we do not care for the transformation + public string? MethodName { get; init; } + public string? ResultTypeName { get; init; } + public string? PostNonResultSnippet { get; init; } + + public AsyncData () {} + + public AsyncData (string resultType, ConstructorType constructorType) + { + if (constructorType == ConstructorType.ResultType) + ResultType = resultType; + else + MethodName = resultType; + } + + public static bool TryParse (AttributeData attributeData, + [NotNullWhen (true)] out AsyncData? data) + { + data = null; + var count = attributeData.ConstructorArguments.Length; + ConstructorType constructorType = ConstructorType.MethodName; + string? resultType = null; + string? resultTypeName = null; + string? methodName = null; + string? postNonResultSnippet = null; + + switch (count) { + case 0: + break; + case 1: + // we have to diff constructors that take a single parameter, either a string or a type + if (attributeData.ConstructorArguments[0].Value! is string methodNameValue) { + constructorType = ConstructorType.MethodName; + methodName = methodNameValue; + } else { + constructorType = ConstructorType.ResultType; + resultType = ((INamedTypeSymbol) attributeData.ConstructorArguments[0].Value!).ToDisplayString(); + } + break; + default: + // 0 should not be an option.. + return false; + } + + if (attributeData.NamedArguments.Length == 0) { + if (constructorType == ConstructorType.ResultType) + data = new (resultType!, ConstructorType.ResultType); + else + data = new (methodName!, ConstructorType.MethodName); + return true; + } + + foreach (var (argumentName, value) in attributeData.NamedArguments) { + switch (argumentName) { + case "ResultType": + resultType = ((INamedTypeSymbol) value.Value!).ToDisplayString(); + break; + case "MethodName": + methodName = (string) value.Value!; + break; + case "ResultTypeName": + resultTypeName = (string) value.Value!; + break; + case "PostNonResultSnippet": + postNonResultSnippet = (string) value.Value!; + break; + default: + data = null; + return false; + } + } + + if (count == 0) { + // use the default constructor and use the init properties + data = new() { + ResultType = resultType, + MethodName = methodName, + ResultTypeName = resultTypeName, + PostNonResultSnippet = postNonResultSnippet + }; + return true; + } + + switch (constructorType) + { + case ConstructorType.MethodName: + data = new (methodName!, ConstructorType.MethodName) { + ResultType = resultType, + ResultTypeName = resultTypeName, + PostNonResultSnippet = postNonResultSnippet + }; + break; + case ConstructorType.ResultType: + data = new (resultType!, ConstructorType.ResultType) { + MethodName = methodName, + ResultTypeName = resultTypeName, + PostNonResultSnippet = postNonResultSnippet + }; + break; + } + + return false; + } + + public bool Equals (AsyncData other) + { + if (ResultType != other.ResultType) + return false; + if (MethodName != other.MethodName) + return false; + if (ResultTypeName != other.ResultTypeName) + return false; + return PostNonResultSnippet == other.PostNonResultSnippet; + } + + /// + public override bool Equals (object? obj) + { + return obj is AsyncData other && Equals (other); + } + + /// + public override int GetHashCode () + => HashCode.Combine (ResultType, MethodName, ResultTypeName, PostNonResultSnippet); + + public static bool operator == (AsyncData x, AsyncData y) + { + return x.Equals (y); + } + + public static bool operator != (AsyncData x, AsyncData y) + { + return !(x == y); + } + + public override string ToString () + => $"{{ ResultType: '{ResultType ?? "null"}', MethodName: '{MethodName ?? "null"}', ResultTypeName: '{ResultTypeName ?? "null"}', PostNonResultSnippet: '{PostNonResultSnippet ?? "null"}' }}"; +} diff --git a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs index afe3ff0f658..5c22a1fbf08 100644 --- a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs +++ b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs @@ -33,6 +33,9 @@ static class AttributesNames { /// [BindingFlag (AttributeTargets.Method | AttributeTargets.Property)] public const string AutoreleaseAttribute = "AutoreleaseAttribute"; + + [BindingAttribute(typeof(AsyncData), AttributeTargets.Method)] + public const string AsyncAttribute = "AsyncAttribute"; [BindingAttribute(typeof(BackingFieldTypeData), AttributeTargets.Enum)] public const string BackingFieldTypeAttribute = "BackingFieldTypeAttribute"; diff --git a/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/AsyncDataTests.cs b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/AsyncDataTests.cs new file mode 100644 index 00000000000..c8f8ba6ce1e --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/AsyncDataTests.cs @@ -0,0 +1,164 @@ +// 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 AsyncDataTests : BaseTransformerTestClass { + + class TestDataTryCreate : IEnumerable { + public IEnumerator GetEnumerator () + { + const string path = "/some/random/path.cs"; + + const string simpleAsyncMethod = @" +using System; +using AppKit; +using Foundation; +using ObjCRuntime; + +namespace Test; + +[NoMacCatalyst] +[BaseType (typeof (NSObject))] +[DisableDefaultCtor] +interface NSTableViewDiffableDataSource { + + [Export (""applySnapshot:animatingDifferences:completion:"")] + [Async] + void ApplySnapshot (NSObject snapshot, bool animatingDifferences, [NullAllowed] Action completion); +} +"; + yield return [(Source: simpleAsyncMethod, Path: path), new AsyncData ()]; + + const string asyncResultTypeName = @" +using System; +using AppKit; +using Foundation; +using ObjCRuntime; + +namespace Test; + +[NoMacCatalyst] +[BaseType (typeof (NSObject))] +[DisableDefaultCtor] +interface NSTableViewDiffableDataSource { + + [Export (""applySnapshot:animatingDifferences:completion:"")] + [Async (ResultTypeName=""NSSpellCheckerCandidates"")] + void ApplySnapshot (NSObject snapshot, bool animatingDifferences, [NullAllowed] Action completion); +} +"; + + yield return [(Source: asyncResultTypeName, Path: path), new AsyncData { + ResultTypeName = "NSSpellCheckerCandidates" + }]; + + const string asyncMethodName = @" +using System; +using AppKit; +using Foundation; +using ObjCRuntime; + +namespace Test; + +[NoMacCatalyst] +[BaseType (typeof (NSObject))] +[DisableDefaultCtor] +interface NSTableViewDiffableDataSource { + + [Export (""applySnapshot:animatingDifferences:completion:"")] + [Async (""ApplyTheSnapshotAsync"")] + void ApplySnapshot (NSObject snapshot, bool animatingDifferences, [NullAllowed] Action completion); +} +"; + + yield return [(Source: asyncMethodName, Path: path), new AsyncData { + MethodName = "ApplyTheSnapshotAsync" + }]; + + const string asyncTypeOf = @" +using System; +using AppKit; +using Foundation; +using ObjCRuntime; + +namespace Test; + +public class SampleResult {} + +[NoMacCatalyst] +[BaseType (typeof (NSObject))] +[DisableDefaultCtor] +interface NSTableViewDiffableDataSource { + + [Export (""applySnapshot:animatingDifferences:completion:"")] + [Async (ResultType = typeof (SampleResult))] + void ApplySnapshot (NSObject snapshot, bool animatingDifferences, [NullAllowed] Action completion); +} +"; + + yield return [(Source: asyncTypeOf, Path: path), new AsyncData { + ResultType = "Test.SampleResult" + }]; + + const string postResult = @" +using System; +using AppKit; +using Foundation; +using ObjCRuntime; + +namespace Test; + +public class SampleResult {} + +[NoMacCatalyst] +[BaseType (typeof (NSObject))] +[DisableDefaultCtor] +interface NSTableViewDiffableDataSource { + + [Export (""applySnapshot:animatingDifferences:completion:"")] + [Async (ResultTypeName = ""NSUrlSessionDataTaskRequest"", PostNonResultSnippet = ""result.Resume ();"")] + void ApplySnapshot (NSObject snapshot, bool animatingDifferences, [NullAllowed] Action completion); +} +"; + + yield return [(Source: postResult, Path: path), new AsyncData { + ResultTypeName = "NSUrlSessionDataTaskRequest", + PostNonResultSnippet = "result.Resume ();" + }]; + } + + IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); + } + + [Theory] + [AllSupportedPlatformsClassData] + void TryCreateTests (ApplePlatform platform, (string Source, string Path) source, AsyncData 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.AsyncAttribute, AsyncData.TryParse); + Assert.Equal (expectedData, attribute); + } +}