Skip to content

Commit

Permalink
feat(NativeCtorsGenerator): Generate nested types properly
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Sep 19, 2021
1 parent 24c16cc commit 1d772b9
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Uno.Extensions;
using Uno.UI.SourceGenerators.XamlGenerator.Utils;

namespace Uno.UI.SourceGenerators.Tests
Expand Down Expand Up @@ -45,10 +46,105 @@ public void When_GetFullyQualifiedString(string input, string expected)
}
}

[TestMethod]
public void When_Generating_Nested_Class()
{
var compilation = CreateCompilationWithProgramText(@"
namespace A.B
{
namespace C
{
partial class D
{
partial class E
{
}
}
}
}");
var type = compilation.GetTypeByMetadataName("A.B.C.D+E");
Assert.IsNotNull(type);
var builder = new IndentedStringBuilder();
var disposables = type.AddToIndentedStringBuilder(builder);
Assert.AreEqual(@"namespace A.B.C
{
partial class D
{
partial class E
{
", builder.ToString());

while (disposables.Count > 0)
{
disposables.Pop().Dispose();
}

Assert.AreEqual(@"namespace A.B.C
{
partial class D
{
partial class E
{
}
}
}
", builder.ToString());
}

[TestMethod]
public void When_Generating_Nested_Class_With_Action()
{
var compilation = CreateCompilationWithProgramText(@"
namespace A.B
{
namespace C
{
partial class D
{
partial struct E
{
}
}
}
}");
var type = compilation.GetTypeByMetadataName("A.B.C.D+E");
Assert.IsNotNull(type);
var builder = new IndentedStringBuilder();
var disposables = type.AddToIndentedStringBuilder(builder, builder => builder.AppendLineInvariant("[MyAttribute]"));
Assert.AreEqual(@"namespace A.B.C
{
partial class D
{
[MyAttribute]
partial struct E
{
", builder.ToString());

while (disposables.Count > 0)
{
disposables.Pop().Dispose();
}

Assert.AreEqual(@"namespace A.B.C
{
partial class D
{
[MyAttribute]
partial struct E
{
}
}
}
", builder.ToString());
}

private static Compilation CreateTestCompilation(string type)
=> CreateCompilationWithProgramText($"public class Test {{ public static {type} _myField {{ get; set; }} }}");

private static Compilation CreateCompilationWithProgramText(string text)
{
var programPath = @"Program.cs";
var programText = $"public class Test {{ public static {type} _myField {{ get; set; }} }}";
var programText = text;
var programTree = CSharpSyntaxTree
.ParseText(programText)
.WithFilePath(programPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,89 @@ internal static class SymbolExtensions
private static bool IsRoslyn34OrEalier { get; }
= typeof(INamedTypeSymbol).Assembly.GetVersionNumber() <= new Version("3.4");


/// <summary>
/// Given an <see cref="INamedTypeSymbol"/>, add the symbol declaration (including parent classes/namespaces) to the given <see cref="IIndentedStringBuilder"/>.
/// </summary>
/// <remarks>
/// <para>IMPORTANT: The returned stack must be disposed after putting everything for the given <see cref="INamedTypeSymbol"/>.</para>
/// <para>Example usage:</para>
/// <code><![CDATA[
/// var stack = myClass.AddToIndentedStringBuilder(builder);
/// using (builder.BlockInvariant("public static void M()"))
/// {
/// builder.AppendLineInvariant("Console.WriteLine(\"Hello world\")");
/// }
///
/// while (disposables.Count > 0)
/// {
/// disposables.Pop().Dispose();
/// }
/// ]]></code>
/// <para>NOTE: Another possible implementation is to accept an <see cref="Action"/> as a parameter to generate the type members, execute the action here, and also dispose
/// the stack here. The advantage is that callers don't need to worry about disposing the stack.</para>
/// </remarks>
public static Stack<IDisposable> AddToIndentedStringBuilder(this INamedTypeSymbol namedTypeSymbol, IIndentedStringBuilder builder, Action<IIndentedStringBuilder>? beforeClassHeaderAction = null)
{
var stack = new Stack<string>();
ISymbol symbol = namedTypeSymbol;
while (symbol != null)
{
if (symbol is INamespaceSymbol namespaceSymbol)
{
if (!namespaceSymbol.IsGlobalNamespace)
{
stack.Push($"namespace {namespaceSymbol}");
}

break;
}
else if (symbol is INamedTypeSymbol namedSymbol)
{
stack.Push(GetDeclarationHeaderFromNamedTypeSymbol(namedSymbol));
}
else
{
throw new InvalidOperationException($"Unexpected symbol type {symbol}");
}

symbol = symbol.ContainingSymbol;
}

var outputDisposableStack = new Stack<IDisposable>();
while (stack.Count > 0)
{
if (stack.Count == 1)
{
// Only the original symbol is left (usually a class header). Execute the given action before adding the class (usually this adds attributes).
beforeClassHeaderAction?.Invoke(builder);
}

outputDisposableStack.Push(builder.BlockInvariant(stack.Pop()));
}

return outputDisposableStack;
}

public static string GetDeclarationHeaderFromNamedTypeSymbol(this INamedTypeSymbol namedTypeSymbol)
{
var abstractKeyword = namedTypeSymbol.IsAbstract ? "abstract " : string.Empty;
var staticKeyword = namedTypeSymbol.IsStatic ? "static " : string.Empty;

// records are not handled.
var typeKeyword = namedTypeSymbol.TypeKind switch
{
TypeKind.Class => "class ",
TypeKind.Interface => "interface ",
TypeKind.Struct => "struct ",
_ => throw new ArgumentException($"Unexpected type kind {namedTypeSymbol.TypeKind}")
};

var declarationIdentifier = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);

return $"{abstractKeyword}{staticKeyword}partial {typeKeyword}{declarationIdentifier}";
}

public static IEnumerable<IPropertySymbol> GetProperties(this INamedTypeSymbol symbol) => symbol.GetMembers().OfType<IPropertySymbol>();

public static IEnumerable<IEventSymbol> GetAllEvents(this INamedTypeSymbol? symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Uno.UI.SourceGenerators.Helpers;
using System.Diagnostics;
using Uno.Extensions;

#if NETFRAMEWORK
using Uno.SourceGeneration;
Expand Down Expand Up @@ -40,52 +41,6 @@ private class SerializationMethodsGenerator : SymbolVisitor
private readonly INamedTypeSymbol? _intPtrSymbol;
private readonly INamedTypeSymbol? _jniHandleOwnershipSymbol;
private readonly INamedTypeSymbol?[]? _javaCtorParams;
private const string BaseClassFormat =
@"// <auto-generated>
// *************************************************************
// This file has been generated by Uno.UI (NativeCtorsGenerator)
// *************************************************************
// </auto-generated>
using System;
namespace {0}
{{
#if __IOS__ || __MACOS__
[global::Foundation.Register]
#endif
partial class {1}
{{
#if {2}
/// <summary>
/// Initializes a new instance of the class.
/// </summary>
public {3}() {{ }}
#endif
#if __ANDROID__
/// <summary>
/// Native constructor, do not use explicitly.
/// </summary>
/// <remarks>
/// Used by the Xamarin Runtime to materialize native
/// objects that may have been collected in the managed world.
/// </remarks>
public {3}(IntPtr javaReference, global::Android.Runtime.JniHandleOwnership transfer) : base (javaReference, transfer) {{ }}
#endif
#if __IOS__ || __MACOS__
/// <summary>
/// Native constructor, do not use explicitly.
/// </summary>
/// <remarks>
/// Used by the Xamarin Runtime to materialize native
/// objects that may have been collected in the managed world.
/// </remarks>
public {3}(IntPtr handle) : base (handle) {{ }}
#endif
}}
}}
";

public SerializationMethodsGenerator(GeneratorExecutionContext context)
{
Expand Down Expand Up @@ -143,14 +98,7 @@ private void ProcessType(INamedTypeSymbol typeSymbol)
{
_context.AddSource(
HashBuilder.BuildIDFromSymbol(typeSymbol),
string.Format(
BaseClassFormat,
typeSymbol.ContainingNamespace,
smallSymbolName,
NeedsExplicitDefaultCtor(typeSymbol),
SyntaxFacts.GetKeywordKind(typeSymbol.Name) == SyntaxKind.None ? typeSymbol.Name : "@" + typeSymbol.Name
)
);
GetGeneratedCode(typeSymbol));
}
}

Expand All @@ -163,15 +111,74 @@ private void ProcessType(INamedTypeSymbol typeSymbol)
{
_context.AddSource(
HashBuilder.BuildIDFromSymbol(typeSymbol),
string.Format(
BaseClassFormat,
typeSymbol.ContainingNamespace,
smallSymbolName,
NeedsExplicitDefaultCtor(typeSymbol),
SyntaxFacts.GetKeywordKind(typeSymbol.Name) == SyntaxKind.None ? typeSymbol.Name : "@" + typeSymbol.Name
)
);
GetGeneratedCode(typeSymbol));
}
}

static string GetGeneratedCode(INamedTypeSymbol typeSymbol)
{
var builder = new IndentedStringBuilder();
builder.AppendLineInvariant("// <auto-generated>");
builder.AppendLineInvariant("// *************************************************************");
builder.AppendLineInvariant("// This file has been generated by Uno.UI (NativeCtorsGenerator)");
builder.AppendLineInvariant("// *************************************************************");
builder.AppendLineInvariant("// </auto-generated>");
builder.AppendLine();
builder.AppendLineInvariant("using System;");
builder.AppendLine();
var disposables = typeSymbol.AddToIndentedStringBuilder(builder, beforeClassHeaderAction: builder =>
{
// These will be generated just before `partial class ClassName {`
builder.Append("#if __IOS__ || __MACOS__");
builder.AppendLine();
builder.AppendLineInvariant("[global::Foundation.Register]");
builder.Append("#endif");
builder.AppendLine();
});

var syntacticValidSymbolName = SyntaxFacts.GetKeywordKind(typeSymbol.Name) == SyntaxKind.None ? typeSymbol.Name : "@" + typeSymbol.Name;

if (NeedsExplicitDefaultCtor(typeSymbol))
{
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant("/// Initializes a new instance of the class.");
builder.AppendLineInvariant("/// </summary>");
builder.AppendLineInvariant($"public {syntacticValidSymbolName}() {{{{ }}}}");
builder.AppendLine();
}

builder.Append("#if __ANDROID__");
builder.AppendLine();
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant("/// Native constructor, do not use explicitly.");
builder.AppendLineInvariant("/// </summary>");
builder.AppendLineInvariant("/// <remarks>");
builder.AppendLineInvariant("/// Used by the Xamarin Runtime to materialize native ");
builder.AppendLineInvariant("/// objects that may have been collected in the managed world.");
builder.AppendLineInvariant("/// </remarks>");
builder.AppendLineInvariant($"public {syntacticValidSymbolName}(IntPtr javaReference, global::Android.Runtime.JniHandleOwnership transfer) : base (javaReference, transfer) {{{{ }}}}");
builder.Append("#endif");
builder.AppendLine();

builder.Append("#if __IOS__ || __MACOS__");
builder.AppendLine();
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant("/// Native constructor, do not use explicitly.");
builder.AppendLineInvariant("/// </summary>");
builder.AppendLineInvariant("/// <remarks>");
builder.AppendLineInvariant("/// Used by the Xamarin Runtime to materialize native ");
builder.AppendLineInvariant("/// objects that may have been collected in the managed world.");
builder.AppendLineInvariant("/// </remarks>");
builder.AppendLineInvariant($"public {syntacticValidSymbolName}(IntPtr handle) : base (handle) {{{{ }}}}");
builder.Append("#endif");
builder.AppendLine();

while (disposables.Count > 0)
{
disposables.Pop().Dispose();
}

return builder.ToString();
}

static IMethodSymbol? GetNativeCtor(INamedTypeSymbol? type, Func<IMethodSymbol, bool> predicate, bool considerAllBaseTypes)
Expand Down Expand Up @@ -201,7 +208,7 @@ private void ProcessType(INamedTypeSymbol typeSymbol)
}
}

private bool NeedsExplicitDefaultCtor(INamedTypeSymbol typeSymbol)
private static bool NeedsExplicitDefaultCtor(INamedTypeSymbol typeSymbol)
{
var hasExplicitConstructor = typeSymbol
.GetMembers(WellKnownMemberNames.InstanceConstructorName)
Expand Down

0 comments on commit 1d772b9

Please sign in to comment.