From 082cc01160c0fd06af36002d93c3a7470c17245c Mon Sep 17 00:00:00 2001 From: backfromexile Date: Tue, 13 Feb 2024 11:51:16 +0100 Subject: [PATCH] Add generic type transpilation (#164) * 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 --- src/Tapper/DefaultTypeMapperProvider.cs | 9 +- src/Tapper/GenericTypeParameterMapper.cs | 17 + src/Tapper/TypeMappers/SourceTypeMapper.cs | 15 +- .../DefaultMessageTypeTranslator.cs | 46 ++- .../Tapper.Test.SourceTypes/GenericClasses.cs | 66 ++++ tests/Tapper.Tests/CompilationSingleton.cs | 7 +- tests/Tapper.Tests/GenericTypeTest.cs | 314 ++++++++++++++++++ 7 files changed, 457 insertions(+), 17 deletions(-) create mode 100644 src/Tapper/GenericTypeParameterMapper.cs create mode 100644 tests/Tapper.Test.SourceTypes/GenericClasses.cs create mode 100644 tests/Tapper.Tests/GenericTypeTest.cs diff --git a/src/Tapper/DefaultTypeMapperProvider.cs b/src/Tapper/DefaultTypeMapperProvider.cs index 7ab00d3..b6a7cd8 100644 --- a/src/Tapper/DefaultTypeMapperProvider.cs +++ b/src/Tapper/DefaultTypeMapperProvider.cs @@ -10,6 +10,7 @@ public class DefaultTypeMapperProvider : ITypeMapperProvider { private readonly ArrayTypeMapper _arrayTypeMapper; private readonly TupleTypeMapper _tupleTypeMapper; + private readonly GenericTypeParameterMapper _genericTypeParameterMapper; private readonly IDictionary _mappers; @@ -17,6 +18,7 @@ public DefaultTypeMapperProvider(Compilation compilation, bool includeReferenced { _arrayTypeMapper = new ArrayTypeMapper(compilation); _tupleTypeMapper = new TupleTypeMapper(); + _genericTypeParameterMapper = new GenericTypeParameterMapper(); var dateTimeTypeMapper = new DateTimeTypeMapper(compilation); var dateTimeOffsetTypeMapper = new DateTimeOffsetTypeMapper(compilation); @@ -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; diff --git a/src/Tapper/GenericTypeParameterMapper.cs b/src/Tapper/GenericTypeParameterMapper.cs new file mode 100644 index 0000000..1961a5c --- /dev/null +++ b/src/Tapper/GenericTypeParameterMapper.cs @@ -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; + } +} diff --git a/src/Tapper/TypeMappers/SourceTypeMapper.cs b/src/Tapper/TypeMappers/SourceTypeMapper.cs index 120df59..cd07c76 100644 --- a/src/Tapper/TypeMappers/SourceTypeMapper.cs +++ b/src/Tapper/TypeMappers/SourceTypeMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Microsoft.CodeAnalysis; namespace Tapper.TypeMappers; @@ -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; } diff --git a/src/Tapper/TypeTranslators/DefaultMessageTypeTranslator.cs b/src/Tapper/TypeTranslators/DefaultMessageTypeTranslator.cs index 088e69d..f1cd590 100644 --- a/src/Tapper/TypeTranslators/DefaultMessageTypeTranslator.cs +++ b/src/Tapper/TypeTranslators/DefaultMessageTypeTranslator.cs @@ -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) { @@ -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); @@ -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) @@ -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); @@ -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); @@ -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; } diff --git a/tests/Tapper.Test.SourceTypes/GenericClasses.cs b/tests/Tapper.Test.SourceTypes/GenericClasses.cs new file mode 100644 index 0000000..1022037 --- /dev/null +++ b/tests/Tapper.Test.SourceTypes/GenericClasses.cs @@ -0,0 +1,66 @@ +using Space1; +using Tapper; + +namespace Tapper.Test.SourceTypes +{ + + [TranspilationSource] + public class GenericClass1 + { + public required string StringProperty { get; set; } + public required T GenericProperty { get; set; } + } + + [TranspilationSource] + public class NestedGenericClass + { + public required string StringProperty { get; set; } + public required T1 GenericProperty { get; set; } + public required GenericClass1 GenericClass1Property { get; set; } + public required GenericClass2 GenericClass2Property { get; set; } + } + + [TranspilationSource] + public class DeeplyNestedGenericClass + { + public required string StringProperty { get; set; } + public required A GenericPropertyA { get; set; } + public required B GenericPropertyB { get; set; } + public required GenericClass1 GenericClass1Property { get; set; } + public required GenericClass2 GenericClass2Property { get; set; } + public required NestedGenericClass NestedGenericClassProperty { get; set; } + } + + [TranspilationSource] + public class InheritedGenericClass2 : GenericClass1 + { + public required T2 GenericPropertyT2 { get; set; } + } + + + [TranspilationSource] + public class InheritedConcreteGenericClass : GenericClass2 + { + } + + [TranspilationSource] + public class InheritedGenericClassWithTheSameName + { + public required T GenericProperty { get; set; } + } + [TranspilationSource] + public class InheritedGenericClassWithTheSameName : InheritedGenericClassWithTheSameName + { + } +} +namespace Space1 +{ + + [TranspilationSource] + public class GenericClass2 + { + public required string StringProperty { get; set; } + public required T1 GenericProperty1 { get; set; } + public required T2 GenericProperty2 { get; set; } + } +} diff --git a/tests/Tapper.Tests/CompilationSingleton.cs b/tests/Tapper.Tests/CompilationSingleton.cs index c4f6191..63e2619 100644 --- a/tests/Tapper.Tests/CompilationSingleton.cs +++ b/tests/Tapper.Tests/CompilationSingleton.cs @@ -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); @@ -91,7 +95,8 @@ static CompilationSingleton() attributeAnnotatedSyntax, messagePackAttributesSyntax, partialClassSyntax, - nestedTypeSyntax + nestedTypeSyntax, + genericClassSyntax, }, references: references, options: compilationOptions); diff --git a/tests/Tapper.Tests/GenericTypeTest.cs b/tests/Tapper.Tests/GenericTypeTest.cs new file mode 100644 index 0000000..f958581 --- /dev/null +++ b/tests/Tapper.Tests/GenericTypeTest.cs @@ -0,0 +1,314 @@ +using Space1; +using Tapper.Test.SourceTypes; +using Xunit; +using Xunit.Abstractions; + +namespace Tapper.Tests; + +public class GenericTypeTest +{ + private readonly ITestOutputHelper _output; + + public GenericTypeTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Test_GenericClass1() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(GenericClass1<>); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.GenericClass1 */ +export type GenericClass1 = { + /** Transpiled from string */ + StringProperty: string; + /** Transpiled from T */ + GenericProperty: T; +} +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_GenericClass2() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(GenericClass2<,>); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Space1.GenericClass2 */ +export type GenericClass2 = { + /** Transpiled from string */ + StringProperty: string; + /** Transpiled from T1 */ + GenericProperty1: T1; + /** Transpiled from T2 */ + GenericProperty2: T2; +} +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_NestedGenericClass() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(NestedGenericClass<,>); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.NestedGenericClass */ +export type NestedGenericClass = { + /** Transpiled from string */ + StringProperty: string; + /** Transpiled from T1 */ + GenericProperty: T1; + /** Transpiled from Tapper.Test.SourceTypes.GenericClass1 */ + GenericClass1Property: GenericClass1; + /** Transpiled from Space1.GenericClass2 */ + GenericClass2Property: GenericClass2; +} +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_DeeplyNestedGenericClass() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(DeeplyNestedGenericClass<,,>); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.DeeplyNestedGenericClass */ +export type DeeplyNestedGenericClass = { + /** Transpiled from string */ + StringProperty: string; + /** Transpiled from A */ + GenericPropertyA: A; + /** Transpiled from B */ + GenericPropertyB: B; + /** Transpiled from Tapper.Test.SourceTypes.GenericClass1 */ + GenericClass1Property: GenericClass1; + /** Transpiled from Space1.GenericClass2 */ + GenericClass2Property: GenericClass2; + /** Transpiled from Tapper.Test.SourceTypes.NestedGenericClass */ + NestedGenericClassProperty: NestedGenericClass; +} +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_InheritedGenericClass() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(InheritedGenericClass2<,>); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.InheritedGenericClass2 */ +export type InheritedGenericClass2 = { + /** Transpiled from T2 */ + GenericPropertyT2: T2; +} & GenericClass1; +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_InheritedConcreteGenericClass() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(InheritedConcreteGenericClass); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.InheritedConcreteGenericClass */ +export type InheritedConcreteGenericClass = { +} & GenericClass2; +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Test_InheritedGenericClassWithTheSameName() + { + var compilation = CompilationSingleton.Compilation; + + var options = new TranspilationOptions( + compilation, + SerializerOption.Json, + NamingStyle.None, + EnumStyle.Value, + NewLineOption.Lf, + 2, + false, + true + ); + + var codeGenerator = new TypeScriptCodeGenerator(compilation, options); + + var type = typeof(InheritedGenericClassWithTheSameName); + var typeSymbol = compilation.GetTypeByMetadataName(type.FullName!)!; + + var writer = new CodeWriter(); + + codeGenerator.AddType(typeSymbol, ref writer); + + var code = writer.ToString(); + var gt = @"/** Transpiled from Tapper.Test.SourceTypes.InheritedGenericClassWithTheSameName */ +export type InheritedGenericClassWithTheSameName = { +} & InheritedGenericClassWithTheSameName; +"; + + _output.WriteLine(code); + _output.WriteLine(gt); + + + Assert.Equal(gt, code, ignoreLineEndingDifferences: true); + } +}