From fe3d6a4ffcb4c40380f3b63a544149cd411c1e72 Mon Sep 17 00:00:00 2001
From: Manuel de la Pena Saenz <mandel@microsoft.com>
Date: Tue, 28 Jan 2025 11:28:58 -0500
Subject: [PATCH] [Rgen] Add Async attr parsing to the transformer.

---
 .../Attributes/AsyncData.cs                   | 154 ++++++++++++++++
 .../AttributesNames.cs                        |   3 +
 .../Attributes/AsyncDataTests.cs              | 168 ++++++++++++++++++
 3 files changed, 325 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 000000000000..a9751fd30601
--- /dev/null
+++ b/src/rgen/Microsoft.Macios.Transformer/Attributes/AsyncData.cs
@@ -0,0 +1,154 @@
+// 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<AsyncData> {
+	/// <summary>
+	/// Diff the constructor used in the bindings.
+	/// </summary>
+	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;
+	}
+
+	/// <inheritdoc />
+	public override bool Equals (object? obj)
+	{
+		return obj is AsyncData other && Equals (other);
+	}
+
+	/// <inheritdoc />
+	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 afe3ff0f658a..5c22a1fbf080 100644
--- a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs
+++ b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs
@@ -33,6 +33,9 @@ static class AttributesNames {
 	/// </summary>
 	[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 000000000000..36d52646acfd
--- /dev/null
+++ b/tests/rgen/Microsoft.Macios.Transformer.Tests/Attributes/AsyncDataTests.cs
@@ -0,0 +1,168 @@
+// 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<object []> {
+		public IEnumerator<object []> 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<TestDataTryCreate>]
+	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<MethodDeclarationSyntax> ()
+			.FirstOrDefault ();
+		Assert.NotNull (declaration);
+
+		var symbol = semanticModel.GetDeclaredSymbol (declaration);
+		Assert.NotNull (symbol);
+		var attribute = symbol.GetAttribute<AsyncData> (AttributesNames.AsyncAttribute, AsyncData.TryParse);
+		Assert.Equal (expectedData, attribute);
+	}
+}