From a7ec9d5e093b8ee884fda96f7ec774a75b06fe55 Mon Sep 17 00:00:00 2001
From: Manuel de la Pena Saenz <mandel@microsoft.com>
Date: Fri, 31 Jan 2025 19:58:10 -0500
Subject: [PATCH] [Rgen] Add the method data model missing pieces for the
 transformer.

Add the missing methods for the transformer to be ablet to create a
data model representation of a method.
---
 .../DataModel/Method.cs                       |   2 +-
 .../AttributesNames.cs                        |   2 +-
 .../DataModel/Method.Transformer.cs           | 120 +++++-
 .../DataModel/MethodTests.cs                  | 363 ++++++++++++++++++
 4 files changed, 476 insertions(+), 11 deletions(-)
 create mode 100644 tests/rgen/Microsoft.Macios.Transformer.Tests/DataModel/MethodTests.cs

diff --git a/src/rgen/Microsoft.Macios.Generator/DataModel/Method.cs b/src/rgen/Microsoft.Macios.Generator/DataModel/Method.cs
index 4be3968ba03d..056c5d0898c3 100644
--- a/src/rgen/Microsoft.Macios.Generator/DataModel/Method.cs
+++ b/src/rgen/Microsoft.Macios.Generator/DataModel/Method.cs
@@ -41,7 +41,7 @@ namespace Microsoft.Macios.Generator.DataModel;
 	/// <summary>
 	/// Modifiers list.
 	/// </summary>
-	public ImmutableArray<SyntaxToken> Modifiers { get; } = [];
+	public ImmutableArray<SyntaxToken> Modifiers { get; init; } = [];
 
 	/// <summary>
 	/// Parameters list.
diff --git a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs
index 1570d81f939f..0472fa7dec14 100644
--- a/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs
+++ b/src/rgen/Microsoft.Macios.Transformer/AttributesNames.cs
@@ -302,7 +302,7 @@ static class AttributesNames {
 	/// When this attribute is applied to a class it will just generate a static class, one that does not derive
 	/// from NSObject.
 	/// </summary>
-	[BindingFlag (AttributeTargets.Class)]
+	[BindingFlag (AttributeTargets.Class | AttributeTargets.Method)]
 	public const string StaticAttribute = "StaticAttribute";
 	
 	/// <summary>
diff --git a/src/rgen/Microsoft.Macios.Transformer/DataModel/Method.Transformer.cs b/src/rgen/Microsoft.Macios.Transformer/DataModel/Method.Transformer.cs
index fd2db969254b..b6deb2c527b5 100644
--- a/src/rgen/Microsoft.Macios.Transformer/DataModel/Method.Transformer.cs
+++ b/src/rgen/Microsoft.Macios.Transformer/DataModel/Method.Transformer.cs
@@ -2,38 +2,140 @@
 // Licensed under the MIT License.
 
 using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Microsoft.Macios.Generator.Availability;
+using Microsoft.Macios.Generator.Extensions;
 using Microsoft.Macios.Transformer.Attributes;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
 
 namespace Microsoft.Macios.Generator.DataModel;
 
 readonly partial struct Method {
 
+	readonly ExportData? overrideExportData;
+
 	/// <summary>
 	/// The data of the export attribute used to mark the value as a property binding. 
 	/// </summary>
-	public ExportData ExportMethodData { get; }
+	public ExportData? ExportMethodData {
+		get {
+			return overrideExportData ?? ExportAttribute;
+		}
+		init => overrideExportData = value;
+	}
 
 	/// <summary>
 	/// True if the method was exported with the MarshalNativeExceptions flag allowing it to support native exceptions.
 	/// </summary>
-	public bool MarshalNativeExceptions => throw new NotImplementedException ();
+	public bool MarshalNativeExceptions => HasMarshalNativeExceptionsFlag;
 
-	public Method (string type, string name, TypeInfo returnType,
+	public Method (string type, 
+		string name, 
+		TypeInfo returnType,
 		SymbolAvailability symbolAvailability,
-		ExportData exportMethodData,
-		ImmutableArray<AttributeCodeChange> attributes,
-		ImmutableArray<SyntaxToken> modifiers,
+		Dictionary<string, List<AttributeData>> attributes,
 		ImmutableArray<Parameter> parameters)
 	{
 		Type = type;
 		Name = name;
+		AttributesDictionary = attributes;	
 		ReturnType = returnType;
 		SymbolAvailability = symbolAvailability;
-		ExportMethodData = exportMethodData;
-		Attributes = attributes;
-		Modifiers = modifiers;
 		Parameters = parameters;
+		
+#pragma warning disable format
+		// Modifiers are special because we might be dealing with several flags that the user has set in the method.
+		// We have to add the partial keyword so that we can have the partial implementation of the method later generated
+		// by the roslyn code generator
+		Modifiers = this switch {
+			// internal static partial
+			{ HasNewFlag: false, HasStaticFlag: true, HasInternalFlag: true } 
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.StaticKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// public static partial
+			{ HasNewFlag: false, HasStaticFlag: true, HasInternalFlag: false } 
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.StaticKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// internal new static partial
+			{ HasNewFlag: true, HasStaticFlag: true, HasInternalFlag: true} 
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.StaticKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// public new static partial
+			{ HasNewFlag: true, HasStaticFlag: true, HasInternalFlag: false }
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.StaticKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// public new virtual partial
+			{ HasNewFlag: true, HasStaticFlag: false, HasAbstractFlag: false, HasInternalFlag: false }
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.VirtualKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// internal new virtual partial
+			{ HasNewFlag: true, HasStaticFlag: false, HasAbstractFlag: false, HasInternalFlag: true }
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.VirtualKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// public new abstract
+			{ HasNewFlag: true, HasAbstractFlag: true, HasInternalFlag: false}
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.AbstractKeyword)],
+			
+			// internal new abstract
+			{ HasNewFlag: true, HasAbstractFlag: true, HasInternalFlag: true}
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.NewKeyword), Token (SyntaxKind.AbstractKeyword)],
+			
+			// public override partial
+			{ HasNewFlag: false, HasOverrideFlag: true, HasInternalFlag: false}
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.OverrideKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// internal override partial
+			{ HasNewFlag: false, HasOverrideFlag: true, HasInternalFlag: true}
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.OverrideKeyword), Token (SyntaxKind.PartialKeyword)],
+			
+			// public abstract 
+			{ HasAbstractFlag: true, HasInternalFlag: false}
+				=> [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.AbstractKeyword)],
+			
+			// internal abstract
+			{ HasAbstractFlag: true, HasInternalFlag: true}
+				=> [Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.AbstractKeyword)],
+			
+			// general case, but internal
+			{ HasInternalFlag: true} => 
+				[Token (SyntaxKind.InternalKeyword), Token (SyntaxKind.VirtualKeyword), Token (SyntaxKind.VirtualKeyword)],
+			
+			// general case
+			_ => [Token (SyntaxKind.PublicKeyword), Token (SyntaxKind.VirtualKeyword), Token (SyntaxKind.PartialKeyword)]
+		};
+#pragma warning restore format
+
+	}
+
+	public static bool TryCreate (MethodDeclarationSyntax declaration, SemanticModel semanticModel,
+		[NotNullWhen (true)] out Method? change)
+	{
+		if (semanticModel.GetDeclaredSymbol (declaration) is not IMethodSymbol method) {
+			change = null;
+			return false;
+		}
+		var parametersBucket = ImmutableArray.CreateBuilder<Parameter> ();
+		// loop over the parameters of the construct since changes on those implies a change in the generated code
+		foreach (var parameter in method.Parameters) {
+			var parameterDeclaration = declaration.ParameterList.Parameters [parameter.Ordinal];
+			if (!Parameter.TryCreate (parameter, parameterDeclaration, semanticModel, out var parameterChange))
+				continue;
+			parametersBucket.Add (parameterChange.Value);
+		}
+		// get the attributes of the method, this are used for two reasons:
+		// 1. The attributes might have flags such as [Internal] or [Abstract] that we need to set in the modifiers
+		// 2. The attributes might have return attributes that we need to set in the return type
+		var attributes = method.GetAttributeData ();	
+		change = new (
+			type: method.ContainingSymbol.ToDisplayString ().Trim (), // we want the full name
+			name: method.Name,
+			returnType: new TypeInfo (method.ReturnType, attributes),
+			symbolAvailability: method.GetAvailabilityForSymbol (), // special case, in the transformer we only translate, we do not merge
+			attributes: attributes,
+			parameters: parametersBucket.ToImmutableArray ());
+		return true;
 	}
 }
diff --git a/tests/rgen/Microsoft.Macios.Transformer.Tests/DataModel/MethodTests.cs b/tests/rgen/Microsoft.Macios.Transformer.Tests/DataModel/MethodTests.cs
new file mode 100644
index 000000000000..227278808630
--- /dev/null
+++ b/tests/rgen/Microsoft.Macios.Transformer.Tests/DataModel/MethodTests.cs
@@ -0,0 +1,363 @@
+// 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.Attributes;
+using Microsoft.Macios.Generator.Availability;
+using Microsoft.Macios.Generator.DataModel;
+using Xamarin.Tests;
+using Xamarin.Utils;
+using static Microsoft.Macios.Generator.Tests.TestDataFactory;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Microsoft.Macios.Transformer.Tests.DataModel;
+
+public class MethodTests : BaseTransformerTestClass {
+	
+	class TestDataTryCreate : IEnumerable<object []> {
+		public IEnumerator<object []> GetEnumerator ()
+		{
+			var path = "/some/random/path.cs";
+			var availabilityBuilder = SymbolAvailability.CreateBuilder ();
+			availabilityBuilder.Add (new SupportedOSPlatformData("ios"));
+			availabilityBuilder.Add(new SupportedOSPlatformData("tvos"));
+			availabilityBuilder.Add(new SupportedOSPlatformData("macos"));
+			availabilityBuilder.Add(new SupportedOSPlatformData("maccatalyst"));
+
+			const string simpleMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Export (""play"")]
+	public void Play ();
+}
+";
+			yield return [
+				(Source: simpleMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.VirtualKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+
+			const string abstractMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Abstract, Export (""play"")]
+	public void Play ();
+}
+";
+			
+			yield return [
+				(Source: abstractMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.AbstractKeyword),
+					]
+				}
+			];
+
+			const string internalAbstractMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Internal, Abstract, Export (""play"")]
+	public void Play ();
+}
+";
+			
+			yield return [
+				(Source: internalAbstractMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.InternalKeyword),
+						Token (SyntaxKind.AbstractKeyword),
+					]
+				}
+			];
+
+			const string newMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[New, Export (""play"")]
+	public void Play ();
+}
+";
+			yield return [
+				(Source: newMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.NewKeyword),
+						Token (SyntaxKind.VirtualKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+
+			const string overrideMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Override, Export (""play"")]
+	public void Play ();
+}
+";
+			
+			yield return [
+				(Source: overrideMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.OverrideKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+			
+			const string intReturnMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Export (""play"")]
+	public int Play ();
+}
+";
+			
+			yield return [
+				(Source: intReturnMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForInt (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.VirtualKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+			
+			const string nullableIntReturnMethod = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Export (""play"")]
+	[return: NullAllowed]
+	public int Play ();
+}
+";
+			
+			yield return [
+				(Source: nullableIntReturnMethod, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForInt (isNullable: true, keepInterfaces: true), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [])
+				{
+					ExportMethodData = new ("play"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.VirtualKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+			
+			const string stringParameter = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Override, Export (""play:"")]
+	public void Play (string name);
+}
+";
+			
+			yield return [
+				(Source: stringParameter, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [
+						new (0, ReturnTypeForString (), "name"),
+					])
+				{
+					ExportMethodData = new ("play:"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.OverrideKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+			
+			const string nullableStringParameter = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Override, Export (""play:"")]
+	public void Play ([NullAllowed] string name);
+}
+";
+			
+			yield return [
+				(Source: nullableStringParameter, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [
+						new (0, ReturnTypeForString (isNullable: true), "name") {
+							Attributes = [
+								new ("NullAllowedAttribute"),
+							]
+						},
+					])
+				{
+					ExportMethodData = new ("play:"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.OverrideKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+			
+			const string severalParameter = @"
+using System;
+using Foundation;
+using ObjCRuntime;
+
+interface AVPlayer {
+	[Override, Export (""play:"")]
+	public void Play (string name, int age);
+}
+";
+			
+			yield return [
+				(Source: severalParameter, Path: path),
+				new Method(
+					type: "AVPlayer", 
+					name: "Play", 
+					returnType: ReturnTypeForVoid (), 
+					symbolAvailability: availabilityBuilder.ToImmutable (), 
+					attributes: new (), 
+					parameters: [
+						new (0, ReturnTypeForString (), "name"),
+						new (1, ReturnTypeForInt(), "age"),
+					])
+				{
+					ExportMethodData = new ("play:"),
+					Modifiers = [
+						Token (SyntaxKind.PublicKeyword),
+						Token (SyntaxKind.OverrideKeyword),
+						Token (SyntaxKind.PartialKeyword),
+					]
+				}
+			];
+		}
+		
+
+		IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
+	}
+	
+	[Theory]
+	[AllSupportedPlatformsClassData<TestDataTryCreate>]
+	void TryCreateTests (ApplePlatform platform, (string Source, string Path) source, Method expectedData)
+	{
+		var compilation = CreateCompilation (platform, sources: source);
+		var syntaxTree = compilation.SyntaxTrees.ForSource (source);
+		var trees = compilation.SyntaxTrees.Where (s => s.FilePath == source.Path).ToArray ();
+		Assert.Single (trees);
+		Assert.NotNull (syntaxTree);
+
+		var semanticModel = compilation.GetSemanticModel (syntaxTree);
+		Assert.NotNull (semanticModel);
+
+		var declaration = syntaxTree.GetRoot ()
+				.DescendantNodes ().OfType<MethodDeclarationSyntax> ()
+				.LastOrDefault ();
+		Assert.NotNull (declaration);
+		Assert.True (Method.TryCreate (declaration, semanticModel, out var parameter));
+		Assert.Equal (expectedData, parameter);
+	}
+}