diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs b/src/libraries/System.Text.Json/Common/JsonIgnoreCondition.cs similarity index 87% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs rename to src/libraries/System.Text.Json/Common/JsonIgnoreCondition.cs index f54b45f97cbf9a..5f2595961a868f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs +++ b/src/libraries/System.Text.Json/Common/JsonIgnoreCondition.cs @@ -7,10 +7,15 @@ namespace System.Text.Json.Serialization /// When specified on , /// determines when properties and fields across the type graph are ignored. /// When specified on , controls whether - /// a property is ignored during serialization and deserialization. This option + /// a property or field is ignored during serialization and deserialization. This option /// overrides the setting on . /// - public enum JsonIgnoreCondition +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + enum JsonIgnoreCondition { /// /// Property is never ignored during serialization or deserialization. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs b/src/libraries/System.Text.Json/Common/JsonNumberHandling.cs similarity index 94% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs rename to src/libraries/System.Text.Json/Common/JsonNumberHandling.cs index c8eabaad5089c7..8dafef22a1982b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberHandling.cs +++ b/src/libraries/System.Text.Json/Common/JsonNumberHandling.cs @@ -7,12 +7,18 @@ namespace System.Text.Json.Serialization /// Determines how handles numbers when serializing and deserializing. /// [Flags] - public enum JsonNumberHandling +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + enum JsonNumberHandling { /// /// Numbers will only be read from tokens and will only be written as JSON numbers (without quotes). /// Strict = 0x0, + /// /// Numbers can be read from tokens. /// Does not prevent numbers from being read from token. @@ -20,10 +26,12 @@ public enum JsonNumberHandling /// Leading or trailing trivia within the string token, including whitespace, is not allowed. /// AllowReadingFromString = 0x1, + /// /// Numbers will be written as JSON strings (with quotes), not as JSON numbers. /// WriteAsString = 0x2, + /// /// The "NaN", "Infinity", and "-Infinity" tokens can be read as /// floating-point constants, and the and values for these diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index cf494364f9866e..433c378f98852a 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -27,18 +27,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests.csproj", "{607D1960-1428-40D5-8AC4-D98E3C9BCF47}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3C544454-BD8B-44F4-A174-B61F18957613}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{EC8CE194-261A-4115-9582-E2DB1A25CAFB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA47-4B7D-AEC2-FA94DB23A423}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}" +EndProject Global GlobalSection(NestedProjects) = preSolution {102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613} - {607D1960-1428-40D5-8AC4-D98E3C9BCF47} = {3C544454-BD8B-44F4-A174-B61F18957613} {73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} {25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB} @@ -52,6 +59,10 @@ Global {7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423} + {6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613} + {18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613} EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,10 +125,22 @@ Global {1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Release|Any CPU.Build.0 = Release|Any CPU - {607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Release|Any CPU.Build.0 = Release|Any CPU + {6485EED4-C313-4551-9865-8ADCED603629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6485EED4-C313-4551-9865-8ADCED603629}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6485EED4-C313-4551-9865-8ADCED603629}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6485EED4-C313-4551-9865-8ADCED603629}.Release|Any CPU.Build.0 = Release|Any CPU + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Release|Any CPU.Build.0 = Release|Any CPU + {33599A6C-F340-4E1B-9B4D-CB8946C22140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33599A6C-F340-4E1B-9B4D-CB8946C22140}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33599A6C-F340-4E1B-9B4D-CB8946C22140}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33599A6C-F340-4E1B-9B4D-CB8946C22140}.Release|Any CPU.Build.0 = Release|Any CPU + {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libraries/System.Text.Json/gen/ClassType.cs b/src/libraries/System.Text.Json/gen/ClassType.cs new file mode 100644 index 00000000000000..fa6aab3d2573bd --- /dev/null +++ b/src/libraries/System.Text.Json/gen/ClassType.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Text.Json.SourceGeneration +{ + internal enum ClassType + { + TypeUnsupportedBySourceGen = 0, + Object = 1, + KnownType = 2, + TypeWithDesignTimeProvidedCustomConverter = 3, + Enumerable = 4, + Dictionary = 5, + Nullable = 6, + Enum = 7, + } +} diff --git a/src/libraries/System.Text.Json/gen/CollectionType.cs b/src/libraries/System.Text.Json/gen/CollectionType.cs new file mode 100644 index 00000000000000..6f736698f47b8f --- /dev/null +++ b/src/libraries/System.Text.Json/gen/CollectionType.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Text.Json.SourceGeneration +{ + internal enum CollectionType + { + NotApplicable = 0, + Array = 1, + List = 2, + IEnumerable = 3, + IList = 4, + Dictionary = 5 + } +} diff --git a/src/libraries/System.Text.Json/gen/IsExternalInit.cs b/src/libraries/System.Text.Json/gen/IsExternalInit.cs new file mode 100644 index 00000000000000..d5984b4be38351 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/IsExternalInit.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// Dummy class so C# init-only properties can compile on NetStandard. + /// + internal sealed class IsExternalInit { } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSerializableSyntaxReceiver.cs b/src/libraries/System.Text.Json/gen/JsonSerializableSyntaxReceiver.cs new file mode 100644 index 00000000000000..f60f4cfb686f9d --- /dev/null +++ b/src/libraries/System.Text.Json/gen/JsonSerializableSyntaxReceiver.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace System.Text.Json.SourceGeneration +{ + internal sealed class JsonSerializableSyntaxReceiver : ISyntaxReceiver + { + public List CompilationUnits { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is CompilationUnitSyntax compilationUnit) + { + CompilationUnits.Add(compilationUnit); + } + } + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs new file mode 100644 index 00000000000000..ef2412f6aaf901 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs @@ -0,0 +1,128 @@ +// 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.Linq; +using System.Reflection; +using System.Text.Json.SourceGeneration.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Generates source code to optimize serialization and deserialization with JsonSerializer. + /// + [Generator] + public sealed class JsonSourceGenerator : ISourceGenerator + { + private JsonSourceGeneratorHelper? _helper; + + /// + /// Helper for unit tests. + /// + public Dictionary? SerializableTypes => _helper.GetSerializableTypes(); + + /// + /// Registers a syntax resolver to receive compilation units. + /// + /// + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new JsonSerializableSyntaxReceiver()); + } + + /// + /// Generates source code to optimize serialization and deserialization with JsonSerializer. + /// + /// + public void Execute(GeneratorExecutionContext executionContext) + { + Compilation compilation = executionContext.Compilation; + + const string JsonSerializableAttributeName = "System.Text.Json.Serialization.JsonSerializableAttribute"; + INamedTypeSymbol jsonSerializableAttribute = compilation.GetTypeByMetadataName(JsonSerializableAttributeName); + if (jsonSerializableAttribute == null) + { + return; + } + + JsonSerializableSyntaxReceiver receiver = (JsonSerializableSyntaxReceiver)executionContext.SyntaxReceiver; + MetadataLoadContextInternal metadataLoadContext = new(compilation); + + TypeExtensions.NullableOfTType = metadataLoadContext.Resolve(typeof(Nullable<>)); + + JsonSourceGeneratorHelper helper = new(executionContext, metadataLoadContext); + _helper = helper; + + // Discover serializable types indicated by JsonSerializableAttribute. + foreach (CompilationUnitSyntax compilationUnit in receiver.CompilationUnits) + { + SemanticModel compilationSemanticModel = executionContext.Compilation.GetSemanticModel(compilationUnit.SyntaxTree); + + foreach (AttributeListSyntax attributeListSyntax in compilationUnit.AttributeLists) + { + AttributeSyntax attributeSyntax = attributeListSyntax.Attributes.First(); + IMethodSymbol attributeSymbol = compilationSemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; + + if (attributeSymbol == null || !jsonSerializableAttribute.Equals(attributeSymbol.ContainingType, SymbolEqualityComparer.Default)) + { + // Not the right attribute. + continue; + } + + // Get JsonSerializableAttribute arguments. + IEnumerable attributeArguments = attributeSyntax.DescendantNodes().Where(node => node is AttributeArgumentSyntax); + + ITypeSymbol? typeSymbol = null; + string? typeInfoPropertyName = null; + + int i = 0; + foreach (AttributeArgumentSyntax node in attributeArguments) + { + if (i == 0) + { + TypeOfExpressionSyntax? typeNode = node.ChildNodes().Single() as TypeOfExpressionSyntax; + if (typeNode != null) + { + ExpressionSyntax typeNameSyntax = (ExpressionSyntax)typeNode.ChildNodes().Single(); + typeSymbol = compilationSemanticModel.GetTypeInfo(typeNameSyntax).ConvertedType; + } + } + else if (i == 1) + { + // Obtain the optional TypeInfoPropertyName string property on the attribute, if present. + SyntaxNode? typeInfoPropertyNameNode = node.ChildNodes().ElementAtOrDefault(1); + if (typeInfoPropertyNameNode != null) + { + typeInfoPropertyName = typeInfoPropertyNameNode.GetFirstToken().ValueText; + } + } + + i++; + } + + if (typeSymbol == null) + { + continue; + } + + + Type type = new TypeWrapper(typeSymbol, metadataLoadContext); + if (type.Namespace == "") + { + // typeof() reference where the type's name isn't fully qualified. + // The compilation is not valid and the user needs to fix their code. + // The compiler will notify the user so we don't have to. + return; + } + + helper.RegisterRootSerializableType(type, typeInfoPropertyName); + } + } + + helper.GenerateSerializationMetadata(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.Generate.cs b/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.Generate.cs new file mode 100644 index 00000000000000..040c11cf9acf0e --- /dev/null +++ b/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.Generate.cs @@ -0,0 +1,651 @@ +// 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; +using System.Text.Json.Serialization; +using System.Text.Json.SourceGeneration.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace System.Text.Json.SourceGeneration +{ + internal sealed partial class JsonSourceGeneratorHelper + { + private readonly string _generationNamespace; + + // TODO: consider public option for this. + // Converter-honoring logic generation can be simplified + // if we don't plan to have a feature around this. + private bool _honorRuntimeProvidedCustomConverters = true; + + private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; + + private const string JsonContextDeclarationSource = "internal partial class JsonContext : JsonSerializerContext"; + + private const string OptionsInstanceVariableName = "Options"; + + private const string PropInitFuncVarName = "PropInitFunc"; + + private const string JsonMetadataServicesClassName = "JsonMetadataServices"; + + private const string CreateValueInfoMethodName = "CreateValueInfo"; + + private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor( + "SYSLIB2021", + "Did not generate serialization metadata for type", + "Did not generate serialization metadata for type {0}", + "JSON source generation", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Error message: {2}"); + + private static DiagnosticDescriptor DuplicateTypeName { get; } = new DiagnosticDescriptor( + "SYSLIB2022", + "Duplicate type name", + "There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.", + "JSON source generation", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Error message: {2}"); + + /// + /// Types that we have initiated serialization metadata generation for. A type may be discoverable in the object graph, + /// but not reachable for serialization (e.g. it is [JsonIgnore]'d); thus we maintain a separate cache. + /// + private readonly HashSet _typesWithMetadataGenerated = new(); + + private void GenerateTypeMetadata(TypeMetadata typeMetadata) + { + Debug.Assert(typeMetadata != null); + + if (_typesWithMetadataGenerated.Contains(typeMetadata)) + { + return; + } + + _typesWithMetadataGenerated.Add(typeMetadata); + + string source; + + switch (typeMetadata.ClassType) + { + case ClassType.KnownType: + { + source = GenerateForTypeWithKnownConverter(typeMetadata); + } + break; + case ClassType.TypeWithDesignTimeProvidedCustomConverter: + { + source = GenerateForTypeWithUnknownConverter(typeMetadata); + } + break; + case ClassType.Nullable: + { + source = GenerateForNullable(typeMetadata); + + GenerateTypeMetadata(typeMetadata.NullableUnderlyingTypeMetadata); + } + break; + case ClassType.Enum: + { + source = GenerateForEnum(typeMetadata); + } + break; + case ClassType.Enumerable: + { + source = GenerateForCollection(typeMetadata); + + GenerateTypeMetadata(typeMetadata.CollectionValueTypeMetadata); + } + break; + case ClassType.Dictionary: + { + source = GenerateForCollection(typeMetadata); + + GenerateTypeMetadata(typeMetadata.CollectionKeyTypeMetadata); + GenerateTypeMetadata(typeMetadata.CollectionValueTypeMetadata); + } + break; + case ClassType.Object: + { + source = GenerateForObject(typeMetadata); + + if (typeMetadata.PropertiesMetadata != null) + { + foreach (PropertyMetadata metadata in typeMetadata.PropertiesMetadata) + { + GenerateTypeMetadata(metadata.TypeMetadata); + } + } + } + break; + case ClassType.TypeUnsupportedBySourceGen: + { + _executionContext.ReportDiagnostic( + Diagnostic.Create(TypeNotSupported, Location.None, new string[] { typeMetadata.CompilableName })); + return; + } + default: + { + throw new InvalidOperationException(); + } + } + + try + { + _executionContext.AddSource($"{typeMetadata.FriendlyName}.cs", SourceText.From(source, Encoding.UTF8)); + } + catch (ArgumentException) + { + _executionContext.ReportDiagnostic(Diagnostic.Create(DuplicateTypeName, Location.None, new string[] { typeMetadata.FriendlyName })); + } + } + + private string GenerateForTypeWithKnownConverter(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + string metadataInitSource = $@"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesClassName}.{typeFriendlyName}Converter);"; + + return GenerateForType(typeMetadata, metadataInitSource); + } + + private string GenerateForTypeWithUnknownConverter(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + StringBuilder sb = new(); + + string metadataInitSource = $@"JsonConverter converter = {typeMetadata.ConverterInstantiationLogic}; + // TODO: consider moving this verification source to common helper. + Type typeToConvert = typeof({typeCompilableName}); + if (!converter.CanConvert(typeToConvert)) + {{ + Type underlyingType = Nullable.GetUnderlyingType(typeToConvert); + if (underlyingType != null && converter.CanConvert(underlyingType)) + {{ + JsonConverter actualConverter = converter; + + if (converter is JsonConverterFactory converterFactory) + {{ + actualConverter = converterFactory.CreateConverter(underlyingType, {OptionsInstanceVariableName}); + + if (actualConverter == null || actualConverter is JsonConverterFactory) + {{ + throw new InvalidOperationException($""JsonConverterFactory '{{converter}} cannot return a 'null' or 'JsonConverterFactory' value.""); + }} + }} + + // Allow nullable handling to forward to the underlying type's converter. + converter = {JsonMetadataServicesClassName}.GetNullableConverter<{typeCompilableName}>((JsonConverter<{typeCompilableName}>)actualConverter); + }} + else + {{ + throw new InvalidOperationException($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); + }} + }} + + _{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + + return GenerateForType(typeMetadata, metadataInitSource); + } + + private string GenerateForNullable(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + TypeMetadata? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata; + Debug.Assert(underlyingTypeMetadata != null); + string underlyingTypeCompilableName = underlyingTypeMetadata.CompilableName; + string underlyingTypeFriendlyName = underlyingTypeMetadata.FriendlyName; + string underlyingTypeInfoNamedArg = underlyingTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen + ? "underlyingTypeInfo: null" + : $"underlyingTypeInfo: {underlyingTypeFriendlyName}"; + + string metadataInitSource = @$"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}( + {OptionsInstanceVariableName}, + {JsonMetadataServicesClassName}.GetNullableConverter<{underlyingTypeCompilableName}>({underlyingTypeInfoNamedArg})); +"; + + return GenerateForType(typeMetadata, metadataInitSource); + } + + private string GenerateForEnum(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + string metadataInitSource = $"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, JsonMetadataServices.GetEnumConverter<{typeCompilableName}>({OptionsInstanceVariableName}));"; + + return GenerateForType(typeMetadata, metadataInitSource); + } + + private string GenerateForCollection(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + // Key metadata + TypeMetadata? collectionKeyTypeMetadata = typeMetadata.CollectionKeyTypeMetadata; + Debug.Assert(!(typeMetadata.CollectionType == CollectionType.Dictionary && collectionKeyTypeMetadata == null)); + string? keyTypeCompilableName = collectionKeyTypeMetadata?.CompilableName; + string? keyTypeReadableName = collectionKeyTypeMetadata?.FriendlyName; + + string? keyTypeMetadataPropertyName; + if (typeMetadata.ClassType != ClassType.Dictionary) + { + keyTypeMetadataPropertyName = "null"; + } + else + { + keyTypeMetadataPropertyName = collectionKeyTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen + ? "null" + : $"this.{keyTypeReadableName}"; + } + + // Value metadata + TypeMetadata? collectionValueTypeMetadata = typeMetadata.CollectionValueTypeMetadata; + Debug.Assert(collectionValueTypeMetadata != null); + string valueTypeCompilableName = collectionValueTypeMetadata.CompilableName; + string valueTypeReadableName = collectionValueTypeMetadata.FriendlyName; + + string valueTypeMetadataPropertyName = collectionValueTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen + ? "null" + : $"this.{valueTypeReadableName}"; + + string numberHandlingArg = $"{GetNumberHandlingAsStr(typeMetadata.NumberHandling)}"; + + CollectionType collectionType = typeMetadata.CollectionType; + string collectionTypeInfoValue = collectionType switch + { + CollectionType.Array => $"{JsonMetadataServicesClassName}.CreateArrayInfo<{valueTypeCompilableName}>({OptionsInstanceVariableName}, {valueTypeMetadataPropertyName}, {numberHandlingArg})", + CollectionType.List => $"{JsonMetadataServicesClassName}.CreateListInfo<{typeCompilableName}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new System.Collections.Generic.List<{valueTypeCompilableName}>(), {valueTypeMetadataPropertyName}, {numberHandlingArg})", + CollectionType.Dictionary => $"{JsonMetadataServicesClassName}.CreateDictionaryInfo<{typeCompilableName}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new System.Collections.Generic.Dictionary<{keyTypeCompilableName}, {valueTypeCompilableName}>(), {keyTypeMetadataPropertyName!}, {valueTypeMetadataPropertyName}, {numberHandlingArg})", + _ => throw new NotSupportedException() + }; + + string metadataInitSource = @$"_{typeFriendlyName} = {collectionTypeInfoValue};"; + return GenerateForType(typeMetadata, metadataInitSource); + } + + private string GenerateForObject(TypeMetadata typeMetadata) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + string createObjectFuncTypeArg = typeMetadata.ConstructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor + ? $"createObjectFunc: static () => new {typeMetadata.CompilableName}()" + : "createObjectFunc: null"; + + List? properties = typeMetadata.PropertiesMetadata; + + StringBuilder sb = new(); + + sb.Append($@"JsonTypeInfo<{typeCompilableName}> objectInfo = {JsonMetadataServicesClassName}.CreateObjectInfo<{typeCompilableName}>(); + _{typeFriendlyName} = objectInfo; +"); + + string propInitFuncVarName = $"{typeFriendlyName}{PropInitFuncVarName}"; + + sb.Append($@" + {JsonMetadataServicesClassName}.InitializeObjectInfo( + objectInfo, + {OptionsInstanceVariableName}, + {createObjectFuncTypeArg}, + {propInitFuncVarName}, + {GetNumberHandlingAsStr(typeMetadata.NumberHandling)});"); + + string metadataInitSource = sb.ToString(); + string? propInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata.IsValueType, propInitFuncVarName, properties); + + return GenerateForType(typeMetadata, metadataInitSource, propInitFuncSource); + } + + private string GeneratePropMetadataInitFunc( + bool declaringTypeIsValueType, + string propInitFuncVarName, + List? properties) + { + const string PropVarName = "properties"; + const string JsonContextVarName = "jsonContext"; + const string JsonPropertyInfoTypeName = "JsonPropertyInfo"; + + string propertyArrayInstantiationValue = properties == null + ? $"System.Array.Empty<{JsonPropertyInfoTypeName}>()" + : $"new {JsonPropertyInfoTypeName}[{properties.Count}]"; + + StringBuilder sb = new(); + + sb.Append($@" + private static {JsonPropertyInfoTypeName}[] {propInitFuncVarName}(JsonSerializerContext context) + {{ + JsonContext {JsonContextVarName} = (JsonContext)context; + JsonSerializerOptions options = context.Options; + + {JsonPropertyInfoTypeName}[] {PropVarName} = {propertyArrayInstantiationValue}; +"); + + if (properties != null) + { + for (int i = 0; i < properties.Count; i++) + { + PropertyMetadata memberMetadata = properties[i]; + + TypeMetadata memberTypeMetadata = memberMetadata.TypeMetadata; + + string clrPropertyName = memberMetadata.ClrName; + + string declaringTypeCompilableName = memberMetadata.DeclaringTypeCompilableName; + + string memberTypeFriendlyName = memberTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen + ? "null" + : $"{JsonContextVarName}.{memberTypeMetadata.FriendlyName}"; + + string typeTypeInfoNamedArg = $"propertyTypeInfo: {memberTypeFriendlyName}"; + + string jsonPropertyNameNamedArg = memberMetadata.JsonPropertyName != null + ? @$"jsonPropertyName: ""{memberMetadata.JsonPropertyName}""" + : "jsonPropertyName: null"; + + string getterNamedArg = memberMetadata.HasGetter + ? $"getter: static (obj) => {{ return (({declaringTypeCompilableName})obj).{clrPropertyName}; }}" + : "getter: null"; + + string setterNamedArg; + if (memberMetadata.HasSetter) + { + string propMutation = declaringTypeIsValueType + ? @$"{{ Unsafe.Unbox<{declaringTypeCompilableName}>(obj).{clrPropertyName} = value; }}" + : $@"{{ (({declaringTypeCompilableName})obj).{clrPropertyName} = value; }}"; + + setterNamedArg = $"setter: static (obj, value) => {propMutation}"; + } + else + { + setterNamedArg = "setter: null"; + } + + JsonIgnoreCondition? ignoreCondition = memberMetadata.IgnoreCondition; + string ignoreConditionNamedArg = ignoreCondition.HasValue + ? $"ignoreCondition: JsonIgnoreCondition.{ignoreCondition.Value}" + : "ignoreCondition: default"; + + string converterNamedArg = memberMetadata.ConverterInstantiationLogic == null + ? "converter: null" + : $"converter: {memberMetadata.ConverterInstantiationLogic}"; + + string memberTypeCompilableName = memberTypeMetadata.CompilableName; + + sb.Append($@" + {PropVarName}[{i}] = {JsonMetadataServicesClassName}.CreatePropertyInfo<{memberTypeCompilableName}>( + options, + isProperty: {memberMetadata.IsProperty.ToString().ToLowerInvariant()}, + declaringType: typeof({memberMetadata.DeclaringTypeCompilableName}), + {typeTypeInfoNamedArg}, + {converterNamedArg}, + {getterNamedArg}, + {setterNamedArg}, + {ignoreConditionNamedArg}, + numberHandling: {GetNumberHandlingAsStr(memberMetadata.NumberHandling)}, + propertyName: ""{clrPropertyName}"", + {jsonPropertyNameNamedArg}); + "); + } + } + + sb.Append(@$" + return {PropVarName}; + }}"); + + return sb.ToString(); + } + + private string GenerateForType(TypeMetadata typeMetadata, string metadataInitSource, string? additionalSource = null) + { + string typeCompilableName = typeMetadata.CompilableName; + string typeFriendlyName = typeMetadata.FriendlyName; + + return @$"{GetUsingStatementsString(typeMetadata)} + +namespace {_generationNamespace} +{{ + {JsonContextDeclarationSource} + {{ + private JsonTypeInfo<{typeCompilableName}> _{typeFriendlyName}; + public JsonTypeInfo<{typeCompilableName}> {typeFriendlyName} + {{ + get + {{ + if (_{typeFriendlyName} == null) + {{ + {WrapWithCheckForCustomConverterIfRequired(metadataInitSource, typeCompilableName, typeFriendlyName, GetNumberHandlingAsStr(typeMetadata.NumberHandling))} + }} + + return _{typeFriendlyName}; + }} + }}{additionalSource} + }} +}} +"; + } + + private string WrapWithCheckForCustomConverterIfRequired(string source, string typeCompilableName, string typeFriendlyName, string numberHandlingNamedArg) + { + if (!_honorRuntimeProvidedCustomConverters) + { + return source; + } + + return @$"JsonConverter customConverter; + if ({OptionsInstanceVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}(typeof({typeCompilableName}))) != null) + {{ + _{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, customConverter); + }} + else + {{ + {source.Replace(Environment.NewLine, $"{Environment.NewLine} ")} + }}"; + } + + // Base source generation context partial class. + private string BaseJsonContextImplementation() + { + StringBuilder sb = new(); + sb.Append(@$"using System.Text.Json; +using System.Text.Json.Serialization; + +namespace {_generationNamespace} +{{ + {JsonContextDeclarationSource} + {{ + private static JsonContext s_default; + public static JsonContext Default => s_default ??= new JsonContext(new JsonSerializerOptions()); + + public JsonContext() : base(null) + {{ + }} + + public JsonContext(JsonSerializerOptions options) : base(options) + {{ + }} + + {GetFetchLogicForRuntimeSpecifiedCustomConverter()} + }} +}} +"); + + return sb.ToString(); + } + + private string GetFetchLogicForRuntimeSpecifiedCustomConverter() + { + if (!_honorRuntimeProvidedCustomConverters) + { + return ""; + } + + return @$"private JsonConverter {RuntimeCustomConverterFetchingMethodName}(System.Type type) + {{ + System.Collections.Generic.IList converters = {OptionsInstanceVariableName}.Converters; + + // TODO: use a dictionary if count > ~15. + for (int i = 0; i < converters.Count; i++) + {{ + JsonConverter converter = converters[i]; + + if (converter.CanConvert(type)) + {{ + if (converter is JsonConverterFactory factory) + {{ + converter = factory.CreateConverter(type, {OptionsInstanceVariableName}); + if (converter == null || converter is JsonConverterFactory) + {{ + throw new System.InvalidOperationException($""The converter '{{factory.GetType()}}' cannot return null or a JsonConverterFactory instance.""); + }} + }} + + return converter; + }} + }} + + return null; + }}"; + } + + private string GetGetTypeInfoImplementation() + { + StringBuilder sb = new(); + + HashSet usingStatements = new(); + + foreach (TypeMetadata metadata in _rootSerializableTypes.Values) + { + usingStatements.UnionWith(GetUsingStatements(metadata)); + } + + sb.Append(@$"{GetUsingStatementsString(usingStatements)} + +namespace {_generationNamespace} +{{ + {JsonContextDeclarationSource} + {{ + public override JsonTypeInfo GetTypeInfo(System.Type type) + {{"); + + // TODO: Make this Dictionary-lookup-based if _handledType.Count > 64. + foreach (TypeMetadata metadata in _rootSerializableTypes.Values) + { + if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen) + { + sb.Append($@" + if (type == typeof({metadata.Type.GetUniqueCompilableTypeName()})) + {{ + return this.{metadata.FriendlyName}; + }} +"); + } + } + + sb.Append(@" + return null!; + } + } +} +"); + + return sb.ToString(); + } + + private static string GetUsingStatementsString(TypeMetadata typeMetadata) + { + HashSet usingStatements = GetUsingStatements(typeMetadata); + return GetUsingStatementsString(usingStatements); + } + + private static string GetUsingStatementsString(HashSet usingStatements) + { + string[] usingsArr = usingStatements.ToArray(); + Array.Sort(usingsArr); + return string.Join("\n", usingsArr); + } + + private static HashSet GetUsingStatements(TypeMetadata typeMetadata) + { + HashSet usingStatements = new(); + + // Add library usings. + usingStatements.Add(FormatAsUsingStatement("System.Runtime.CompilerServices")); + usingStatements.Add(FormatAsUsingStatement("System.Text.Json")); + usingStatements.Add(FormatAsUsingStatement("System.Text.Json.Serialization")); + usingStatements.Add(FormatAsUsingStatement("System.Text.Json.Serialization.Metadata")); + + // Add imports to root type. + usingStatements.Add(FormatAsUsingStatement(typeMetadata.Type.Namespace)); + + switch (typeMetadata.ClassType) + { + case ClassType.Nullable: + { + AddUsingStatementsForType(typeMetadata.NullableUnderlyingTypeMetadata!); + } + break; + case ClassType.Enumerable: + { + AddUsingStatementsForType(typeMetadata.CollectionValueTypeMetadata); + } + break; + case ClassType.Dictionary: + { + AddUsingStatementsForType(typeMetadata.CollectionKeyTypeMetadata); + AddUsingStatementsForType(typeMetadata.CollectionValueTypeMetadata); + } + break; + case ClassType.Object: + { + if (typeMetadata.PropertiesMetadata != null) + { + foreach (PropertyMetadata metadata in typeMetadata.PropertiesMetadata) + { + AddUsingStatementsForType(metadata.TypeMetadata); + } + } + } + break; + default: + break; + } + + void AddUsingStatementsForType(TypeMetadata typeMetadata) + { + usingStatements.Add(FormatAsUsingStatement(typeMetadata.Type.Namespace)); + + if (typeMetadata.CollectionKeyTypeMetadata != null) + { + Debug.Assert(typeMetadata.CollectionValueTypeMetadata != null); + usingStatements.Add(FormatAsUsingStatement(typeMetadata.CollectionKeyTypeMetadata.Type.Namespace)); + } + + if (typeMetadata.CollectionValueTypeMetadata != null) + { + usingStatements.Add(FormatAsUsingStatement(typeMetadata.CollectionValueTypeMetadata.Type.Namespace)); + } + } + + return usingStatements; + } + + private static string FormatAsUsingStatement(string @namespace) => $"using {@namespace};"; + + private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) => + numberHandling.HasValue + ? $"(JsonNumberHandling){(int)numberHandling.Value}" + : "default"; + + private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.cs b/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.cs new file mode 100644 index 00000000000000..10f1dd2f011a6e --- /dev/null +++ b/src/libraries/System.Text.Json/gen/JsonSourceGeneratorHelper.cs @@ -0,0 +1,467 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Text.Json.Serialization; +using System.Text.Json.SourceGeneration.Reflection; +using System.Linq; + +namespace System.Text.Json.SourceGeneration +{ + internal sealed partial class JsonSourceGeneratorHelper + { + private readonly Type _ienumerableType; + private readonly Type _listOfTType; + private readonly Type _dictionaryType; + + private readonly Type _booleanType; + private readonly Type _byteArrayType; + private readonly Type _charType; + private readonly Type _dateTimeType; + private readonly Type _dateTimeOffsetType; + private readonly Type _guidType; + private readonly Type _stringType; + private readonly Type _uriType; + private readonly Type _versionType; + + private readonly HashSet _numberTypes = new(); + + private readonly HashSet _knownTypes = new(); + + /// + /// Type information for member types in input object graphs. + /// + private readonly Dictionary _typeMetadataCache = new(); + + /// + /// Types that were specified with System.Text.Json.Serialization.JsonSerializableAttribute. + /// + private Dictionary _rootSerializableTypes; + + private readonly GeneratorExecutionContext _executionContext; + + private readonly MetadataLoadContextInternal _metadataLoadContext; + + private const string SystemTextJsonNamespace = "System.Text.Json"; + + private const string JsonConverterAttributeFullName = "System.Text.Json.Serialization.JsonConverterAttribute"; + + private const string JsonIgnoreAttributeFullName = "System.Text.Json.Serialization.JsonIgnoreAttribute"; + + private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition"; + + private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute"; + + private const string JsonNumberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonNumberHandlingAttribute"; + + private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute"; + + public JsonSourceGeneratorHelper( + GeneratorExecutionContext executionContext, + MetadataLoadContextInternal metadataLoadContext) + { + _generationNamespace = $"{executionContext.Compilation.AssemblyName}.JsonSourceGeneration"; + _executionContext = executionContext; + _metadataLoadContext = metadataLoadContext; + + _ienumerableType = metadataLoadContext.Resolve(typeof(IEnumerable)); + _listOfTType = metadataLoadContext.Resolve(typeof(List<>)); + _dictionaryType = metadataLoadContext.Resolve(typeof(Dictionary<,>)); + + _booleanType = metadataLoadContext.Resolve(typeof(bool)); + _byteArrayType = metadataLoadContext.Resolve(typeof(byte[])); + _charType = metadataLoadContext.Resolve(typeof(char)); + _dateTimeType = metadataLoadContext.Resolve(typeof(DateTime)); + _dateTimeOffsetType = metadataLoadContext.Resolve(typeof(DateTimeOffset)); + _guidType = metadataLoadContext.Resolve(typeof(Guid)); + _stringType = metadataLoadContext.Resolve(typeof(string)); + _uriType = metadataLoadContext.Resolve(typeof(Uri)); + _versionType = metadataLoadContext.Resolve(typeof(Version)); + + PopulateKnownTypes(metadataLoadContext); + } + + public void RegisterRootSerializableType(Type type, string? typeInfoPropertyName) + { + _rootSerializableTypes ??= new Dictionary(); + _rootSerializableTypes[type.FullName] = GetOrAddTypeMetadata(type, typeInfoPropertyName); + } + + public void GenerateSerializationMetadata() + { + if (_rootSerializableTypes == null || _rootSerializableTypes.Count == 0) + { + return; + } + + foreach (KeyValuePair pair in _rootSerializableTypes) + { + TypeMetadata typeMetadata = pair.Value; + GenerateTypeMetadata(typeMetadata); + } + + // Add base default instance source. + _executionContext.AddSource("JsonContext.g.cs", SourceText.From(BaseJsonContextImplementation(), Encoding.UTF8)); + + // Add GetJsonTypeInfo override implementation. + _executionContext.AddSource("JsonContext.GetJsonTypeInfo.g.cs", SourceText.From(GetGetTypeInfoImplementation(), Encoding.UTF8)); + } + + private TypeMetadata GetOrAddTypeMetadata(Type type, string? typeInfoPropertyName = null) + { + if (_typeMetadataCache.TryGetValue(type, out TypeMetadata? typeMetadata)) + { + return typeMetadata!; + } + + // Add metadata to cache now to prevent stack overflow when the same type is found somewhere else in the object graph. + typeMetadata = new(); + _typeMetadataCache[type] = typeMetadata; + + ClassType classType; + Type? collectionKeyType = null; + Type? collectionValueType = null; + Type? nullableUnderlyingType = null; + List? propertiesMetadata = null; + CollectionType collectionType = CollectionType.NotApplicable; + ObjectConstructionStrategy constructionStrategy = default; + JsonNumberHandling? numberHandling = null; + bool containsOnlyPrimitives = true; + + bool foundDesignTimeCustomConverter = false; + string? converterInstatiationLogic = null; + + IList attributeDataList = CustomAttributeData.GetCustomAttributes(type); + foreach (CustomAttributeData attributeData in attributeDataList) + { + Type attributeType = attributeData.AttributeType; + if (attributeType.FullName == "System.Text.Json.Serialization.JsonNumberHandlingAttribute") + { + IList ctorArgs = attributeData.ConstructorArguments; + numberHandling = (JsonNumberHandling)ctorArgs[0].Value; + continue; + } + else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null) + { + foundDesignTimeCustomConverter = true; + converterInstatiationLogic = GetConverterInstantiationLogic(attributeData); + } + } + + if (foundDesignTimeCustomConverter) + { + classType = converterInstatiationLogic != null + ? ClassType.TypeWithDesignTimeProvidedCustomConverter + : ClassType.TypeUnsupportedBySourceGen; + } + else if (_knownTypes.Contains(type)) + { + classType = ClassType.KnownType; + } + else if (type.IsNullableValueType(out nullableUnderlyingType)) + { + Debug.Assert(nullableUnderlyingType != null); + classType = ClassType.Nullable; + } + else if (type.IsEnum) + { + classType = ClassType.Enum; + } + else if (_ienumerableType.IsAssignableFrom(type)) + { + // Only T[], List, and Dictionary are supported. + + if (type.IsArray) + { + classType = ClassType.Enumerable; + collectionType = CollectionType.Array; + collectionValueType = type.GetElementType(); + } + else if (!type.IsGenericType) + { + classType = ClassType.TypeUnsupportedBySourceGen; + } + else + { + Type genericTypeDef = type.GetGenericTypeDefinition(); + Type[] genericTypeArgs = type.GetGenericArguments(); + + if (genericTypeDef == _listOfTType) + { + classType = ClassType.Enumerable; + collectionType = CollectionType.List; + collectionValueType = genericTypeArgs[0]; + } + else if (genericTypeDef == _dictionaryType) + { + classType = ClassType.Dictionary; + collectionType = CollectionType.Dictionary; + collectionKeyType = genericTypeArgs[0]; + collectionValueType = genericTypeArgs[1]; + } + else + { + classType = ClassType.TypeUnsupportedBySourceGen; + } + } + } + else + { + classType = ClassType.Object; + + if (type.GetConstructor(Type.EmptyTypes) != null && !type.IsAbstract && !type.IsInterface) + { + // TODO: support parameterized ctors. + constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor; + } + + for (Type? currentType = type; currentType != null; currentType = currentType.BaseType) + { + const BindingFlags bindingFlags = + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.DeclaredOnly; + + foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags)) + { + PropertyMetadata metadata = GetPropertyMetadata(propertyInfo); + + // Ignore indexers. + if (propertyInfo.GetIndexParameters().Length > 0) + { + continue; + } + + string key = metadata.JsonPropertyName ?? metadata.ClrName; + + if (metadata.HasGetter || metadata.HasSetter) + { + (propertiesMetadata ??= new()).Add(metadata); + } + + if (containsOnlyPrimitives && !IsPrimitive(propertyInfo.PropertyType)) + { + containsOnlyPrimitives = false; + } + } + + foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) + { + PropertyMetadata metadata = GetPropertyMetadata(fieldInfo); + + if (metadata.HasGetter || metadata.HasSetter) + { + (propertiesMetadata ??= new()).Add(metadata); + } + } + } + } + + typeMetadata.Initialize( + compilableName: type.GetUniqueCompilableTypeName(), + friendlyName: typeInfoPropertyName ?? type.GetFriendlyTypeName(), + type, + classType, + isValueType: type.IsValueType, + numberHandling, + propertiesMetadata, + collectionType, + collectionKeyTypeMetadata: collectionKeyType != null ? GetOrAddTypeMetadata(collectionKeyType) : null, + collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeMetadata(collectionValueType) : null, + constructionStrategy, + nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeMetadata(nullableUnderlyingType) : null, + converterInstatiationLogic, + containsOnlyPrimitives); + + return typeMetadata; + } + + private PropertyMetadata GetPropertyMetadata(MemberInfo memberInfo) + { + IList attributeDataList = CustomAttributeData.GetCustomAttributes(memberInfo); + + bool hasJsonInclude = false; + JsonIgnoreCondition? ignoreCondition = null; + JsonNumberHandling? numberHandling = null; + string? jsonPropertyName = null; + + bool foundDesignTimeCustomConverter = false; + string? converterInstantiationLogic = null; + + foreach (CustomAttributeData attributeData in attributeDataList) + { + Type attributeType = attributeData.AttributeType; + + if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null) + { + foundDesignTimeCustomConverter = true; + converterInstantiationLogic = GetConverterInstantiationLogic(attributeData); + } + else if (attributeType.Assembly.FullName == SystemTextJsonNamespace) + { + switch (attributeData.AttributeType.FullName) + { + case JsonIgnoreAttributeFullName: + { + IList namedArgs = attributeData.NamedArguments; + + if (namedArgs.Count == 0) + { + ignoreCondition = JsonIgnoreCondition.Always; + } + else if (namedArgs.Count == 1 && + namedArgs[0].MemberInfo.MemberType == MemberTypes.Property && + ((PropertyInfo)namedArgs[0].MemberInfo).PropertyType.FullName == JsonIgnoreConditionFullName) + { + ignoreCondition = (JsonIgnoreCondition)namedArgs[0].TypedValue.Value; + } + } + break; + case JsonIncludeAttributeFullName: + { + hasJsonInclude = true; + } + break; + case JsonNumberHandlingAttributeFullName: + { + IList ctorArgs = attributeData.ConstructorArguments; + numberHandling = (JsonNumberHandling)ctorArgs[0].Value; + } + break; + case JsonPropertyNameAttributeFullName: + { + IList ctorArgs = attributeData.ConstructorArguments; + jsonPropertyName = (string)ctorArgs[0].Value; + // Null check here is done at runtime within JsonSerializer. + } + break; + default: + break; + } + } + } + + Type memberCLRType; + bool hasGetter; + bool hasSetter; + bool getterIsVirtual = false; + bool setterIsVirtual = false; + + switch (memberInfo) + { + case PropertyInfo propertyInfo: + { + MethodInfo setMethod = propertyInfo.SetMethod; + + memberCLRType = propertyInfo.PropertyType; + hasGetter = PropertyAccessorCanBeReferenced(propertyInfo.GetMethod, hasJsonInclude); + hasSetter = PropertyAccessorCanBeReferenced(setMethod, hasJsonInclude) && !setMethod.IsInitOnly(); + getterIsVirtual = propertyInfo.GetMethod?.IsVirtual == true; + setterIsVirtual = propertyInfo.SetMethod?.IsVirtual == true; + } + break; + case FieldInfo fieldInfo: + { + Debug.Assert(fieldInfo.IsPublic); + + memberCLRType = fieldInfo.FieldType; + hasGetter = true; + hasSetter = !fieldInfo.IsInitOnly; + } + break; + default: + throw new InvalidOperationException(); + } + + return new PropertyMetadata + { + ClrName = memberInfo.Name, + IsProperty = memberInfo.MemberType == MemberTypes.Property, + JsonPropertyName = jsonPropertyName, + HasGetter = hasGetter, + HasSetter = hasSetter, + GetterIsVirtual = getterIsVirtual, + SetterIsVirtual = setterIsVirtual, + IgnoreCondition = ignoreCondition, + NumberHandling = numberHandling, + HasJsonInclude = hasJsonInclude, + TypeMetadata = GetOrAddTypeMetadata(memberCLRType), + DeclaringTypeCompilableName = memberInfo.DeclaringType.GetUniqueCompilableTypeName(), + ConverterInstantiationLogic = converterInstantiationLogic + }; + } + + private static bool PropertyAccessorCanBeReferenced(MethodInfo? memberAccessor, bool hasJsonInclude) => + (memberAccessor != null && !memberAccessor.IsPrivate) && (memberAccessor.IsPublic || hasJsonInclude); + + private string? GetConverterInstantiationLogic(CustomAttributeData attributeData) + { + if (attributeData.AttributeType.FullName != JsonConverterAttributeFullName) + { + return null; + } + + Type converterType = new TypeWrapper((ITypeSymbol)attributeData.ConstructorArguments[0].Value, _metadataLoadContext); + + if (converterType == null || converterType.GetConstructor(Type.EmptyTypes) == null || converterType.IsNestedPrivate) + { + return null; + } + + return $"new {converterType.GetUniqueCompilableTypeName()}()"; + } + + private void PopulateNumberTypes(MetadataLoadContextInternal metadataLoadContext) + { + Debug.Assert(_numberTypes != null); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(byte))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(decimal))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(double))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(short))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(sbyte))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(int))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(long))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(float))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(ushort))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(uint))); + _numberTypes.Add(metadataLoadContext.Resolve(typeof(ulong))); + } + + private void PopulateKnownTypes(MetadataLoadContextInternal metadataLoadContext) + { + PopulateNumberTypes(metadataLoadContext); + + Debug.Assert(_knownTypes != null); + Debug.Assert(_numberTypes != null); + + _knownTypes.UnionWith(_numberTypes); + _knownTypes.Add(_booleanType); + _knownTypes.Add(_byteArrayType); + _knownTypes.Add(_charType); + _knownTypes.Add(_dateTimeType); + _knownTypes.Add(_dateTimeOffsetType); + _knownTypes.Add(_guidType); + _knownTypes.Add(metadataLoadContext.Resolve(typeof(object))); + _knownTypes.Add(_stringType); + + // System.Private.Uri may not be loaded in input compilation. + if (_uriType != null) + { + _knownTypes.Add(_uriType); + } + + _knownTypes.Add(metadataLoadContext.Resolve(typeof(Version))); + } + + private bool IsPrimitive(Type type) + => _knownTypes.Contains(type) && type != _uriType && type != _versionType; + + public Dictionary? GetSerializableTypes() => _rootSerializableTypes?.ToDictionary(p => p.Key, p => p.Value.Type); + } +} diff --git a/src/libraries/System.Text.Json/gen/ObjectConstructionStrategy.cs b/src/libraries/System.Text.Json/gen/ObjectConstructionStrategy.cs new file mode 100644 index 00000000000000..03705d27075a35 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/ObjectConstructionStrategy.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Indicates which kind of constructor an object is to be created with. + /// + internal enum ObjectConstructionStrategy + { + /// + /// Object is abstract or an interface. + /// + NotApplicable = 0, + /// + /// Object should be created with a parameterless constructor. + /// + ParameterlessConstructor = 1, + /// + /// Object should be created with a parameterized constructor. + /// + ParameterizedConstructor = 2, + } +} diff --git a/src/libraries/System.Text.Json/gen/PropertyMetadata.cs b/src/libraries/System.Text.Json/gen/PropertyMetadata.cs new file mode 100644 index 00000000000000..68a5a408e83131 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/PropertyMetadata.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace System.Text.Json.SourceGeneration +{ + [DebuggerDisplay("Name={Name}, Type={TypeMetadata}")] + internal class PropertyMetadata + { + /// + /// The CLR name of the property. + /// + public string ClrName { get; init; } + + /// + /// Is this a property or a field? + /// + public bool IsProperty { get; init; } + + /// + /// The property name specified via JsonPropertyNameAttribute, if available. + /// + public string? JsonPropertyName { get; init; } + + /// + /// Whether the property has a public or internal (only usable when JsonIncludeAttribute is specified) + /// getter that can be referenced in generated source code. + /// + public bool HasGetter { get; init; } + + /// + /// Whether the property has a public or internal (only usable when JsonIncludeAttribute is specified) + /// setter that can be referenced in generated source code. + /// + public bool HasSetter { get; init; } + + public bool GetterIsVirtual { get; init; } + + public bool SetterIsVirtual { get; init; } + + /// + /// The for the property. + /// + public JsonIgnoreCondition? IgnoreCondition { get; init; } + + /// + /// The for the property. + /// + public JsonNumberHandling? NumberHandling { get; init; } + + /// + /// Whether the property has the JsonIncludeAttribute. If so, non-public accessors can be used for (de)serialziation. + /// + public bool HasJsonInclude { get; init; } + + /// + /// Metadata for the property's type. + /// + public TypeMetadata TypeMetadata { get; init; } + + /// + /// Compilable name of the property's declaring type. + /// + public string DeclaringTypeCompilableName { get; init; } + + /// + /// Source code to instantiate design-time specified custom converter. + /// + public string? ConverterInstantiationLogic { get; init; } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs new file mode 100644 index 00000000000000..d5ed28e4800a17 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs @@ -0,0 +1,56 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class AssemblyWrapper : Assembly + { + private readonly MetadataLoadContextInternal _metadataLoadContext; + + public AssemblyWrapper(IAssemblySymbol assembly, MetadataLoadContextInternal metadataLoadContext) + { + Symbol = assembly; + _metadataLoadContext = metadataLoadContext; + } + + internal IAssemblySymbol Symbol { get; } + + public override string FullName => Symbol.Identity.Name; + + public override Type[] GetExportedTypes() + { + return GetTypes(); + } + + public override Type[] GetTypes() + { + var types = new List(); + var stack = new Stack(); + stack.Push(Symbol.GlobalNamespace); + while (stack.Count > 0) + { + INamespaceSymbol current = stack.Pop(); + + foreach (INamedTypeSymbol type in current.GetTypeMembers()) + { + types.Add(type.AsType(_metadataLoadContext)); + } + + foreach (INamespaceSymbol ns in current.GetNamespaceMembers()) + { + stack.Push(ns); + } + } + return types.ToArray(); + } + + public override Type GetType(string name) + { + return Symbol.GetTypeByMetadataName(name)!.AsType(_metadataLoadContext); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs new file mode 100644 index 00000000000000..979d32c8f7c99e --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs @@ -0,0 +1,94 @@ +// 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.Globalization; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class ConstructorInfoWrapper : ConstructorInfo + { + private readonly IMethodSymbol _ctor; + private readonly MetadataLoadContextInternal _metadataLoadContext; + + public ConstructorInfoWrapper(IMethodSymbol ctor, MetadataLoadContextInternal metadataLoadContext) + { + _ctor = ctor; + _metadataLoadContext = metadataLoadContext; + } + + public override Type DeclaringType => _ctor.ContainingType.AsType(_metadataLoadContext); + + public override MethodAttributes Attributes => throw new NotImplementedException(); + + public override RuntimeMethodHandle MethodHandle => throw new NotSupportedException(); + + public override string Name => _ctor.Name; + + public override Type ReflectedType => throw new NotImplementedException(); + + public override bool IsGenericMethod => _ctor.IsGenericMethod; + + public override Type[] GetGenericArguments() + { + var typeArguments = new List(); + foreach (ITypeSymbol t in _ctor.TypeArguments) + { + typeArguments.Add(t.AsType(_metadataLoadContext)); + } + return typeArguments.ToArray(); + } + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _ctor.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override MethodImplAttributes GetMethodImplementationFlags() + { + throw new NotImplementedException(); + } + + public override ParameterInfo[] GetParameters() + { + var parameters = new List(); + foreach (IParameterSymbol p in _ctor.Parameters) + { + parameters.Add(new ParameterInfoWrapper(p, _metadataLoadContext)); + } + return parameters.ToArray(); + } + + public override object Invoke(BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs new file mode 100644 index 00000000000000..75b31db587a8f6 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class CustomAttributeDataWrapper : CustomAttributeData + { + public CustomAttributeDataWrapper(AttributeData a, MetadataLoadContextInternal metadataLoadContext) + { + var namedArguments = new List(); + foreach (KeyValuePair na in a.NamedArguments) + { + var member = a.AttributeClass!.GetMembers(na.Key).First(); + + MemberInfo memberInfo = member is IPropertySymbol + ? new PropertyInfoWrapper((IPropertySymbol)member, metadataLoadContext) + : new FieldInfoWrapper((IFieldSymbol)member, metadataLoadContext); + + namedArguments.Add(new CustomAttributeNamedArgument(memberInfo, na.Value.Value)); + } + + var constructorArguments = new List(); + foreach (TypedConstant ca in a.ConstructorArguments) + { + constructorArguments.Add(new CustomAttributeTypedArgument(ca.Type.AsType(metadataLoadContext), ca.Value)); + } + Constructor = new ConstructorInfoWrapper(a.AttributeConstructor!, metadataLoadContext); + NamedArguments = namedArguments; + ConstructorArguments = constructorArguments; + } + + public override ConstructorInfo Constructor { get; } + + public override IList NamedArguments { get; } + + public override IList ConstructorArguments { get; } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs new file mode 100644 index 00000000000000..cf36ceacbeee55 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs @@ -0,0 +1,96 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; +using System.Globalization; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class FieldInfoWrapper : FieldInfo + { + private readonly IFieldSymbol _field; + private readonly MetadataLoadContextInternal _metadataLoadContext; + public FieldInfoWrapper(IFieldSymbol parameter, MetadataLoadContextInternal metadataLoadContext) + { + _field = parameter; + _metadataLoadContext = metadataLoadContext; + } + + private FieldAttributes? _attributes; + + public override FieldAttributes Attributes + { + get + { + if (!_attributes.HasValue) + { + _attributes = default(FieldAttributes); + + if (_field.IsStatic) + { + _attributes |= FieldAttributes.Static; + } + + switch (_field.DeclaredAccessibility) + { + case Accessibility.Public: + _attributes |= FieldAttributes.Public; + break; + case Accessibility.Private: + _attributes |= FieldAttributes.Private; + break; + } + } + + return _attributes.Value; + } + } + + public override RuntimeFieldHandle FieldHandle => throw new NotImplementedException(); + + public override Type FieldType => _field.Type.AsType(_metadataLoadContext); + + public override Type DeclaringType => _field.ContainingType.AsType(_metadataLoadContext); + + public override string Name => _field.Name; + + public override Type ReflectedType => throw new NotImplementedException(); + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotImplementedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + + public override object GetValue(object obj) + { + throw new NotImplementedException(); + } + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _field.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + + public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs new file mode 100644 index 00000000000000..1b4de7811fd741 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs @@ -0,0 +1,54 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class MemberInfoWrapper : MemberInfo + { + private readonly ISymbol _member; + private readonly MetadataLoadContextInternal _metadataLoadContext; + + public MemberInfoWrapper(ISymbol member, MetadataLoadContextInternal metadataLoadContext) + { + _member = member; + _metadataLoadContext = metadataLoadContext; + } + + public override Type DeclaringType => _member.ContainingType.AsType(_metadataLoadContext); + + public override MemberTypes MemberType => throw new NotImplementedException(); + + public override string Name => _member.Name; + + public override Type ReflectedType => throw new NotImplementedException(); + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _member.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs new file mode 100644 index 00000000000000..885885325fe53b --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs @@ -0,0 +1,114 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class MetadataLoadContextInternal + { + private readonly Dictionary _assemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly Compilation _compilation; + + private IAssemblySymbol? _collectionsAssemblySymbol; + + public MetadataLoadContextInternal(Compilation compilation) + { + _compilation = compilation; + Dictionary assemblies = compilation.References + .OfType() + .ToDictionary( + r => string.IsNullOrWhiteSpace(r.FilePath) ? + new AssemblyName(r.Display) : AssemblyName.GetAssemblyName(r.FilePath), + r => (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(r)!); + + foreach (var item in assemblies) + { + string key = item.Key.Name; + _assemblies[key] = item.Value!; + + if (_collectionsAssemblySymbol == null && key == "System.Collections") + { + _collectionsAssemblySymbol = item.Value!; + } + } + + CoreAssembly = new AssemblyWrapper(compilation.GetTypeByMetadataName("System.Object")!.ContainingAssembly, this); + MainAssembly = new AssemblyWrapper(compilation.Assembly, this); + } + + public Type Resolve() => Resolve(typeof(T)); + + public Type? Resolve(Type type) + { + string assemblyName = type.Assembly.GetName().Name; + IAssemblySymbol assemblySymbol; + + if (assemblyName == "System.Private.CoreLib" || assemblyName == "mscorlib" || assemblyName == "System.Runtime" || assemblyName == "System.Private.Uri") + { + Type resolvedType = ResolveFromAssembly(type, CoreAssembly.Symbol); + if (resolvedType != null) + { + return resolvedType; + } + + if (_collectionsAssemblySymbol != null && typeof(IEnumerable).IsAssignableFrom(type)) + { + resolvedType = ResolveFromAssembly(type, _collectionsAssemblySymbol); + if (resolvedType != null) + { + return resolvedType; + } + } + } + + CustomAttributeData? typeForwardedFrom = type.GetCustomAttributeData(typeof(TypeForwardedFromAttribute)); + if (typeForwardedFrom != null) + { + assemblyName = typeForwardedFrom.GetConstructorArgument(0); + } + + if (!_assemblies.TryGetValue(new AssemblyName(assemblyName).Name, out assemblySymbol)) + { + return null; + } + + return ResolveFromAssembly(type, assemblySymbol); + } + + private Type? ResolveFromAssembly(Type type, IAssemblySymbol assemblySymbol) + { + if (type.IsArray) + { + var typeSymbol = assemblySymbol.GetTypeByMetadataName(type.GetElementType().FullName); + if (typeSymbol == null) + { + return null!; + } + + return _compilation.CreateArrayTypeSymbol(typeSymbol).AsType(this); + } + + // Resolve the full name + return assemblySymbol.GetTypeByMetadataName(type.FullName)!.AsType(this); + } + + private AssemblyWrapper CoreAssembly { get; } + public Assembly MainAssembly { get; } + + internal Assembly LoadFromAssemblyName(string fullName) + { + if (_assemblies.TryGetValue(new AssemblyName(fullName).Name, out var assembly)) + { + return new AssemblyWrapper(assembly, this); + } + return null!; + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs new file mode 100644 index 00000000000000..44eecf0b5919db --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs @@ -0,0 +1,138 @@ +// 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.Globalization; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class MethodInfoWrapper : MethodInfo + { + private readonly IMethodSymbol _method; + private readonly MetadataLoadContextInternal _metadataLoadContext; + + public MethodInfoWrapper(IMethodSymbol method, MetadataLoadContextInternal metadataLoadContext) + { + _method = method; + _metadataLoadContext = metadataLoadContext; + } + + public override ICustomAttributeProvider ReturnTypeCustomAttributes => throw new NotImplementedException(); + + private MethodAttributes? _attributes; + + public override MethodAttributes Attributes + { + get + { + if (!_attributes.HasValue) + { + _attributes = default(MethodAttributes); + + if (_method.IsAbstract) + { + _attributes |= MethodAttributes.Abstract; + } + + if (_method.IsStatic) + { + _attributes |= MethodAttributes.Static; + } + + if (_method.IsVirtual) + { + _attributes |= MethodAttributes.Virtual; + } + + switch (_method.DeclaredAccessibility) + { + case Accessibility.Public: + _attributes |= MethodAttributes.Public; + break; + case Accessibility.Private: + _attributes |= MethodAttributes.Private; + break; + } + } + + return _attributes.Value; + } + } + + public override RuntimeMethodHandle MethodHandle => throw new NotSupportedException(); + + public override Type DeclaringType => _method.ContainingType.AsType(_metadataLoadContext); + + public override Type ReturnType => _method.ReturnType.AsType(_metadataLoadContext); + + public override string Name => _method.Name; + + public override bool IsGenericMethod => _method.IsGenericMethod; + + public bool IsInitOnly => _method.IsInitOnly; + + public override Type ReflectedType => throw new NotImplementedException(); + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _method.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override MethodInfo GetBaseDefinition() + { + throw new NotImplementedException(); + } + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override Type[] GetGenericArguments() + { + var typeArguments = new List(); + foreach (ITypeSymbol t in _method.TypeArguments) + { + typeArguments.Add(t.AsType(_metadataLoadContext)); + } + return typeArguments.ToArray(); + } + + public override MethodImplAttributes GetMethodImplementationFlags() + { + throw new NotImplementedException(); + } + + public override ParameterInfo[] GetParameters() + { + var parameters = new List(); + foreach (IParameterSymbol p in _method.Parameters) + { + parameters.Add(new ParameterInfoWrapper(p, _metadataLoadContext)); + } + return parameters.ToArray(); + } + + public override object Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs new file mode 100644 index 00000000000000..b8891251c7e627 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs @@ -0,0 +1,36 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class ParameterInfoWrapper : ParameterInfo + { + private readonly IParameterSymbol _parameter; + + private readonly MetadataLoadContextInternal _metadataLoadContext; + + public ParameterInfoWrapper(IParameterSymbol parameter, MetadataLoadContextInternal metadataLoadContext) + { + _parameter = parameter; + _metadataLoadContext = metadataLoadContext; + } + + public override Type ParameterType => _parameter.Type.AsType(_metadataLoadContext); + + public override string Name => _parameter.Name; + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _parameter.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs new file mode 100644 index 00000000000000..294c6dd1059165 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs @@ -0,0 +1,96 @@ +// 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.Globalization; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class PropertyInfoWrapper : PropertyInfo + { + private readonly IPropertySymbol _property; + private MetadataLoadContextInternal _metadataLoadContext; + + public PropertyInfoWrapper(IPropertySymbol property, MetadataLoadContextInternal metadataLoadContext) + { + _property = property; + _metadataLoadContext = metadataLoadContext; + } + + public override PropertyAttributes Attributes => throw new NotImplementedException(); + + public override bool CanRead => _property.GetMethod != null; + + public override bool CanWrite => _property.SetMethod != null; + + public override Type PropertyType => _property.Type.AsType(_metadataLoadContext); + + public override Type DeclaringType => _property.ContainingType.AsType(_metadataLoadContext); + + public override string Name => _property.Name; + + public override Type ReflectedType => throw new NotImplementedException(); + + public override MethodInfo[] GetAccessors(bool nonPublic) + { + throw new NotImplementedException(); + } + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _property.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override MethodInfo GetGetMethod(bool nonPublic) + { + return _property.GetMethod!.AsMethodInfo(_metadataLoadContext); + } + + public override ParameterInfo[] GetIndexParameters() + { + var parameters = new List(); + foreach (IParameterSymbol p in _property.Parameters) + { + parameters.Add(new ParameterInfoWrapper(p, _metadataLoadContext)); + } + return parameters.ToArray(); + } + + public override MethodInfo GetSetMethod(bool nonPublic) + { + return _property.SetMethod!.AsMethodInfo(_metadataLoadContext); + } + + public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + + public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs new file mode 100644 index 00000000000000..07dce68fcbb899 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal static class ReflectionExtensions + { + public static CustomAttributeData GetCustomAttributeData(this MemberInfo memberInfo, Type type) + { + return memberInfo.CustomAttributes.FirstOrDefault(a => type.IsAssignableFrom(a.AttributeType)); + } + + public static TValue GetConstructorArgument(this CustomAttributeData customAttributeData, int index) + { + return index < customAttributeData.ConstructorArguments.Count ? (TValue)customAttributeData.ConstructorArguments[index].Value! : default!; + } + + public static bool IsInitOnly(this MethodInfo method) + { + MethodInfoWrapper? methodInfoWrapper = method as MethodInfoWrapper; + + if (methodInfoWrapper == null) + { + throw new ArgumentException("Expected a MethodInfoWrapper instance.", nameof(method)); + } + + return methodInfoWrapper.IsInitOnly; + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs new file mode 100644 index 00000000000000..610e7fe4eef6aa --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs @@ -0,0 +1,34 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal static class RoslynExtensions + { + public static Type AsType(this ITypeSymbol typeSymbol, MetadataLoadContextInternal metadataLoadContext) + { + if (typeSymbol == null) + { + return null; + } + + return new TypeWrapper(typeSymbol, metadataLoadContext); + } + + public static MethodInfo AsMethodInfo(this IMethodSymbol methodSymbol, MetadataLoadContextInternal metadataLoadContext) => (methodSymbol == null ? null : new MethodInfoWrapper(methodSymbol, metadataLoadContext))!; + + public static IEnumerable BaseTypes(this INamedTypeSymbol typeSymbol) + { + var t = typeSymbol; + while (t != null) + { + yield return t; + t = t.BaseType; + } + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs new file mode 100644 index 00000000000000..4bfb71a9ce8717 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Linq; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal static class TypeExtensions + { + public static string GetUniqueCompilableTypeName(this Type type) => GetCompilableTypeName(type, type.FullName); + + public static string GetCompilableTypeName(this Type type) => GetCompilableTypeName(type, type.Name); + + private static string GetCompilableTypeName(Type type, string name) + { + if (!type.IsGenericType) + { + return name.Replace('+', '.'); + } + + // TODO: Guard upstream against open generics. + Debug.Assert(!type.ContainsGenericParameters); + + int backTickIndex = name.IndexOf('`'); + string baseName = name.Substring(0, backTickIndex).Replace('+', '.'); + + return $"{baseName}<{string.Join(",", type.GetGenericArguments().Select(arg => GetUniqueCompilableTypeName(arg)))}>"; + } + + public static string GetFriendlyTypeName(this Type type) + { + return GetFriendlyTypeName(type.GetCompilableTypeName()); + } + + private static string GetFriendlyTypeName(string compilableName) + { + return compilableName.Replace(".", "").Replace("<", "").Replace(">", "").Replace(",", "").Replace("[]", "Array"); + } + + public static Type NullableOfTType { get; set; } + + public static bool IsNullableValueType(this Type type, out Type? underlyingType) + { + Debug.Assert(NullableOfTType != null); + + // TODO: log bug because Nullable.GetUnderlyingType doesn't work due to + // https://github.com/dotnet/runtimelab/blob/7472c863db6ec5ddab7f411ddb134a6e9f3c105f/src/libraries/System.Private.CoreLib/src/System/Nullable.cs#L124 + // i.e. type.GetGenericTypeDefinition() will never equal typeof(Nullable<>), as expected in that code segment. + if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableOfTType) + { + underlyingType = type.GetGenericArguments()[0]; + return true; + } + + underlyingType = null; + return false; + } + + public static Type? GetCompatibleBaseClass(this Type type, string baseTypeFullName) + { + Type? baseTypeToCheck = type; + + while (baseTypeToCheck != null && baseTypeToCheck != typeof(object)) + { + if (baseTypeToCheck.FullName == baseTypeFullName) + { + return baseTypeToCheck; + } + + baseTypeToCheck = baseTypeToCheck.BaseType; + } + + return null; + } + } +} diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs new file mode 100644 index 00000000000000..2ffc4725922ab6 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs @@ -0,0 +1,490 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace System.Text.Json.SourceGeneration.Reflection +{ + internal class TypeWrapper : Type + { + private readonly ITypeSymbol _typeSymbol; + + private readonly MetadataLoadContextInternal _metadataLoadContext; + + private INamedTypeSymbol? _namedTypeSymbol; + + private IArrayTypeSymbol? _arrayTypeSymbol; + + private Type _elementType; + + public TypeWrapper(ITypeSymbol namedTypeSymbol, MetadataLoadContextInternal metadataLoadContext) + { + _typeSymbol = namedTypeSymbol; + _metadataLoadContext = metadataLoadContext; + _namedTypeSymbol = _typeSymbol as INamedTypeSymbol; + _arrayTypeSymbol = _typeSymbol as IArrayTypeSymbol; + } + + public override Assembly Assembly => new AssemblyWrapper(_typeSymbol.ContainingAssembly, _metadataLoadContext); + + private string? _assemblyQualifiedName; + + public override string AssemblyQualifiedName + { + get + { + if (_assemblyQualifiedName == null) + { + StringBuilder sb = new(); + + AssemblyIdentity identity = _typeSymbol.ContainingAssembly.Identity; + + sb.Append(FullName); + + sb.Append(", "); + sb.Append(identity.Name); + + sb.Append(", Version="); + sb.Append(identity.Version); + + if (string.IsNullOrWhiteSpace(identity.CultureName)) + { + sb.Append(", Culture=neutral"); + } + + sb.Append(", PublicKeyToken="); + ImmutableArray publicKeyToken = identity.PublicKeyToken; + if (publicKeyToken.Length > 0) + { + foreach (byte b in publicKeyToken) + { + sb.Append(b.ToString("x2")); + } + } + else + { + sb.Append("null"); + } + + _assemblyQualifiedName = sb.ToString(); + } + + return _assemblyQualifiedName; + } + } + + public override Type BaseType => _typeSymbol.BaseType!.AsType(_metadataLoadContext); + + private string? _fullName; + + public override string FullName + { + get + { + if (_fullName == null) + { + StringBuilder sb = new(); + + if (this.IsNullableValueType(out Type? underlyingType)) + { + sb.Append("System.Nullable`1[["); + sb.Append(underlyingType.AssemblyQualifiedName); + sb.Append("]]"); + } + 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)) + { + sb.Insert(0, $"{Namespace}."); + } + } + + _fullName = sb.ToString(); + } + + return _fullName; + } + } + + public override Guid GUID => Guid.Empty; + + public override Module Module => throw new NotImplementedException(); + + public override string Namespace => + IsArray ? + GetElementType().Namespace : + _typeSymbol.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining))!; + + public override Type UnderlyingSystemType => this; + + public override string Name + { + get + { + if (_arrayTypeSymbol == null) + { + return _typeSymbol.MetadataName; + } + + Type elementType = GetElementType(); + return elementType.Name + "[]"; + } + } + + private Type _enumType; + + public override bool IsEnum + { + get + { + _enumType ??= _metadataLoadContext.Resolve(typeof(Enum)); + return IsSubclassOf(_enumType); + } + } + + public override bool IsGenericType => _namedTypeSymbol?.IsGenericType == true; + + public override bool ContainsGenericParameters => _namedTypeSymbol?.IsUnboundGenericType == true; + + public override bool IsGenericTypeDefinition => base.IsGenericTypeDefinition; + + public INamespaceSymbol GetNamespaceSymbol => _typeSymbol.ContainingNamespace; + + public override Type[] GetGenericArguments() + { + var args = new List(); + foreach (ITypeSymbol item in _namedTypeSymbol.TypeArguments) + { + args.Add(item.AsType(_metadataLoadContext)); + } + return args.ToArray(); + } + + public override Type GetGenericTypeDefinition() + { + return _namedTypeSymbol.ConstructedFrom.AsType(_metadataLoadContext); + } + + public override IList GetCustomAttributesData() + { + var attributes = new List(); + foreach (AttributeData a in _typeSymbol.GetAttributes()) + { + attributes.Add(new CustomAttributeDataWrapper(a, _metadataLoadContext)); + } + return attributes; + } + + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + { + var ctors = new List(); + foreach (IMethodSymbol c in _namedTypeSymbol.Constructors) + { + ctors.Add(new ConstructorInfoWrapper(c, _metadataLoadContext)); + } + return ctors.ToArray(); + } + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override Type GetElementType() + { + _elementType ??= _arrayTypeSymbol?.ElementType.AsType(_metadataLoadContext)!; + return _elementType; + } + + public override EventInfo GetEvent(string name, BindingFlags bindingAttr) + { + throw new NotImplementedException(); + } + + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + { + throw new NotImplementedException(); + } + + public override FieldInfo GetField(string name, BindingFlags bindingAttr) + { + throw new NotImplementedException(); + } + + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + { + var fields = new List(); + foreach (ISymbol item in _typeSymbol.GetMembers()) + { + // Associated Symbol checks the field is not a backingfield. + if (item is IFieldSymbol field && field.AssociatedSymbol == null && !field.IsReadOnly) + { + if ((item.DeclaredAccessibility & Accessibility.Public) == Accessibility.Public) + { + fields.Add(new FieldInfoWrapper(field, _metadataLoadContext)); + } + } + } + return fields.ToArray(); + } + + public override Type GetInterface(string name, bool ignoreCase) + { + throw new NotImplementedException(); + } + + public override Type[] GetInterfaces() + { + var interfaces = new List(); + foreach (INamedTypeSymbol i in _typeSymbol.Interfaces) + { + interfaces.Add(i.AsType(_metadataLoadContext)); + } + return interfaces.ToArray(); + } + + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + { + var members = new List(); + foreach (ISymbol m in _typeSymbol.GetMembers()) + { + members.Add(new MemberInfoWrapper(m, _metadataLoadContext)); + } + return members.ToArray(); + } + + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + { + var methods = new List(); + foreach (ISymbol m in _typeSymbol.GetMembers()) + { + // TODO: Efficiency + if (m is IMethodSymbol method && !_namedTypeSymbol.Constructors.Contains(method)) + { + methods.Add(method.AsMethodInfo(_metadataLoadContext)); + } + } + return methods.ToArray(); + } + + public override Type GetNestedType(string name, BindingFlags bindingAttr) + { + throw new NotImplementedException(); + } + + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + { + var nestedTypes = new List(); + foreach (INamedTypeSymbol type in _typeSymbol.GetTypeMembers()) + { + nestedTypes.Add(type.AsType(_metadataLoadContext)); + } + return nestedTypes.ToArray(); + } + + // TODO: make sure to use bindingAttr for correctness. Current implementation assumes public and non-static. + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + { + var properties = new List(); + + foreach (ISymbol item in _typeSymbol.GetMembers()) + { + if (item is IPropertySymbol property) + { + if ((item.DeclaredAccessibility & Accessibility.Public) == Accessibility.Public) + { + properties.Add(new PropertyInfoWrapper(property, _metadataLoadContext)); + } + } + } + + return properties.ToArray(); + } + + public override object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] namedParameters) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotImplementedException(); + } + + private TypeAttributes? _typeAttributes; + + protected override TypeAttributes GetAttributeFlagsImpl() + { + if (!_typeAttributes.HasValue) + { + _typeAttributes = default(TypeAttributes); + + if (_typeSymbol.IsAbstract) + { + _typeAttributes |= TypeAttributes.Abstract; + } + + if (_typeSymbol.TypeKind == TypeKind.Interface) + { + _typeAttributes |= TypeAttributes.Interface; + } + + if (_typeSymbol.ContainingType != null && _typeSymbol.DeclaredAccessibility == Accessibility.Private) + { + _typeAttributes |= TypeAttributes.NestedPrivate; + } + } + + return _typeAttributes.Value; + } + + protected override ConstructorInfo GetConstructorImpl(BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier[] modifiers) + { + foreach (ConstructorInfo constructor in GetConstructors(bindingAttr)) + { + ParameterInfo[] parameters = constructor.GetParameters(); + + if (parameters.Length == types.Length) + { + bool mismatched = false; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].ParameterType != types[i]) + { + mismatched = true; + break; + } + } + + if (!mismatched) + { + return constructor; + } + } + } + + return null; + } + + protected override MethodInfo GetMethodImpl(string name, BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier[] modifiers) + { + throw new NotImplementedException(); + } + + protected override PropertyInfo GetPropertyImpl(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, ParameterModifier[] modifiers) + { + // TODO: peformance; caching; honor bindingAttr + foreach (PropertyInfo propertyInfo in GetProperties(bindingAttr)) + { + if (propertyInfo.Name == name) + { + return propertyInfo; + } + } + + return null!; + } + + protected override bool HasElementTypeImpl() + { + throw new NotImplementedException(); + } + + protected override bool IsArrayImpl() + { + return _arrayTypeSymbol != null; + } + + private Type _valueType; + + protected override bool IsValueTypeImpl() + { + _valueType ??= _metadataLoadContext.Resolve(typeof(ValueType)); + return IsSubclassOf(_valueType); + } + + protected override bool IsByRefImpl() + { + throw new NotImplementedException(); + } + + protected override bool IsCOMObjectImpl() + { + throw new NotImplementedException(); + } + + protected override bool IsPointerImpl() + { + throw new NotImplementedException(); + } + + protected override bool IsPrimitiveImpl() + { + throw new NotImplementedException(); + } + + public override bool IsAssignableFrom(Type c) + { + if (c is TypeWrapper tr) + { + return tr._typeSymbol.AllInterfaces.Contains(_typeSymbol, SymbolEqualityComparer.Default) || + (tr._namedTypeSymbol != null && tr._namedTypeSymbol.BaseTypes().Contains(_typeSymbol, SymbolEqualityComparer.Default)); + } + else if (_metadataLoadContext.Resolve(c) is TypeWrapper trr) + { + return trr._typeSymbol.AllInterfaces.Contains(_typeSymbol, SymbolEqualityComparer.Default) || + (trr._namedTypeSymbol != null && trr._namedTypeSymbol.BaseTypes().Contains(_typeSymbol, SymbolEqualityComparer.Default)); + } + return false; + } + +#pragma warning disable RS1024 // Compare symbols correctly + public override int GetHashCode() => _typeSymbol.GetHashCode(); +#pragma warning restore RS1024 // Compare symbols correctly + + public override bool Equals(object o) + { + if (o is TypeWrapper tw) + { + return _typeSymbol.Equals(tw._typeSymbol, SymbolEqualityComparer.Default); + } + else if (o is Type t && _metadataLoadContext.Resolve(t) is TypeWrapper tww) + { + return _typeSymbol.Equals(tww._typeSymbol, SymbolEqualityComparer.Default); + } + + return base.Equals(o); + } + + public override bool Equals(Type o) + { + if (o is TypeWrapper tw) + { + return _typeSymbol.Equals(tw._typeSymbol, SymbolEqualityComparer.Default); + } + else if (_metadataLoadContext.Resolve(o) is TypeWrapper tww) + { + return _typeSymbol.Equals(tww._typeSymbol, SymbolEqualityComparer.Default); + } + return base.Equals(o); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj new file mode 100644 index 00000000000000..92e70583da6722 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj @@ -0,0 +1,48 @@ + + + netstandard2.0 + false + enable + + CS1574 + + + + $(DefineConstants);BUILDING_SOURCE_GENERATOR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Text.Json/gen/TypeMetadata.cs b/src/libraries/System.Text.Json/gen/TypeMetadata.cs new file mode 100644 index 00000000000000..7f9ea6385f2150 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/TypeMetadata.cs @@ -0,0 +1,82 @@ +// 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.Text.Json.Serialization; + +namespace System.Text.Json.SourceGeneration +{ + [DebuggerDisplay("Type={Type}, ClassType={ClassType}")] + internal class TypeMetadata + { + private bool _hasBeenInitialized; + + public string CompilableName { get; private set; } + + public string FriendlyName { get; private set; } + + public Type Type { get; private set; } + + public ClassType ClassType { get; private set; } + + public bool IsValueType { get; private set; } + + public JsonNumberHandling? NumberHandling { get; private set; } + + public List? PropertiesMetadata { get; private set; } + + public CollectionType CollectionType { get; private set; } + + public TypeMetadata? CollectionKeyTypeMetadata { get; private set; } + + public TypeMetadata? CollectionValueTypeMetadata { get; private set; } + + public ObjectConstructionStrategy ConstructionStrategy { get; private set; } + + public TypeMetadata? NullableUnderlyingTypeMetadata { get; private set; } + + public string? ConverterInstantiationLogic { get; private set; } + + public bool ContainsOnlyPrimitives { get; private set; } + + public void Initialize( + string compilableName, + string friendlyName, + Type type, + ClassType classType, + bool isValueType, + JsonNumberHandling? numberHandling, + List? propertiesMetadata, + CollectionType collectionType, + TypeMetadata? collectionKeyTypeMetadata, + TypeMetadata? collectionValueTypeMetadata, + ObjectConstructionStrategy constructionStrategy, + TypeMetadata? nullableUnderlyingTypeMetadata, + string? converterInstantiationLogic, + bool containsOnlyPrimitives) + { + if (_hasBeenInitialized) + { + throw new InvalidOperationException("Type metadata has already been initialized."); + } + + _hasBeenInitialized = true; + + CompilableName = compilableName; + FriendlyName = friendlyName; + Type = type; + ClassType = classType; + IsValueType = isValueType; + NumberHandling = numberHandling; + PropertiesMetadata = propertiesMetadata; + CollectionType = collectionType; + CollectionKeyTypeMetadata = collectionKeyTypeMetadata; + CollectionValueTypeMetadata = collectionValueTypeMetadata; + ConstructionStrategy = constructionStrategy; + NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata; + ConverterInstantiationLogic = converterInstantiationLogic; + ContainsOnlyPrimitives = containsOnlyPrimitives; + } + } +} diff --git a/src/libraries/System.Text.Json/pkg/System.Text.Json.pkgproj b/src/libraries/System.Text.Json/pkg/System.Text.Json.pkgproj index c02e237aea1ea3..c714baa66768a7 100644 --- a/src/libraries/System.Text.Json/pkg/System.Text.Json.pkgproj +++ b/src/libraries/System.Text.Json/pkg/System.Text.Json.pkgproj @@ -5,6 +5,7 @@ net461;netcoreapp2.0;uap10.0.16299;$(AllXamarinFrameworks) +