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); + } +}