Skip to content

Commit

Permalink
Add generic type transpilation (#164)
Browse files Browse the repository at this point in the history
* implement generic type transpilation

* extract `GenericTypeParameterMapper` to its own file

* fix wrong type name in exception message

* add another test for inheritance of a concrete generic type with the same name

* apply PR feedback/comments
  • Loading branch information
backfromexile authored Feb 13, 2024
1 parent de3e3e1 commit 082cc01
Show file tree
Hide file tree
Showing 7 changed files with 457 additions and 17 deletions.
9 changes: 8 additions & 1 deletion src/Tapper/DefaultTypeMapperProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ public class DefaultTypeMapperProvider : ITypeMapperProvider
{
private readonly ArrayTypeMapper _arrayTypeMapper;
private readonly TupleTypeMapper _tupleTypeMapper;
private readonly GenericTypeParameterMapper _genericTypeParameterMapper;

private readonly IDictionary<ITypeSymbol, ITypeMapper> _mappers;

public DefaultTypeMapperProvider(Compilation compilation, bool includeReferencedAssemblies)
{
_arrayTypeMapper = new ArrayTypeMapper(compilation);
_tupleTypeMapper = new TupleTypeMapper();
_genericTypeParameterMapper = new GenericTypeParameterMapper();

var dateTimeTypeMapper = new DateTimeTypeMapper(compilation);
var dateTimeOffsetTypeMapper = new DateTimeOffsetTypeMapper(compilation);
Expand Down Expand Up @@ -49,7 +51,12 @@ public ITypeMapper GetTypeMapper(ITypeSymbol type)
return _arrayTypeMapper;
}

var sourceType = type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType
if (type is ITypeParameterSymbol)
{
return _genericTypeParameterMapper;
}

var sourceType = type is INamedTypeSymbol namedTypeSymbol
? namedTypeSymbol.ConstructedFrom
: type;

Expand Down
17 changes: 17 additions & 0 deletions src/Tapper/GenericTypeParameterMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using Microsoft.CodeAnalysis;

namespace Tapper;

internal class GenericTypeParameterMapper : ITypeMapper
{
public ITypeSymbol Assign { get; } = default!;

public string MapTo(ITypeSymbol typeSymbol, ITranspilationOptions options)
{
if (typeSymbol is not ITypeParameterSymbol typeParameterSymbol)
throw new InvalidOperationException($"{nameof(GenericTypeParameterMapper)} does not support {typeSymbol.ToDisplayString()}.");

return typeParameterSymbol.Name;
}
}
15 changes: 14 additions & 1 deletion src/Tapper/TypeMappers/SourceTypeMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Tapper.TypeMappers;
Expand All @@ -14,8 +15,20 @@ public SourceTypeMapper(INamedTypeSymbol sourceTypes)

public string MapTo(ITypeSymbol typeSymbol, ITranspilationOptions options)
{
if (SymbolEqualityComparer.Default.Equals(typeSymbol, Assign))
var symbol = (typeSymbol as INamedTypeSymbol)?.ConstructedFrom ?? typeSymbol;

if (SymbolEqualityComparer.Default.Equals(symbol, Assign))
{
if (typeSymbol is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
{
var mappedTypeParameters = namedTypeSymbol.TypeArguments.Select(param =>
{
var mapper = options.TypeMapperProvider.GetTypeMapper(param);
return mapper.MapTo(param, options);
});
return $"{typeSymbol.Name}<{string.Join(", ", mappedTypeParameters)}>";
}

return typeSymbol.Name;
}

Expand Down
46 changes: 32 additions & 14 deletions src/Tapper/TypeTranslators/DefaultMessageTypeTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ public void Translate(ref CodeWriter codeWriter, INamedTypeSymbol typeSymbol, IT
.IgnoreStatic()
.ToArray();

codeWriter.Append($"/** Transpiled from {typeSymbol.ToDisplayString()} */{newLineString}");
codeWriter.Append($"export type {typeSymbol.Name} = {{{newLineString}");

codeWriter.Append($"/** Transpiled from {typeSymbol.OriginalDefinition.ToDisplayString()} */{newLineString}");
codeWriter.Append($"export type {MessageTypeTranslatorHelper.GetGenericTypeName(typeSymbol)} = {{{newLineString}");

foreach (var member in members)
{
Expand All @@ -43,7 +44,7 @@ public void Translate(ref CodeWriter codeWriter, INamedTypeSymbol typeSymbol, IT

if (MessageTypeTranslatorHelper.IsSourceType(typeSymbol.BaseType, options))
{
codeWriter.Append($" & {typeSymbol.BaseType.Name};");
codeWriter.Append($" & {MessageTypeTranslatorHelper.GetConcreteTypeName(typeSymbol.BaseType, options)};");
}

codeWriter.Append(newLineString);
Expand All @@ -52,6 +53,33 @@ public void Translate(ref CodeWriter codeWriter, INamedTypeSymbol typeSymbol, IT

file static class MessageTypeTranslatorHelper
{
public static string GetConcreteTypeName(INamedTypeSymbol typeSymbol, ITranspilationOptions options)
{
var genericTypeArguments = "";
if (typeSymbol.IsGenericType)
{
var mappedGenericTypeArguments = typeSymbol.TypeArguments.Select(typeArg =>
{
var mapper = options.TypeMapperProvider.GetTypeMapper(typeArg);
return mapper.MapTo(typeArg, options);
});
genericTypeArguments = $"<{string.Join(", ", mappedGenericTypeArguments)}>";
}

return $"{typeSymbol.Name}{genericTypeArguments}";
}

public static string GetGenericTypeName(INamedTypeSymbol typeSymbol)
{
var genericTypeParameters = "";
if (typeSymbol.IsGenericType)
{
genericTypeParameters = $"<{string.Join(", ", typeSymbol.TypeParameters.Select(param => param.Name))}>";
}

return $"{typeSymbol.Name}{genericTypeParameters}";
}

public static (ITypeSymbol TypeSymbol, bool IsNullable) GetMemberTypeSymbol(ISymbol symbol, ITranspilationOptions options)
{
if (symbol is IPropertySymbol propertySymbol)
Expand All @@ -62,11 +90,6 @@ public static (ITypeSymbol TypeSymbol, bool IsNullable) GetMemberTypeSymbol(ISym
{
if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
if (!namedTypeSymbol.IsGenericType)
{
return (typeSymbol, false);
}

if (namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
{
return (namedTypeSymbol.TypeArguments[0], true);
Expand All @@ -87,11 +110,6 @@ public static (ITypeSymbol TypeSymbol, bool IsNullable) GetMemberTypeSymbol(ISym
{
if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
if (!namedTypeSymbol.IsGenericType)
{
return (typeSymbol, false);
}

if (namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
{
return (namedTypeSymbol.TypeArguments[0], true);
Expand All @@ -112,7 +130,7 @@ public static bool IsSourceType([NotNullWhen(true)] INamedTypeSymbol? typeSymbol
{
if (typeSymbol is not null && typeSymbol.SpecialType != SpecialType.System_Object)
{
if (options.SourceTypes.Contains(typeSymbol, SymbolEqualityComparer.Default))
if (options.SourceTypes.Contains(typeSymbol.ConstructedFrom, SymbolEqualityComparer.Default))
{
return true;
}
Expand Down
66 changes: 66 additions & 0 deletions tests/Tapper.Test.SourceTypes/GenericClasses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Space1;
using Tapper;

namespace Tapper.Test.SourceTypes
{

[TranspilationSource]
public class GenericClass1<T>
{
public required string StringProperty { get; set; }
public required T GenericProperty { get; set; }
}

[TranspilationSource]
public class NestedGenericClass<T1, T2>
{
public required string StringProperty { get; set; }
public required T1 GenericProperty { get; set; }
public required GenericClass1<T1> GenericClass1Property { get; set; }
public required GenericClass2<T1, T2> GenericClass2Property { get; set; }
}

[TranspilationSource]
public class DeeplyNestedGenericClass<A, B, C>
{
public required string StringProperty { get; set; }
public required A GenericPropertyA { get; set; }
public required B GenericPropertyB { get; set; }
public required GenericClass1<A> GenericClass1Property { get; set; }
public required GenericClass2<B, C> GenericClass2Property { get; set; }
public required NestedGenericClass<string, B> NestedGenericClassProperty { get; set; }
}

[TranspilationSource]
public class InheritedGenericClass2<T1, T2> : GenericClass1<T1>
{
public required T2 GenericPropertyT2 { get; set; }
}


[TranspilationSource]
public class InheritedConcreteGenericClass : GenericClass2<bool, int>
{
}

[TranspilationSource]
public class InheritedGenericClassWithTheSameName<T>
{
public required T GenericProperty { get; set; }
}
[TranspilationSource]
public class InheritedGenericClassWithTheSameName : InheritedGenericClassWithTheSameName<string>
{
}
}
namespace Space1
{

[TranspilationSource]
public class GenericClass2<T1, T2>
{
public required string StringProperty { get; set; }
public required T1 GenericProperty1 { get; set; }
public required T2 GenericProperty2 { get; set; }
}
}
7 changes: 6 additions & 1 deletion tests/Tapper.Tests/CompilationSingleton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ static CompilationSingleton()
File.ReadAllText("../../../../Tapper.Test.SourceTypes/NestedType.cs"),
options);

var genericClassSyntax = CSharpSyntaxTree.ParseText(
File.ReadAllText("../../../../Tapper.Test.SourceTypes/GenericClasses.cs"),
options);

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable);

Expand Down Expand Up @@ -91,7 +95,8 @@ static CompilationSingleton()
attributeAnnotatedSyntax,
messagePackAttributesSyntax,
partialClassSyntax,
nestedTypeSyntax
nestedTypeSyntax,
genericClassSyntax,
},
references: references,
options: compilationOptions);
Expand Down
Loading

0 comments on commit 082cc01

Please sign in to comment.