diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 0a70692bc5176..8216bd6e3dabb 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1101,6 +1101,11 @@ private string GetRootJsonContextImplementation() { string contextTypeRef = _currentContext.ContextTypeRef; string contextTypeName = _currentContext.ContextType.Name; + int backTickIndex = contextTypeName.IndexOf('`'); + if (backTickIndex != -1) + { + contextTypeName = contextTypeName.Substring(0, backTickIndex); + } StringBuilder sb = new(); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index d767e229b9982..f16cfeedaa642 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -466,7 +466,7 @@ private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [Not } declarationElements[tokenCount] = "class"; - declarationElements[tokenCount + 1] = currentSymbol.Name; + declarationElements[tokenCount + 1] = GetClassDeclarationName(currentSymbol); (classDeclarationList ??= new List()).Add(string.Join(" ", declarationElements)); } @@ -478,6 +478,38 @@ private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [Not return true; } + private static string GetClassDeclarationName(INamedTypeSymbol typeSymbol) + { + if (typeSymbol.TypeArguments.Length == 0) + { + return typeSymbol.Name; + } + + StringBuilder sb = new StringBuilder(); + + sb.Append(typeSymbol.Name); + sb.Append('<'); + + bool first = true; + foreach (ITypeSymbol typeArg in typeSymbol.TypeArguments) + { + if (!first) + { + sb.Append(", "); + } + else + { + first = false; + } + + sb.Append(typeArg.Name); + } + + sb.Append('>'); + + return sb.ToString(); + } + private TypeGenerationSpec? GetRootSerializableType( SemanticModel compilationSemanticModel, AttributeSyntax attributeSyntax, diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs index 1abdee7d89d04..b41bc1cd6c34c 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; namespace System.Text.Json.Reflection { @@ -16,43 +14,61 @@ public static string GetCompilableName(this Type type) return GetCompilableName(type.GetElementType()) + "[]"; } - string compilableName; - - if (!type.IsGenericType) + if (type.IsGenericParameter) { - compilableName = type.FullName; + return type.Name; } - else - { - StringBuilder sb = new(); - string fullName = type.FullName; - int backTickIndex = fullName.IndexOf('`'); + StringBuilder sb = new(); - string baseName = fullName.Substring(0, backTickIndex); + sb.Append("global::"); - sb.Append(baseName); + string @namespace = type.Namespace; + if (!string.IsNullOrEmpty(@namespace) && @namespace != JsonConstants.GlobalNamespaceValue) + { + sb.Append(@namespace); + sb.Append('.'); + } - sb.Append('<'); + int argumentIndex = 0; + AppendTypeChain(sb, type, type.GetGenericArguments(), ref argumentIndex); - Type[] genericArgs = type.GetGenericArguments(); - int genericArgCount = genericArgs.Length; - List genericArgNames = new(genericArgCount); + return sb.ToString(); - for (int i = 0; i < genericArgCount; i++) + static void AppendTypeChain(StringBuilder sb, Type type, Type[] genericArguments, ref int argumentIndex) + { + Type declaringType = type.DeclaringType; + if (declaringType != null) { - genericArgNames.Add(GetCompilableName(genericArgs[i])); + AppendTypeChain(sb, declaringType, genericArguments, ref argumentIndex); + sb.Append('.'); } + int backTickIndex = type.Name.IndexOf('`'); + if (backTickIndex == -1) + { + sb.Append(type.Name); + } + else + { + sb.Append(type.Name, 0, backTickIndex); + + sb.Append('<'); - sb.Append(string.Join(", ", genericArgNames)); + int startIndex = argumentIndex; + argumentIndex = type.GetGenericArguments().Length; + for (int i = startIndex; i < argumentIndex; i++) + { + if (i != startIndex) + { + sb.Append(", "); + } - sb.Append('>'); + sb.Append(GetCompilableName(genericArguments[i])); + } - compilableName = sb.ToString(); + sb.Append('>'); + } } - - compilableName = compilableName.Replace("+", "."); - return "global::" + compilableName; } public static string GetTypeInfoPropertyName(this Type type) diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs index 39713ff6d9956..239ae60f4a0ef 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs @@ -40,7 +40,7 @@ public override string AssemblyQualifiedName { get { - if (_assemblyQualifiedName == null) + if (_assemblyQualifiedName == null && !IsGenericParameter) { StringBuilder sb = new(); @@ -111,13 +111,15 @@ public override string AssemblyQualifiedName public override Type BaseType => _typeSymbol.BaseType!.AsType(_metadataLoadContext); + public override Type DeclaringType => _typeSymbol.ContainingType?.ConstructedFrom.AsType(_metadataLoadContext); + private string? _fullName; public override string FullName { get { - if (_fullName == null) + if (_fullName == null && !IsGenericParameter) { StringBuilder sb = new(); @@ -133,24 +135,32 @@ public override string FullName } else { - sb.Append(Name); - - for (ISymbol currentSymbol = _typeSymbol.ContainingSymbol; currentSymbol != null && currentSymbol.Kind != SymbolKind.Namespace; currentSymbol = currentSymbol.ContainingSymbol) - { - sb.Insert(0, $"{currentSymbol.Name}+"); - } - if (!string.IsNullOrWhiteSpace(Namespace) && Namespace != JsonConstants.GlobalNamespaceValue) { - sb.Insert(0, $"{Namespace}."); + sb.Append(Namespace); + sb.Append('.'); } - if (this.IsGenericType && !ContainsGenericParameters) + AppendContainingTypes(sb, _typeSymbol); + + sb.Append(Name); + + if (IsGenericType && !ContainsGenericParameters) { sb.Append('['); + bool first = true; foreach (Type genericArg in GetGenericArguments()) { + if (!first) + { + sb.Append(','); + } + else + { + first = false; + } + sb.Append('['); sb.Append(genericArg.AssemblyQualifiedName); sb.Append(']'); @@ -164,6 +174,16 @@ public override string FullName } return _fullName; + + static void AppendContainingTypes(StringBuilder sb, ITypeSymbol typeSymbol) + { + if (typeSymbol.ContainingType != null) + { + AppendContainingTypes(sb, typeSymbol.ContainingType); + sb.Append(typeSymbol.ContainingType.MetadataName); + sb.Append('+'); + } + } } } @@ -207,20 +227,55 @@ public override bool IsEnum public override bool IsGenericType => _namedTypeSymbol?.IsGenericType == true; - public override bool ContainsGenericParameters => _namedTypeSymbol?.IsUnboundGenericType == true; + public override bool ContainsGenericParameters + { + get + { + if (IsGenericParameter) + { + return true; + } - public override bool IsGenericTypeDefinition => base.IsGenericTypeDefinition; + for (INamedTypeSymbol currentSymbol = _namedTypeSymbol; currentSymbol != null; currentSymbol = currentSymbol.ContainingType) + { + if (currentSymbol.TypeArguments.Any(arg => arg.TypeKind == TypeKind.TypeParameter)) + { + return true; + } + } + + return false; + } + } + + public override bool IsGenericTypeDefinition => IsGenericType && SymbolEqualityComparer.Default.Equals(_namedTypeSymbol, _namedTypeSymbol.ConstructedFrom); + + public override bool IsGenericParameter => _typeSymbol.TypeKind == TypeKind.TypeParameter; public INamespaceSymbol GetNamespaceSymbol => _typeSymbol.ContainingNamespace; public override Type[] GetGenericArguments() { - var args = new List(); - foreach (ITypeSymbol item in _namedTypeSymbol.TypeArguments) + if (!IsGenericType) { - args.Add(item.AsType(_metadataLoadContext)); + return EmptyTypes; } + + var args = new List(); + AddTypeArguments(args, _namedTypeSymbol, _metadataLoadContext); return args.ToArray(); + + static void AddTypeArguments(List args, INamedTypeSymbol typeSymbol, MetadataLoadContextInternal metadataLoadContext) + { + if (typeSymbol.ContainingType != null) + { + AddTypeArguments(args, typeSymbol.ContainingType, metadataLoadContext); + } + foreach (ITypeSymbol item in typeSymbol.TypeArguments) + { + args.Add(item.AsType(metadataLoadContext)); + } + } } public override Type GetGenericTypeDefinition() diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index 71a7e4b81a8c9..076530513efdb 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -118,4 +118,23 @@ internal partial class DictionaryTypeContext : JsonSerializerContext { } [JsonSerializable(typeof(JsonMessage))] public partial class PublicContext : JsonSerializerContext { } + + [JsonSerializable(typeof(JsonMessage))] + public partial class GenericContext : JsonSerializerContext { } + + public partial class ContextGenericContainer + { + [JsonSerializable(typeof(JsonMessage))] + public partial class NestedInGenericContainerContext : JsonSerializerContext { } + } + + [JsonSerializable(typeof(MyContainingClass.MyNestedClass.MyNestedNestedClass))] + [JsonSerializable(typeof(MyContainingClass.MyNestedClass.MyNestedNestedGenericClass))] + [JsonSerializable(typeof(MyContainingClass.MyNestedGenericClass.MyNestedGenericNestedClass))] + [JsonSerializable(typeof(MyContainingClass.MyNestedGenericClass.MyNestedGenericNestedGenericClass))] + [JsonSerializable(typeof(MyContainingGenericClass.MyNestedClass.MyNestedNestedClass))] + [JsonSerializable(typeof(MyContainingGenericClass.MyNestedClass.MyNestedNestedGenericClass))] + [JsonSerializable(typeof(MyContainingGenericClass.MyNestedGenericClass.MyNestedGenericNestedClass))] + [JsonSerializable(typeof(MyContainingGenericClass.MyNestedGenericClass.MyNestedGenericNestedGenericClass))] + internal partial class NestedGenericTypesContext : JsonSerializerContext { } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs index 6909f866b57d1..6b533a8da1234 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -21,6 +21,14 @@ public static void VariousNestingAndVisibilityLevelsAreSupported() Assert.NotNull(NestedPublicContext.NestedProtectedInternalClass.Default); } + [Fact] + public static void VariousGenericsAreSupported() + { + Assert.NotNull(GenericContext.Default); + Assert.NotNull(ContextGenericContainer.NestedInGenericContainerContext.Default); + Assert.NotNull(NestedGenericTypesContext.Default); + } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/63802", TargetFrameworkMonikers.NetFramework)] public static void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresent() diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index 290b65c1aee54..1addbb24e61bc 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -193,4 +193,32 @@ public class DerivedClass : PolymorphicClass public bool Boolean { get; set; } } } + + public class MyContainingClass + { + public class MyNestedClass + { + public class MyNestedNestedClass { } + public class MyNestedNestedGenericClass { } + } + public class MyNestedGenericClass + { + public class MyNestedGenericNestedClass { } + public class MyNestedGenericNestedGenericClass { } + } + } + + public class MyContainingGenericClass + { + public class MyNestedClass + { + public class MyNestedNestedClass { } + public class MyNestedNestedGenericClass { } + } + public class MyNestedGenericClass + { + public class MyNestedGenericNestedClass { } + public class MyNestedGenericNestedGenericClass { } + } + } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/TypeWrapperTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/TypeWrapperTests.cs index ca6d9dce1bd1e..8374978be3fd6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/TypeWrapperTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/TypeWrapperTests.cs @@ -198,5 +198,124 @@ public void MySecondMethod() { } receivedMembersWithAttributeNames ); } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/58226", TestPlatforms.Browser)] + public void VariousGenericSerializableTypesAreSupported() + { + string source = @" + using System; + using System.Collections.Generic; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(HelloWorld.MyClass.NestedGenericClass))] + [JsonSerializable(typeof(HelloWorld.MyGenericClass.NestedClass))] + [JsonSerializable(typeof(HelloWorld.MyGenericClass.NestedGenericClass))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public class MyClass + { + public class NestedGenericClass + { + } + } + + public class MyGenericClass + { + public class NestedClass + { + } + public class NestedGenericClass + { + } + } + }"; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + + JsonSourceGenerator generator = new JsonSourceGenerator(); + + Compilation outCompilation = CompilationHelper.RunGenerators(compilation, out ImmutableArray generatorDiags, generator); + + // Make sure compilation was successful. + Assert.Empty(generatorDiags.Where(diag => diag.Severity.Equals(DiagnosticSeverity.Error))); + Assert.Empty(outCompilation.GetDiagnostics().Where(diag => diag.Severity.Equals(DiagnosticSeverity.Error))); + + Dictionary types = generator.GetSerializableTypes(); + Assert.Equal(4, types.Count); + + // Check for generic class. + Type originalType = typeof(Dictionary); + Type foundType = types[originalType.FullName]; + Assert.Equal(originalType, foundType, TestComparerForType.Instance); + Assert.Equal(originalType.GetGenericArguments(), foundType.GetGenericArguments(), TestComparerForType.Instance); + + // Check for generic type definition. + Type foundGenericTypeDefinition = foundType.GetGenericTypeDefinition(); + Type originalGenericTypeDefinition = originalType.GetGenericTypeDefinition(); + Assert.Equal(originalGenericTypeDefinition, foundGenericTypeDefinition, TestComparerForType.Instance); + Assert.Equal(originalGenericTypeDefinition.GetGenericArguments(), foundGenericTypeDefinition.GetGenericArguments(), TestComparerForType.Instance); + + // Check for nested generic class. + foundType = types.Values.Single(t => t.FullName.Contains("MyClass") && t.FullName.Contains("NestedGenericClass")); + Assert.Equal("NestedGenericClass`1", foundType.Name); + Assert.Equal($"HelloWorld.MyClass+NestedGenericClass`1[[{typeof(string).AssemblyQualifiedName}]]", foundType.FullName); + Assert.True(foundType.IsGenericType); + Assert.Equal(new[] { typeof(string) }, foundType.GetGenericArguments(), TestComparerForType.Instance); + + // Check for declaring type. + foundType = foundType.DeclaringType; + Assert.Equal("MyClass", foundType.Name); + Assert.Equal("HelloWorld.MyClass", foundType.FullName); + Assert.False(foundType.IsGenericType); + + // Check for class nested in generic class. + foundType = types.Values.Single(t => t.FullName.Contains("MyGenericClass") && t.FullName.Contains("NestedClass")); + Assert.Equal("NestedClass", foundType.Name); + Assert.Equal($"HelloWorld.MyGenericClass`1+NestedClass[[{typeof(string).AssemblyQualifiedName}]]", foundType.FullName); + Assert.True(foundType.IsGenericType); + Assert.Equal(new[] { typeof(string) }, foundType.GetGenericArguments(), TestComparerForType.Instance); + + // Check for generic class nested in generic class. + foundType = types.Values.Single(t => t.FullName.Contains("MyGenericClass") && t.FullName.Contains("NestedGenericClass")); + Assert.Equal("NestedGenericClass`1", foundType.Name); + Assert.Equal($"HelloWorld.MyGenericClass`1+NestedGenericClass`1[[{typeof(string).AssemblyQualifiedName}],[{typeof(int).AssemblyQualifiedName}]]", foundType.FullName); + Assert.True(foundType.IsGenericType); + Assert.Equal(new[] { typeof(string), typeof(int) }, foundType.GetGenericArguments(), TestComparerForType.Instance); + + // Check for generic declaring type. + foundType = foundType.DeclaringType; + Assert.Equal("MyGenericClass`1", foundType.Name); + Assert.Equal("HelloWorld.MyGenericClass`1", foundType.FullName); + Assert.True(foundType.IsGenericType); + Assert.True(foundType.IsGenericTypeDefinition); + Assert.Equal("T1", foundType.GetGenericArguments().Single().Name); + } + + sealed class TestComparerForType : EqualityComparer + { + public static TestComparerForType Instance { get; } = new TestComparerForType(); + public override bool Equals(Type? x, Type? y) + { + if (x is null || y is null) + { + return x == y; + } + return x.Name == y.Name && + x.FullName == y.FullName && + x.AssemblyQualifiedName == y.AssemblyQualifiedName && + Instance.Equals(x.DeclaringType, y.DeclaringType) && + x.IsGenericType == y.IsGenericType && + x.IsGenericParameter == y.IsGenericParameter && + x.IsGenericTypeDefinition == y.IsGenericTypeDefinition && + x.ContainsGenericParameters == y.ContainsGenericParameters; + } + public override int GetHashCode(Type obj) => obj.Name.GetHashCode(); + } } }