Skip to content

Commit

Permalink
Generate adapter code for arrays and typed arrays (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored and vmoroz committed Mar 24, 2023
1 parent 6c476c2 commit c197b3c
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 74 deletions.
138 changes: 138 additions & 0 deletions Generator/AdapterGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class AdapterGenerator : SourceGenerator

private readonly Dictionary<string, ISymbol> _adaptedMembers = new();
private readonly Dictionary<string, ITypeSymbol> _adaptedStructs = new();
private readonly Dictionary<string, ITypeSymbol> _adaptedArrays = new();

internal AdapterGenerator(GeneratorExecutionContext context)
{
Expand Down Expand Up @@ -132,6 +133,19 @@ private string GetStructAdapterName(ITypeSymbol structType, bool toJS)
return (getAdapterName, setAdapterName);
}

private string GetArrayAdapterName(ITypeSymbol elementType, bool toJS)
{
string ns = GetNamespace(elementType);
string elementName = elementType.Name;
string prefix = toJS ? AdapterFromPrefix : AdapterToPrefix;
string adapterName = $"{prefix}{ns.Replace('.', '_')}_{elementName}_Array";
if (!_adaptedArrays.ContainsKey(adapterName))
{
_adaptedArrays.Add(adapterName, elementType);
}
return adapterName;
}

internal void GenerateAdapters(SourceBuilder s)
{
foreach (KeyValuePair<string, ISymbol> nameAndSymbol in _adaptedMembers)
Expand Down Expand Up @@ -164,6 +178,14 @@ internal void GenerateAdapters(SourceBuilder s)
ITypeSymbol structSymbol = nameAndSymbol.Value;
GenerateStructAdapter(ref s, adapterName, structSymbol);
}

foreach (KeyValuePair<string, ITypeSymbol> nameAndSymbol in _adaptedArrays)
{
s++;
string adapterName = nameAndSymbol.Key;
ITypeSymbol elementSymbol = nameAndSymbol.Value;
GenerateArrayAdapter(ref s, adapterName, elementSymbol);
}
}

private void GenerateConstructorAdapter(
Expand Down Expand Up @@ -345,6 +367,59 @@ private void GenerateStructAdapter(
}
}

private void GenerateArrayAdapter(
ref SourceBuilder s,
string adapterName,
ITypeSymbol elementType)
{
string ns = GetNamespace(elementType);
string elementName = elementType.Name;

if (adapterName.StartsWith(AdapterFromPrefix))
{
s += $"private static JSValue {adapterName}({ns}.{elementName}[] array)";
s += "{";
s += "JSArray jsArray = new JSArray(array.Length);";
s += "for (int i = 0; i < array.Length; i++)";
s += "{";
s += $"jsArray[i] = {Convert("array[i]", elementType, null)};";
s += "}";
s += "return jsArray;";
s += "}";
}
else
{
s += $"private static {ns}.{elementName}[] {adapterName}(JSValue value)";
s += "{";
s += "JSArray jsArray = (JSArray)value;";
s += $"{ns}.{elementName}[] array = new {ns}.{elementName}[jsArray.Length];";
s += "for (int i = 0; i < array.Length; i++)";
s += "{";
s += $"array[i] = {Convert("jsArray[i]", null, elementType)};";
s += "}";
s += "return array;";
s += "}";
}
}

private bool IsTypedArrayType(ITypeSymbol elementType)
{
return elementType.SpecialType switch
{
SpecialType.System_SByte => true,
SpecialType.System_Byte => true,
SpecialType.System_Int16 => true,
SpecialType.System_UInt16 => true,
SpecialType.System_Int32 => true,
SpecialType.System_UInt32 => true,
SpecialType.System_Int64 => true,
SpecialType.System_UInt64 => true,
SpecialType.System_Single => true,
SpecialType.System_Double => true,
_ => false,
};
}

private void AdaptThisArg(ref SourceBuilder s, ISymbol symbol)
{

Expand Down Expand Up @@ -425,6 +500,14 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol
}
else if (toType.TypeKind == TypeKind.Struct)
{
if (toType is INamedTypeSymbol namedType &&
namedType.TypeParameters.Length == 1 &&
namedType.OriginalDefinition.Name == "Memory" &&
IsTypedArrayType(namedType.TypeArguments[0]))
{
return $"((JSTypedArray<{namedType.TypeArguments[0]}>){fromExpression}).AsMemory()";
}

VerifyReferencedTypeIsExported(toType);

string adapterName = GetStructAdapterName(toType, toJS: false);
Expand All @@ -438,6 +521,26 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol
return $"{adapterName}({fromExpression})";
}
}
else if (toType.TypeKind == TypeKind.Array)
{
ITypeSymbol elementType = ((IArrayTypeSymbol)toType).ElementType;
VerifyReferencedTypeIsExported(elementType);

string adapterName = GetArrayAdapterName(elementType, toJS: false);
if (isNullable)
{
return $"({fromExpression}).IsNullOrUndefined() ? ({elementType}[]?)null : " +
$"{adapterName}({fromExpression})";
}
else
{
return $"{adapterName}({fromExpression})";
}
}
else if (toType is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0)
{
// TODO: Handle generic collections.
}

// TODO: Handle other kinds of conversions from JSValue.
// TODO: Handle unwrapping external values.
Expand Down Expand Up @@ -483,6 +586,14 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol
}
else if (fromType.TypeKind == TypeKind.Struct)
{
if (fromType is INamedTypeSymbol namedType &&
namedType.TypeParameters.Length == 1 &&
namedType.OriginalDefinition.Name == "Memory" &&
IsTypedArrayType(namedType.TypeArguments[0]))
{
return $"new JSTypedArray<{namedType.TypeArguments[0]}>({fromExpression})";
}

VerifyReferencedTypeIsExported(fromType);

string adapterName = GetStructAdapterName(fromType, toJS: true);
Expand All @@ -496,6 +607,26 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol
return $"{adapterName}({fromExpression})";
}
}
else if (fromType.TypeKind == TypeKind.Array)
{
ITypeSymbol elementType = ((IArrayTypeSymbol)fromType).ElementType;
VerifyReferencedTypeIsExported(elementType);

string adapterName = GetArrayAdapterName(elementType, toJS: true);
if (isNullable)
{
return $"{fromExpression} == null ? JSValue.Null : " +
$"{adapterName}({fromExpression})";
}
else
{
return $"{adapterName}({fromExpression})";
}
}
else if (fromType is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0)
{
// TODO: Handle generic collections.
}

// TODO: Handle other kinds of conversions to JSValue.
// TODO: Consider wrapping unsupported types in a value of type "external".
Expand All @@ -512,6 +643,13 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol

private void VerifyReferencedTypeIsExported(ITypeSymbol type)
{
switch (type.SpecialType)
{
case SpecialType.System_Object:
case SpecialType.System_String: return;
default: break;
}

if (ModuleGenerator.GetJSExportAttribute(type) == null)
{
// TODO: Consider an option to automatically export referenced classes?
Expand Down
4 changes: 2 additions & 2 deletions Generator/ModuleGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public void Execute(GeneratorExecutionContext context)
// No type definitions are generated when using a custom init function.
if (moduleInitializer is not IMethodSymbol)
{
SourceText typeDefinitions = TypeDefinitionsGenerator.GenerateTypeDefinitions(
exportItems);
TypeDefinitionsGenerator tsGenerator = new(exportItems);
SourceText typeDefinitions = tsGenerator.GenerateTypeDefinitions();
if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(
"build_property.TargetPath", out string? targetPath))
{
Expand Down
106 changes: 93 additions & 13 deletions Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,29 @@

namespace NodeApi.Generator;

// An analyzer bug results in incorrect reports of CA1822 against methods in this class.
#pragma warning disable CA1822 // Mark members as static

internal class TypeDefinitionsGenerator : SourceGenerator
{
private static readonly Regex s_newlineRegex = new("\n *");
private static readonly Regex s_summaryRegex = new("<summary>(.*)</summary>");
private static readonly Regex s_remarksRegex = new("<remarks>(.*)</remarks>");

internal static SourceText GenerateTypeDefinitions(IEnumerable<ISymbol> exportItems)
private readonly IEnumerable<ISymbol> _exportItems;

public TypeDefinitionsGenerator(IEnumerable<ISymbol> exportItems)
{
_exportItems = exportItems;
}

internal SourceText GenerateTypeDefinitions()
{
var s = new SourceBuilder();

s += "// Generated type definitions for .NET module";

foreach (ISymbol exportItem in exportItems)
foreach (ISymbol exportItem in _exportItems)
{
if (exportItem is ITypeSymbol exportType &&
(exportType.TypeKind == TypeKind.Class || exportType.TypeKind == TypeKind.Struct))
Expand Down Expand Up @@ -50,7 +60,7 @@ internal static SourceText GenerateTypeDefinitions(IEnumerable<ISymbol> exportIt
return s;
}

private static void GenerateClassTypeDefinitions(ref SourceBuilder s, ITypeSymbol exportClass)
private void GenerateClassTypeDefinitions(ref SourceBuilder s, ITypeSymbol exportClass)
{
s++;
GenerateDocComments(ref s, exportClass);
Expand Down Expand Up @@ -117,8 +127,10 @@ member is IMethodSymbol exportConstructor &&
s += "}";
}

private static string GetTSType(ITypeSymbol type)
private string GetTSType(ITypeSymbol type)
{
string tsType = "unknown";

string? specialType = type.SpecialType switch
{
SpecialType.System_Void => "void",
Expand All @@ -137,25 +149,93 @@ private static string GetTSType(ITypeSymbol type)
////SpecialType.System_DateTime => "Date",
_ => null,
};

if (specialType != null)
{
return specialType;
tsType = specialType;
}

if (type.TypeKind == TypeKind.Class)
else if (type.TypeKind == TypeKind.Array)
{
// TODO: Check if class is exported.
ITypeSymbol elementType = ((IArrayTypeSymbol)type).ElementType;
tsType = GetTSType(elementType) + "[]";
}
else if (type.TypeKind == TypeKind.Array)
else if (type is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0)
{
if (namedType.OriginalDefinition.Name == "Nullable")
{
tsType = GetTSType(namedType.TypeArguments[0]) + " | null";
}
else if (namedType.OriginalDefinition.Name == "Memory")
{
ITypeSymbol elementType = namedType.TypeArguments[0];
tsType = elementType.SpecialType switch
{
SpecialType.System_SByte => "Int8Array",
SpecialType.System_Int16 => "Int16Array",
SpecialType.System_Int32 => "Int32Array",
SpecialType.System_Int64 => "BigInt64Array",
SpecialType.System_Byte => "Uint8Array",
SpecialType.System_UInt16 => "Uint16Array",
SpecialType.System_UInt32 => "Uint32Array",
SpecialType.System_UInt64 => "BigUint64Array",
SpecialType.System_Single => "Float32Array",
SpecialType.System_Double => "Float64Array",
_ => "unknown",
};
}
else if (namedType.OriginalDefinition.Name == "IList")
{
tsType = GetTSType(namedType.TypeArguments[0]) + "[]";
}
else if (namedType.OriginalDefinition.Name == "IReadOnlyList")
{
tsType = "readonly " + GetTSType(namedType.TypeArguments[0]) + "[]";
}
else if (namedType.OriginalDefinition.Name == "ICollection" ||
namedType.OriginalDefinition.Name == "ISet")
{
string elementTsType = GetTSType(namedType.TypeArguments[0]);
return $"Set<{elementTsType}>";
}
else if (namedType.OriginalDefinition.Name == "IReadOnlyCollection" ||
namedType.OriginalDefinition.Name == "IReadOnlySet")
{
string elementTsType = GetTSType(namedType.TypeArguments[0]);
return $"ReadonlySet<{elementTsType}>";
}
else if (namedType.OriginalDefinition.Name == "IEnumerable")
{
string elementTsType = GetTSType(namedType.TypeArguments[0]);
return $"Iterable<{elementTsType}>";
}
else if (namedType.OriginalDefinition.Name == "IDictionary")
{
string keyTSType = GetTSType(namedType.TypeArguments[0]);
string valueTSType = GetTSType(namedType.TypeArguments[1]);
tsType = $"Map<{keyTSType}, {valueTSType}>";
}
else if (namedType.OriginalDefinition.Name == "IReadOnlyDictionary")
{
string keyTSType = GetTSType(namedType.TypeArguments[0]);
string valueTSType = GetTSType(namedType.TypeArguments[1]);
tsType = $"ReadonlyMap<{keyTSType}, {valueTSType}>";
}
}
else if (_exportItems.Contains(type, SymbolEqualityComparer.Default))
{
tsType = type.Name;
}

if (type.NullableAnnotation == NullableAnnotation.Annotated &&
tsType != "any" && !tsType.EndsWith(" | null"))
{
// TODO: Get element type.
return "any[]";
tsType += " | null";
}

return "any";
return tsType;
}

private static string GetTSParameters(IMethodSymbol method, string indent)
private string GetTSParameters(IMethodSymbol method, string indent)
{
if (method.Parameters.Length == 0)
{
Expand Down
Loading

0 comments on commit c197b3c

Please sign in to comment.