using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace QuestPDF.InteropGenerators;
public static class NewGenerator
{
private const string PythonTemplate = @"# auto-generated
from cffi import FFI
from typing import Callable, Optional, Tuple, Any, Self
";
private const string CsharpInteropTemplate = @"//
#nullable enable
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using QuestPDF.Helpers;
using QuestPDF.Companion;
namespace QuestPDF;
internal unsafe partial class Interop
{
{{ methods }}
}
";
public static string AnalyzeAndGenerate(INamespaceSymbol namespaceSymbol)
{
var methods = CollectExtensionMethods(namespaceSymbol);
var targetMethods = methods
.Where(x => !IsObsolete(x.Item2))
.ToList();
var supportedExtensionMethods = targetMethods
.Where(x => IsMethodSupported(x.Item2))
.ToList();
var simpleMethodHeaders = supportedExtensionMethods
.Select(x => ConvertMethodToHeader(x.Item1, x.Item2))
.ToList();
var excluded = targetMethods.Except(supportedExtensionMethods).ToList();
var pythonCHeaders = string.Join("\n", simpleMethodHeaders);
var pythonContainerClass = supportedExtensionMethods
.GroupBy(x => x.Item2.IsExtensionMethod ? x.Item2.Parameters.First().Type.Name : x.Item1.Name)
.Select(x => ConvertToPythonContainerClient(x.Key, x))
.ToList();
var interopTemplateMethods = string.Join("\n\n", supportedExtensionMethods.Select(x => ConvertToUnmanagedCallersOnlyDefinition(x.Item1, x.Item2)));
return CsharpInteropTemplate.Replace("{{ methods }}", interopTemplateMethods);
}
private static bool IsObsolete(IMethodSymbol method)
{
return method.GetAttributes().Any(attr => attr.AttributeClass?.ToDisplayString() == "System.ObsoleteAttribute");
}
private static bool IsMethodSupported(IMethodSymbol method)
{
if (method.IsGenericMethod)
return false;
if (method.Parameters.Any(x => IsTaskRelatedType(x.Type)))
return false;
// method has lambdas
if (method.Parameters.Any(x => x.Type.TypeKind == TypeKind.Delegate))
return false;
var problematicMethods = new[] {"Border", "GeneratePdf", "GenerateXps", "Fallback", "DebugArea"};
if (problematicMethods.Contains(method.Name))
return false;
if (method.IsExtensionMethod)
{
return method.Parameters.Skip(1).All(x => IsValueType(x.Type) || x.Type.SpecialType == SpecialType.System_String || IsColorType(x.Type) || IsUnitType(x.Type));
}
return true;
}
public static bool IsTaskRelatedType(ITypeSymbol type)
{
var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Check for Task, Task, ValueTask, ValueTask
if (fullName.StartsWith("global::System.Threading.Tasks.Task") ||
fullName.StartsWith("global::System.Threading.Tasks.ValueTask"))
return true;
// Check for CancellationToken
if (fullName == "global::System.Threading.CancellationToken")
return true;
// Check for IAsyncEnumerable and IAsyncEnumerator
if (fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerable") ||
fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerator"))
return true;
return false;
}
#region Method Discovery
private static IEnumerable<(INamedTypeSymbol, IMethodSymbol)> CollectExtensionMethods(INamespaceSymbol root)
{
var result = new List<(INamedTypeSymbol, IMethodSymbol)>();
TraverseNamespaces(root);
return result;
void TraverseNamespaces(INamespaceSymbol namespaceSymbol)
{
foreach (var member in namespaceSymbol.GetMembers())
{
if (member is INamespaceSymbol childNs)
{
TraverseNamespaces(childNs);
}
else if (member is INamedTypeSymbol type)
{
HandleType(type);
}
}
}
void HandleType(INamedTypeSymbol type)
{
if (type.DeclaredAccessibility != Accessibility.Public || type.IsImplicitlyDeclared)
return;
foreach (var member in type.GetMembers())
{
if (member is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false } method)
{
result.Add((type, method));
}
}
}
}
#endregion
#region Method Conversion
private static string ConvertMethodToHeader(INamedTypeSymbol typeSymbol, IMethodSymbol method)
{
var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
var returnType = GetCReturnType(method.ReturnType);
var parameters = GetCParameters(method);
return $"{returnType} {methodName}({string.Join(", ", parameters)});";
static List GetCParameters(IMethodSymbol method)
{
var parameters = new List();
foreach (var param in method.Parameters)
{
var paramType = GetCReturnType(param.Type);
var paramName = ToSnakeCase(param.Name);
parameters.Add($"{paramType} {paramName}");
}
return parameters;
}
}
static string ToSnakeCase(string text)
{
return string.Concat(text.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLowerInvariant();
}
static string ConvertToInterfaceFunctionName(INamedTypeSymbol typeSymbol, IMethodSymbol method)
{
var typeName = method.IsExtensionMethod ? method.Parameters.First().Type.Name : typeSymbol.Name;
if (typeName.StartsWith("I") && typeName.Length > 1 && char.IsUpper(typeName[1]))
typeName = typeName.Substring(1);
return $"questpdf_{ToSnakeCase(typeName)}_{ToSnakeCase(method.Name)}";
}
private static string ConvertToPythonContainerClient(string groupName, IEnumerable<(INamedTypeSymbol, IMethodSymbol)> methodSymbols)
{
var sb = new StringBuilder();
sb.AppendLine($"class {groupName}:");
sb.AppendLine("\tdef __init__(self, handler_pointer: \"ffi.CData\"):");
sb.AppendLine("\t\tself.handler_pointer = handler_pointer");
foreach (var (typeSymbol, methodSymbol) in methodSymbols)
{
if (!methodSymbol.IsExtensionMethod)
{
sb.AppendLine($"\t\t# Conversion not supported");
continue;
}
var additionalParameters = string.Join(", ", methodSymbol.Parameters.Skip(1).Select(ParamToInvocationArgument));
if (additionalParameters.Length > 0)
additionalParameters = ", " + additionalParameters;
sb.AppendLine();
sb.AppendLine($"\tdef {ToSnakeCase(methodSymbol.Name)}(self{additionalParameters}) -> Self:");
sb.AppendLine($"\t\tresult = lib.{ConvertToInterfaceFunctionName(typeSymbol, methodSymbol)}(self.handler_pointer{additionalParameters})");
sb.AppendLine("\t\treturn Container(result)");
}
return sb.ToString();
static string ParamToInvocationArgument(IParameterSymbol param)
{
var type = "";
return $"{ToSnakeCase(param.Name)}{type}";
}
}
private static string ConvertToUnmanagedCallersOnlyDefinition(INamedTypeSymbol typeSymbol, IMethodSymbol method)
{
var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
var returnType = GetCSharpInteropReturnType(method.ReturnType);
var parameters = string.Join(", ", GetMethodParams(method));
var additionalParameters = string.Join(", ", method.Parameters.Skip(method.IsExtensionMethod ? 1 : 0).Select(ParamToInvocationArgument));
var typePointer = (method.IsExtensionMethod ? method.Parameters.First().Type : typeSymbol).ToDisplayString();
if (!method.IsExtensionMethod)
{
parameters = parameters.Any() ? $"nint nativePointer, {parameters}" : "nint nativePointer";
additionalParameters = additionalParameters.Any() ? $"nativePointer, {additionalParameters}" : "nativePointer";
}
var typePointerInputName = method.IsExtensionMethod ? method.Parameters.First().Name : "nativePointer";
var sb = new StringBuilder();
sb.AppendLine($"[UnmanagedCallersOnly(EntryPoint = \"{methodName}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
sb.AppendLine($"public static {returnType} {typeSymbol.Name}{method.Name}({parameters})");
sb.AppendLine("{");
if (method.ReturnsVoid)
{
sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
sb.AppendLine($"extendedTypePointer.{method.Name}({additionalParameters});");
}
else
{
sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
sb.AppendLine($"var result = extendedTypePointer.{method.Name}({additionalParameters});");
sb.AppendLine("return BoxHandle(result);");
}
sb.AppendLine("}");
return sb.ToString();
static string ParamToInvocationArgument(IParameterSymbol param)
{
if (param.Type.SpecialType == SpecialType.System_String)
return $"Marshal.PtrToStringUTF8((IntPtr){param.Name}) ?? \"\"";
if (param.Type.TypeKind == TypeKind.Enum)
return $"({param.Type.ToDisplayString()}){param.Name}";
if (IsColorType(param.Type))
return $"(QuestPDF.Infrastructure.Color){param.Name}";
return param.Name;
}
}
static IEnumerable GetMethodParams(IMethodSymbol method)
{
foreach (var param in method.Parameters)
{
var paramType = GetCSharpInteropReturnType(param.Type);
yield return $"{paramType} {param.Name}";
}
}
static string GetCSharpInteropReturnType(ITypeSymbol type)
{
if (type.SpecialType == SpecialType.System_Void)
return "void";
if (type.SpecialType == SpecialType.System_Int32)
return "int";
if (type.SpecialType == SpecialType.System_UInt32)
return "uint";
if (type.SpecialType == SpecialType.System_Boolean)
return "bool";
if (type.SpecialType == SpecialType.System_Single)
return "float";
if (type.SpecialType == SpecialType.System_Double)
return "double";
if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
return "uint";
if (type.TypeKind == TypeKind.Enum)
return "int";
// For object types, return IntPtr
return "nint";
}
static string GetCReturnType(ITypeSymbol type)
{
if (type.SpecialType == SpecialType.System_Void)
return "void";
if (type.SpecialType == SpecialType.System_Int32)
return "int32_t";
if (type.SpecialType == SpecialType.System_UInt32)
return "uint32_t";
if (type.SpecialType == SpecialType.System_Boolean)
return "bool";
if (type.SpecialType == SpecialType.System_Single)
return "float";
if (type.SpecialType == SpecialType.System_Double)
return "double";
if (type.SpecialType == SpecialType.System_String)
return "char*";
if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
return "uint32_t";
if (type.TypeKind == TypeKind.Enum)
return "int32_t";
// For object types, return void pointer
return "void*";
}
static bool IsValueType(ITypeSymbol type)
{
return type.SpecialType switch
{
SpecialType.System_Int32 => true,
SpecialType.System_UInt32 => true,
SpecialType.System_Boolean => true,
SpecialType.System_Single => true,
SpecialType.System_Double => true,
_ => type.IsValueType
};
}
static bool IsColorType(ITypeSymbol type)
{
return type.ToDisplayString() == "QuestPDF.Infrastructure.Color";
}
static bool IsUnitType(ITypeSymbol type)
{
return type.ToDisplayString() == "QuestPDF.Infrastructure.Unit";
}
#endregion
}