From 5e8610113d151edd1cbe4dcc44eb772fb41f3479 Mon Sep 17 00:00:00 2001 From: "Christian Donn Relacion Sarmago (Synapxe)" Date: Thu, 30 May 2024 18:06:08 +0800 Subject: [PATCH] Test out source generated mappings #2794 --- Hl7.Fhir.sln | 8 + src/Benchmarks/ModelInspectorBenchmarks.cs | 201 +++++++++++---- .../Introspection/ClassMapping.cs | 22 +- .../Introspection/ClassMappingCollection.cs | 15 +- .../Introspection/EnumMapping.cs | 2 +- .../Introspection/EnumMappingCollection.cs | 15 +- .../Introspection/ModelInspector.cs | 33 ++- .../AllFhirTypesGenerator.cs | 232 ++++++++++++++++++ .../Helpers.cs | 117 +++++++++ .../Hl7.Fhir.Model.SourceGeneration.csproj | 18 ++ .../SyntaxContextReceiver.cs | 73 ++++++ src/Hl7.Fhir.R4/Hl7.Fhir.R4.csproj | 1 + src/Hl7.Fhir.R4B/Hl7.Fhir.R4B.csproj | 1 + src/Hl7.Fhir.R5/Hl7.Fhir.R5.csproj | 1 + src/Hl7.Fhir.STU3/Hl7.Fhir.STU3.csproj | 1 + .../Hl7.Fhir.Shims.STU3AndUp.projitems | 1 + .../Model/ModelInfo.cs | 14 +- .../SourceGeneration/ModelInfo..cs | 52 ++++ 18 files changed, 724 insertions(+), 83 deletions(-) create mode 100644 src/Hl7.Fhir.Model.SourceGeneration/AllFhirTypesGenerator.cs create mode 100644 src/Hl7.Fhir.Model.SourceGeneration/Helpers.cs create mode 100644 src/Hl7.Fhir.Model.SourceGeneration/Hl7.Fhir.Model.SourceGeneration.csproj create mode 100644 src/Hl7.Fhir.Model.SourceGeneration/SyntaxContextReceiver.cs create mode 100644 src/Hl7.Fhir.Shims.STU3AndUp/SourceGeneration/ModelInfo..cs diff --git a/Hl7.Fhir.sln b/Hl7.Fhir.sln index 5710502d89..f93e6efd91 100644 --- a/Hl7.Fhir.sln +++ b/Hl7.Fhir.sln @@ -103,6 +103,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hl7.Fhir.STU3.Tests", "src\ EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Hl7.Fhir.Shims.Base", "src\Hl7.Fhir.Shims.Base\Hl7.Fhir.Shims.Base.shproj", "{150A59A2-371D-4747-8B08-C8E6340EC962}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hl7.Fhir.Model.SourceGeneration", "src\Hl7.Fhir.Model.SourceGeneration\Hl7.Fhir.Model.SourceGeneration.csproj", "{BFEC5DED-1666-4F15-8483-963C4261DB63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -296,6 +298,12 @@ Global {CFF0DDA5-5155-4144-B426-91C7623D08E7}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {CFF0DDA5-5155-4144-B426-91C7623D08E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {CFF0DDA5-5155-4144-B426-91C7623D08E7}.Release|Any CPU.Build.0 = Release|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFEC5DED-1666-4F15-8483-963C4261DB63}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Benchmarks/ModelInspectorBenchmarks.cs b/src/Benchmarks/ModelInspectorBenchmarks.cs index 20a94ff49c..7b3626c36a 100644 --- a/src/Benchmarks/ModelInspectorBenchmarks.cs +++ b/src/Benchmarks/ModelInspectorBenchmarks.cs @@ -1,66 +1,173 @@ using BenchmarkDotNet.Attributes; using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; +using Hl7.Fhir.Utility; using System; +using System.Linq; -namespace Firely.Sdk.Benchmarks +namespace Firely.Sdk.Benchmarks; + +[MemoryDiagnoser] +public class ModelInspectorBenchmarks { - [MemoryDiagnoser] - public class ModelInspectorBenchmarks + [GlobalSetup] + public void BenchmarkSetup() { - [GlobalSetup] - public void BenchmarkSetup() - { - // PropertyInfoExtensions.NoCodeGenSupport = true; - } + // PropertyInfoExtensions.NoCodeGenSupport = true; + var inspector = ScanAssemblies(); + Console.WriteLine($"ScanAssemblies: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); - internal Type[] PopularResources = new Type[] - { - typeof(Observation), typeof(Patient), typeof(Organization), - typeof(Procedure), typeof(StructureDefinition), typeof(MedicationRequest), - typeof(ValueSet), typeof(Questionnaire), typeof(Appointment), - typeof(OperationOutcome) - }; - - [Benchmark] - public void ScanAssemblies() - { - // Make sure we work uncached initially on each run - //ModelInspector.Clear(); - //ClassMapping.Clear(); + inspector = ImportTypeAllResources(); + Console.WriteLine($"ImportTypeAllResources: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); + + inspector = NewWithSourceGenMappings(); + Console.WriteLine($"NewWithSourceGenMappings: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); + + inspector = NewWithTypesAllResources(); + Console.WriteLine($"NewWithTypesAllResources: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); + + inspector = ImportType4Resources(); + Console.WriteLine($"ImportType4Resources: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); + + inspector = NewWithTypes4Resources(); + Console.WriteLine($"NewWithTypes4Resources: Types: {inspector.ClassMappings.Count}, Enums: {inspector.EnumMappings.Count()}"); + } + + internal static readonly Type[] PopularResources = new Type[] + { + typeof(Observation), typeof(Patient), typeof(Organization), + typeof(Procedure), typeof(StructureDefinition), typeof(MedicationRequest), + typeof(ValueSet), typeof(Questionnaire), typeof(Appointment), + typeof(OperationOutcome) + }; + + + internal static readonly Type[] TestResources = + [ + typeof(CapabilityStatement), typeof(Appointment), typeof(OperationDefinition), + ]; - _ = ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); + //[IterationSetup] + //[IterationCleanup] + public void ResetCache() + { + // Make sure we work uncached initially on each run + ModelInspector.Clear(); + ClassMapping.Clear(); + EnumMapping.Clear(); + } + + [Benchmark] + public ModelInspector ScanAssemblies() + { + ResetCache(); + return ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); + } + + [Benchmark] + public ModelInspector ImportTypeAllResources() + { + ResetCache(); + var inspector = new ModelInspector(Hl7.Fhir.Specification.FhirRelease.R5); + foreach (var t in ModelInfo.GenerateAllFhirTypes()) + { + inspector.ImportType(t); } - [Benchmark] - public void GetPropertiesPopular() + return inspector; + } + + [Benchmark] + public ModelInspector ImportType4Resources() + { + ResetCache(); + var inspector = new ModelInspector(Hl7.Fhir.Specification.FhirRelease.R5); //ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); + foreach (var t in TestResources) { - // Make sure we work uncached initially on each run - //ModelInspector.Clear(); - //ClassMapping.Clear(); - - var inspector = ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); - foreach (var t in PopularResources) - { - var mapping = inspector.FindClassMapping(t); - _ = mapping.PropertyMappings; - } + inspector.ImportType(t); } - //[Benchmark] - //public void GetPropertiesAll() - //{ - // // Make sure we work uncached initially on each run - // ModelInspector.Clear(); - // ClassMapping.Clear(); + return inspector; + } - // var inspector = ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); - // foreach (var m in inspector.ClassMappings) - // { - // _ = m.PropertyMappings; - // } - //} + [Benchmark] + public ModelInspector NewWithSourceGenMappings() + { + FhirReleaseParser.Parse(ModelInfo.Version); + ResetCache(); + return new ModelInspector(ModelInfo.GenerateAllClassMappings(), ModelInfo.GenerateAllEnumMappings()) { FhirRelease = Hl7.Fhir.Specification.FhirRelease.R5 }; + } + + [Benchmark] + public ModelInspector NewWithTypesAllResources() + { + FhirReleaseParser.Parse(ModelInfo.Version); + ResetCache(); + return ModelInspector.ForTypes(ModelInfo.Version, ModelInfo.GenerateAllFhirTypes()); + } + [Benchmark] + public ModelInspector NewWithTypes4Resources() + { + ResetCache(); + var inspector = ModelInspector.ForTypes(ModelInfo.Version, [ + typeof(CapabilityStatement), + typeof(CapabilityStatement.ConditionalDeleteStatus), + typeof(CapabilityStatement.ConditionalReadStatus), + typeof(CapabilityStatement.DocumentMode), + typeof(CapabilityStatement.EventCapabilityMode), + typeof(CapabilityStatement.ReferenceHandlingPolicy), + typeof(CapabilityStatement.ResourceVersionPolicy), + typeof(CapabilityStatement.RestfulCapabilityMode), + typeof(CapabilityStatement.SystemRestfulInteraction), + typeof(CapabilityStatement.TypeRestfulInteraction), + typeof(CapabilityStatement.DocumentComponent), + typeof(CapabilityStatement.EndpointComponent), + typeof(CapabilityStatement.ImplementationComponent), + typeof(CapabilityStatement.MessagingComponent), + typeof(CapabilityStatement.OperationComponent), + typeof(CapabilityStatement.ResourceComponent), + typeof(CapabilityStatement.ResourceInteractionComponent), + typeof(CapabilityStatement.RestComponent), + typeof(CapabilityStatement.SearchParamComponent), + typeof(CapabilityStatement.SecurityComponent), + typeof(CapabilityStatement.SoftwareComponent), + typeof(CapabilityStatement.SupportedMessageComponent), + typeof(CapabilityStatement.SystemInteractionComponent), + typeof(Appointment), + typeof(Appointment.AppointmentStatus), + typeof(Appointment.IANATimezones), + typeof(Appointment.ParticipationStatus), + typeof(Appointment.WeekOfMonth), + typeof(Appointment.MonthlyTemplateComponent), + typeof(Appointment.ParticipantComponent), + typeof(Appointment.RecurrenceTemplateComponent), + typeof(Appointment.WeeklyTemplateComponent), + typeof(Appointment.YearlyTemplateComponent), + typeof(OperationDefinition), + typeof(OperationDefinition.BindingComponent), + typeof(OperationDefinition.OperationKind), + typeof(OperationDefinition.OperationParameterScope), + typeof(OperationDefinition.OverloadComponent), + typeof(OperationDefinition.ParameterComponent), + typeof(OperationDefinition.ReferencedFromComponent), + ]); + return inspector; } + + //[Benchmark] + //public void GetPropertiesAll() + //{ + // // Make sure we work uncached initially on each run + // ModelInspector.Clear(); + // ClassMapping.Clear(); + + // var inspector = ModelInspector.ForAssembly(typeof(ModelInfo).Assembly); + // foreach (var m in inspector.ClassMappings) + // { + // _ = m.PropertyMappings; + // } + //} + } diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs index ef314bd070..1b336c884c 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs @@ -106,7 +106,7 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res return true; } - private ClassMapping(string name, Type nativeType, FhirRelease release) + internal ClassMapping(string name, Type nativeType, FhirRelease release) { Name = name; NativeType = nativeType; @@ -138,29 +138,29 @@ private ClassMapping(string name, Type nativeType, FhirRelease release) /// /// The .NET class that implements the FHIR datatype/resource /// - public Type NativeType { get; private set; } + public Type NativeType { get; internal set; } /// /// Is true when this class represents a Resource datatype. /// - public bool IsResource { get; private set; } = false; + public bool IsResource { get; internal set; } = false; /// /// Is true when this class represents a FHIR primitive /// /// This is different from a .NET primitive, as FHIR primitives are complex types with a primitive value. - public bool IsFhirPrimitive { get; private set; } = false; + public bool IsFhirPrimitive { get; internal set; } = false; /// /// The element is of an atomic .NET type, not a FHIR generated POCO. /// - public bool IsPrimitive { get; private set; } = false; + public bool IsPrimitive { get; internal set; } = false; /// /// Is true when this class represents a code with a required binding. /// /// See . - public bool IsCodeOfT { get; private set; } = false; + public bool IsCodeOfT { get; internal set; } = false; /// /// Indicates whether this class represents the nested complex type for a backbone element. @@ -171,25 +171,25 @@ private ClassMapping(string name, Type nativeType, FhirRelease release) /// /// Indicates whether this class represents the nested complex type for a backbone element. /// - public bool IsBackboneType { get; private set; } = false; + public bool IsBackboneType { get; internal set; } = false; /// /// If this is a backbone type (), then this contains the path /// in the StructureDefinition where the backbone was defined first. /// - public string? DefinitionPath { get; private set; } + public string? DefinitionPath { get; internal set; } /// /// Indicates whether this class can be used for binding. /// - public bool IsBindable { get; private set; } + public bool IsBindable { get; internal set; } /// /// The canonical for the StructureDefinition defining this type /// /// Will be null for backbone types. - public string? Canonical { get; private set; } + public string? Canonical { get; internal set; } // This list is created lazily. This not only improves initial startup time of // applications but also ensures circular references between types will not cause loops. @@ -212,7 +212,7 @@ private PropertyMappingCollection propertyMappings /// The collection of zero or more (or subclasses) declared /// on this class. /// - public ValidationAttribute[] ValidationAttributes { get; private set; } = Array.Empty(); + public ValidationAttribute[] ValidationAttributes { get; internal set; } = Array.Empty(); /// /// Holds a reference to a property that represents the value of a FHIR Primitive. This diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMappingCollection.cs b/src/Hl7.Fhir.Base/Introspection/ClassMappingCollection.cs index c39ac4b542..ca53668a88 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMappingCollection.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMappingCollection.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Hl7.Fhir.Introspection { @@ -18,12 +19,16 @@ internal class ClassMappingCollection { public ClassMappingCollection() { - // Nothing + _byName = new(StringComparer.OrdinalIgnoreCase); + _byType = new(); + _byCanonical = new(); } public ClassMappingCollection(IEnumerable mappings) { - AddRange(mappings); + _byName = new(mappings.Select(static x => new KeyValuePair(x.Name, x)), StringComparer.OrdinalIgnoreCase); + _byType = new(mappings.Select(static x => new KeyValuePair(x.NativeType, x))); + _byCanonical = new(mappings.Where(static x => x.Canonical is not null).Select(static x => new KeyValuePair(x.Canonical!, x))); } /// @@ -53,19 +58,19 @@ public void AddRange(IEnumerable mappings) /// List of the class mappings, keyed by name. /// public IReadOnlyDictionary ByName => _byName; - private readonly ConcurrentDictionary _byName = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _byName; /// /// List of the class mappings, keyed by canonical. /// public IReadOnlyDictionary ByCanonical => _byCanonical; - private readonly ConcurrentDictionary _byCanonical = new(); + private readonly ConcurrentDictionary _byCanonical; /// /// List of the class mappings, keyed by canonical. /// public IReadOnlyDictionary ByType => _byType; - public readonly ConcurrentDictionary _byType = new(); + public readonly ConcurrentDictionary _byType; } } diff --git a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs index 4d1a00e09f..6e8ac416a4 100644 --- a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs @@ -60,7 +60,7 @@ public static bool TryCreate(Type type, [NotNullWhen(true)] out EnumMapping? res return true; } - private EnumMapping(string name, string? canonical, Type nativeType, FhirRelease release, string? defaultCodeSystem) + internal EnumMapping(string name, string? canonical, Type nativeType, FhirRelease release, string? defaultCodeSystem) { Name = name; Canonical = canonical; diff --git a/src/Hl7.Fhir.Base/Introspection/EnumMappingCollection.cs b/src/Hl7.Fhir.Base/Introspection/EnumMappingCollection.cs index e6167b8f64..67b892fef5 100644 --- a/src/Hl7.Fhir.Base/Introspection/EnumMappingCollection.cs +++ b/src/Hl7.Fhir.Base/Introspection/EnumMappingCollection.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Hl7.Fhir.Introspection { @@ -18,12 +19,16 @@ internal class EnumMappingCollection { public EnumMappingCollection() { - // Nothing + _byName = new(StringComparer.OrdinalIgnoreCase); + _byType = new(); + _byCanonical = new(); } public EnumMappingCollection(IEnumerable mappings) { - AddRange(mappings); + _byName = new(mappings.Select(static x => new KeyValuePair(x.Name, x)), StringComparer.OrdinalIgnoreCase); + _byType = new(mappings.Select(static x => new KeyValuePair(x.NativeType, x))); + _byCanonical = new(mappings.Where(static x => x.Canonical is not null).Select(static x => new KeyValuePair(x.Canonical!, x))); } /// @@ -54,19 +59,19 @@ public void AddRange(IEnumerable mappings) /// List of the enumerations, keyed by name. /// public IReadOnlyDictionary ByName => _byName; - private readonly ConcurrentDictionary _byName = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _byName; /// /// List of the enums, keyed by canonical. /// public IReadOnlyDictionary ByCanonical => _byCanonical; - private readonly ConcurrentDictionary _byCanonical = new(); + private readonly ConcurrentDictionary _byCanonical; /// /// List of the enums, keyed by canonical. /// public IReadOnlyDictionary ByType => _byType; - public readonly ConcurrentDictionary _byType = new(); + public readonly ConcurrentDictionary _byType; } } diff --git a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs index cbb08dae64..75f2527fbc 100644 --- a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs +++ b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs @@ -118,6 +118,36 @@ static bool isFhirModelAssembly(Assembly a) => /// public static ModelInspector ForType() where T : Resource => ForType(typeof(T)); + public ModelInspector(IEnumerable classMappings, IEnumerable enumMappings) + { + _classMappings = new ClassMappingCollection(classMappings); + _enumMappings = new EnumMappingCollection(enumMappings); + } + + public static ModelInspector ForTypes(string version, ReadOnlySpan types) + { + var fhirRelease = FhirReleaseParser.Parse(version); + var classMappings = new List(types.Length); + var enumMappings = new List(types.Length); + foreach (var type in types) + { + if (!type.IsEnum && ClassMapping.TryCreate(type, out var classMapping, fhirRelease)) + { + classMappings.Add(classMapping); + } + else if (type.IsEnum && EnumMapping.TryCreate(type, out var enumMapping, fhirRelease)) + { + enumMappings.Add(enumMapping); + } + } + + return new ModelInspector(classMappings, enumMappings) + { + FhirRelease = fhirRelease, + FhirVersion = version, + }; + } + /// /// Returns a fully configured with the /// FHIR metadata contents of the base assembly @@ -144,7 +174,7 @@ public ModelInspector(FhirRelease fhirRelease) /// /// This is taken from the ModelInfo.Version string when the ModelInspector /// reflects on a satellite assembly. - public string? FhirVersion { get; private set; } + public string? FhirVersion { get; internal set; } private readonly EnumMappingCollection _enumMappings = new(); @@ -352,6 +382,7 @@ public bool IsInstanceTypeFor(string superclass, string subclass) public string? GetFhirTypeNameForType(Type type) => FindClassMapping(type) is { } mapping ? mapping.Name : null; + #endregion } } diff --git a/src/Hl7.Fhir.Model.SourceGeneration/AllFhirTypesGenerator.cs b/src/Hl7.Fhir.Model.SourceGeneration/AllFhirTypesGenerator.cs new file mode 100644 index 0000000000..a02a69497f --- /dev/null +++ b/src/Hl7.Fhir.Model.SourceGeneration/AllFhirTypesGenerator.cs @@ -0,0 +1,232 @@ +#define CACHING // this changes the source generated code so that it uses Lazy[] so that the generated types are done only once. +//#define LAUNCH_DEBUGGER +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Hl7.Fhir.Model.SourceGeneration; + +[Generator] +public class AllFhirTypesGenerator : ISourceGenerator +{ + private static readonly string AssemblyVersion = typeof(AllFhirTypesGenerator).Assembly.GetCustomAttribute()?.InformationalVersion ?? "1.0.0.0"; + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(SyntaxContextReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.MethodDeclarations.Count == 0) + { + // nothing to do yet + return; + } + +#if LAUNCH_DEBUGGER + System.Diagnostics.Debugger.Launch(); +#endif + var hl7Asms = receiver.MethodDeclarations + .SelectMany(x => x.Assemblies) + .Distinct(SymbolEqualityComparer.Default) + .OfType() + .ToArray(); + + // if the AssembliesContainingTypes property is empty, we scan for referenced + // assemblies with the FhirModelAssemblyAttribute + if (hl7Asms.Length == 0) + { + hl7Asms = context.Compilation.References + .Select(context.Compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Where(x => x.TryGetAttribute("Hl7.Fhir.Introspection.FhirModelAssemblyAttribute", out _)) + .ToArray(); + } + + var fhirTypes = new HashSet(SymbolEqualityComparer.Default); + var classMappings = new List<(INamedTypeSymbol, AttributeData)>(); + var enumMappings = new List<(INamedTypeSymbol, AttributeData)>(); + + context.Compilation.GlobalNamespace.TraverseNamespace(fhirTypes, classMappings, enumMappings); + foreach (var asm in hl7Asms) + { + asm.GlobalNamespace.TraverseNamespace(fhirTypes, classMappings, enumMappings); + } + + if (receiver.MethodDeclarations.Count > 0) + { +#if CACHING + var arrayTerminator = "]);"; + var arrayAccess = ".Value"; +#else + var arrayTerminator = "];"; + var arrayAccess = "()"; +#endif + StringBuilder code = new( + $$""" + namespace Hl7.Fhir.Model.SourceGeneration + { + using Hl7.Fhir.Introspection; + using System.Linq; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Hl7.Fhir.Model.SourceGeneration", "{{AssemblyVersion}}")] + internal static class AllTypesContainer + { + private static readonly Hl7.Fhir.Specification.FhirRelease FhirRelease = Hl7.Fhir.Utility.FhirReleaseParser.Parse(Hl7.Fhir.Model.ModelInfo.Version); + + private static global::System.Collections.Generic.IEnumerable GetValidationAttributes(global::System.Reflection.MemberInfo t, Hl7.Fhir.Specification.FhirRelease version) + { + return Hl7.Fhir.Utility.ReflectionHelper.GetAttributes(t).Where(isRelevant); + + bool isRelevant(global::System.Attribute a) => a is not Hl7.Fhir.Introspection.IFhirVersionDependent vd || a.AppliesToRelease(version); + } + + public static {{WriteMethodSignature("System.Type", "AllTypes")}}() => + [ + + """); + + foreach (var fhirType in fhirTypes) + { + code.AppendLine($" typeof({fhirType.ToDisplayString()}),"); + } + + code.Append( + $$""" + {{arrayTerminator}} + public static {{WriteMethodSignature("Hl7.Fhir.Introspection.ClassMapping", "AllClassMappings")}}() => + [ + + """); + + foreach (var fhirType in classMappings) + { + if (fhirType.Item1.IsCqlType()) + { + WriteCqlType(code, fhirType); + } + else + { + WriteFhirType(code, fhirType); + } + } + + code.Append( + $$""" + {{arrayTerminator}} + public static {{WriteMethodSignature("Hl7.Fhir.Introspection.EnumMapping", "AllEnumMappings")}}() => + [ + + """); + + foreach (var fhirType in enumMappings) + { + WriteFhirEnumeration(code, fhirType); + } + + code.Append( + $$""" + {{arrayTerminator}} + } + } + + """); + foreach (var item in receiver.MethodDeclarations) + { + var methodSymbol = item.Method; + var returnType = methodSymbol.ReturnType.ToDisplayString(); + var propertyToAccess = $"All{((IArrayTypeSymbol)methodSymbol.ReturnType).ElementType.Name}s"; + + // generate code + code.Append( + $$""" + + namespace {{item.Class.ContainingNamespace.ToDisplayString()}} + { + {{item.Class.DeclaredAccessibility.ToCSharp()}}{{(item.Class.IsStatic ? " static" : string.Empty)}} partial class {{item.Class.Name}} + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Hl7.Fhir.Model.SourceGeneration", "{{AssemblyVersion}}")] + {{item.Method.DeclaredAccessibility.ToCSharp()}}{{(item.Method.IsStatic ? " static" : string.Empty)}} partial {{returnType}} {{item.Method.Name}}() => Hl7.Fhir.Model.SourceGeneration.AllTypesContainer.{{propertyToAccess}}{{arrayAccess}}; + } + } + """); + } + +#if DEBUG + var csharp = code.ToString(); + System.Diagnostics.Debug.WriteLine(csharp); +#endif + context.AddSource("AllTypesContainer.g.cs", SourceText.From(code.ToString()!, Encoding.UTF8)); + } + + static string WriteMethodSignature(string returnType, string methodName) + { +#if CACHING + return $"readonly System.Lazy<{returnType}[]> {methodName} = new("; +#else + return $"{returnType}[] {methodName}"; +#endif + } + + static void WriteCqlType(StringBuilder code, (INamedTypeSymbol, AttributeData) symbol) + { + var cqlType = symbol.Item1; + code.AppendLine($" new Hl7.Fhir.Introspection.ClassMapping(\"System.{cqlType.Name}\", typeof({cqlType.ToDisplayString()}), FhirRelease),"); + } + + static void WriteFhirType(StringBuilder code, (INamedTypeSymbol, AttributeData) symbol) + { + // for FhirTypeAttribute + + var fhirType = symbol.Item1; + var data = symbol.Item2; + var name = data.ConstructorArguments[0].Value?.ToString(); + var canonical = data.ConstructorArguments.ElementAtOrDefault(1).Value?.ToString(); + var isResource = data.NamedArguments.FirstOrDefault(x => x.Key == "IsResource").Value.Value?.ToString()?.ToLower(); + var isFhirPrimitive = fhirType.IsDerivedFrom("Hl7.Fhir.Model.PrimitiveType"); + var isCodeOfT = fhirType.IsCodeOfT(); + var hasValidationAttributes = fhirType.GetAttributes().Any(attrib => attrib.AttributeClass.IsDerivedFrom("System.ComponentModel.Validation.ValidationAttribute")); + + bool isBackbone = false; + string? definitionPath = null; + if (fhirType.ContainingType != null && fhirType.TryGetAttribute("Hl7.Fhir.Introspection.BackboneTypeAttribute", out var backboneAttribute)) + { + isBackbone = true; + definitionPath = isBackbone ? backboneAttribute!.ConstructorArguments[0].Value?.ToString() : null; + } + + var isBindable = fhirType.TryGetAttribute("Hl7.Fhir.Introspection.BindableAttribute", out var bindableAttribute); + code.Append($$""" + new Hl7.Fhir.Introspection.ClassMapping("{{name}}", typeof({{fhirType.ToDisplayString()}}), FhirRelease) + { + IsResource = {{(isResource ?? "false")}}, + IsCodeOfT = {{isCodeOfT.ToString().ToLower()}}, + IsFhirPrimitive = {{isFhirPrimitive.ToString().ToLower()}}, + IsBackboneType = {{isBackbone.ToString().ToLower()}}, + DefinitionPath = {{definitionPath.SurroundWithQuotesOrNull()}}, + IsBindable = {{isBindable.ToString().ToLower()}}, + Canonical = {{canonical.SurroundWithQuotesOrNull()}}, + ValidationAttributes = {{(hasValidationAttributes ? $"GetValidationAttributes(typeof({fhirType.ToDisplayString()}), FhirRelease).ToArray(), // this can be optimized further" : "[],")}} + }, + """); + code.AppendLine($""); + } + + static void WriteFhirEnumeration(StringBuilder code, (INamedTypeSymbol, AttributeData) symbol) + { + // for FhirEnumerationAttribute + // arg1 is name, + // arg2 is the valueset, + // arg3 is the system + var enumType = symbol.Item1; + var data = symbol.Item2; + var name = data.ConstructorArguments[0].Value?.ToString(); + var valueset = data.ConstructorArguments[1].Value?.ToString(); + var system = data.ConstructorArguments.ElementAtOrDefault(2).Value?.ToString(); + code.AppendLine($" new Hl7.Fhir.Introspection.EnumMapping(\"{name}\", \"{valueset}\", typeof({enumType.ToDisplayString()}), FhirRelease, \"{system}\"),"); + } + } +} diff --git a/src/Hl7.Fhir.Model.SourceGeneration/Helpers.cs b/src/Hl7.Fhir.Model.SourceGeneration/Helpers.cs new file mode 100644 index 0000000000..9a235330c0 --- /dev/null +++ b/src/Hl7.Fhir.Model.SourceGeneration/Helpers.cs @@ -0,0 +1,117 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; + +namespace Hl7.Fhir.Model.SourceGeneration +{ + internal static class Helpers + { + public static string SurroundWithQuotesOrNull(this string? value) + { + if (string.IsNullOrEmpty(value)) + { + return "null"; + } + else + { + return $"\"{value}\""; + } + } + + public static string ToCSharp(this Accessibility accessibility) + => accessibility switch + { + Accessibility.Internal => "internal", + Accessibility.Private => "private", + Accessibility.ProtectedOrInternal or Accessibility.ProtectedAndInternal => "protected internal", + Accessibility.Protected => "protected", + Accessibility.Public => "public", + _ => "internal", + }; + + public static bool IsCodeOfT(this INamedTypeSymbol type) + { + return type.Arity == 1 && type.ToDisplayString().StartsWith("Hl7.Fhir.Mode.Code<"); + } + + public static bool IsDerivedFrom(this INamedTypeSymbol type, string typeName) + { + if (type.ToDisplayString() == typeName) + { + return true; + } + else if (type.BaseType is not null) + { + return IsDerivedFrom(type.BaseType, typeName); + } + + return false; + } + + public static bool IsCqlType(this INamedTypeSymbol type) + => type.ToDisplayString() == "Hl7.Fhir.ElementModel.Types.Any" || + type.BaseType?.ToDisplayString() == "Hl7.Fhir.ElementModel.Types.Any"; + + public static bool IsClassMapping(this INamedTypeSymbol type, out AttributeData? data) + => type.TryGetAttribute("Hl7.Fhir.Introspection.FhirTypeAttribute", out data) || IsCqlType(type); + + public static bool IsEnumMapping(this INamedTypeSymbol type, out AttributeData? data) + => type.TryGetAttribute("Hl7.Fhir.Utility.FhirEnumerationAttribute", out data); + + public static bool TryGetAttribute(this ISymbol symbol, string attributeTypeName, out AttributeData? attributeData) + { + foreach (var attributeListSyntax in symbol.GetAttributes()) + { + if (attributeListSyntax.AttributeClass?.ToDisplayString() == attributeTypeName) + { + attributeData = attributeListSyntax; + return true; + } + } + + attributeData = null; + return false; + } + + internal static void TraverseNamespace(this INamespaceSymbol ns, HashSet allTypes, List<(INamedTypeSymbol, AttributeData)> classMappings, List<(INamedTypeSymbol, AttributeData)> enumMappings) + { + foreach (var type in ns.GetTypeMembers()) + { + if (!type.IsGenericType && + type.CanBeReferencedByName && + (type.IsClassMapping(out _) || type.IsEnumMapping(out _))) + { + if (allTypes.Add(type)) + { + if (type.IsClassMapping(out var typeData)) + { + classMappings.Add((type, typeData)); + } + else if (type.IsEnumMapping(out var enumData)) + { + enumMappings.Add((type, enumData)); + } + + foreach (var nestedType in type.GetTypeMembers().Where(x => x.IsClassMapping(out _) || x.IsEnumMapping(out _))) + { + allTypes.Add(nestedType); + if (nestedType.IsClassMapping(out var typeData2)) + { + classMappings.Add((nestedType, typeData2)); + } + else if (nestedType.IsEnumMapping(out var enumData2)) + { + enumMappings.Add((nestedType, enumData2)); + } + } + } + } + } + + foreach (var childNamespace in ns.GetNamespaceMembers()) + { + TraverseNamespace(childNamespace, allTypes, classMappings, enumMappings); + } + } + } +} diff --git a/src/Hl7.Fhir.Model.SourceGeneration/Hl7.Fhir.Model.SourceGeneration.csproj b/src/Hl7.Fhir.Model.SourceGeneration/Hl7.Fhir.Model.SourceGeneration.csproj new file mode 100644 index 0000000000..4ce598022c --- /dev/null +++ b/src/Hl7.Fhir.Model.SourceGeneration/Hl7.Fhir.Model.SourceGeneration.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + true + 12 + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/src/Hl7.Fhir.Model.SourceGeneration/SyntaxContextReceiver.cs b/src/Hl7.Fhir.Model.SourceGeneration/SyntaxContextReceiver.cs new file mode 100644 index 0000000000..4ae4f481ce --- /dev/null +++ b/src/Hl7.Fhir.Model.SourceGeneration/SyntaxContextReceiver.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Integrated Health Information Systems Pte Ltd. All rights reserved. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Hl7.Fhir.Model.SourceGeneration; + +internal sealed class SyntaxContextReceiver : ISyntaxContextReceiver +{ + public const string GeneratedAllTypesAttributeName = "Hl7.Fhir.Model.GenerateAllFhirTypesAttribute"; + + public HashSet<(ITypeSymbol Class, IMethodSymbol Method, IAssemblySymbol[] Assemblies)> MethodDeclarations { get; } = new(); + + internal static SyntaxContextReceiver Create() + { + return new SyntaxContextReceiver(); + } + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (IsSyntaxTargetForGeneration(context, out var methodSymbol)) + { + var syntax = GetSemanticTargetForGeneration(methodSymbol); + if (syntax is not null) + { + MethodDeclarations.Add(syntax.Value); + } + } + } + + private static bool IsSyntaxTargetForGeneration(GeneratorSyntaxContext context, out IMethodSymbol? methodSymbol) + { + if (context.Node is MethodDeclarationSyntax methodDeclarationSyntax) + { + var sm = context.SemanticModel; + methodSymbol = sm.GetDeclaredSymbol(methodDeclarationSyntax)!; + if (methodSymbol.IsPartialDefinition) + { + return true; + } + } + + methodSymbol = null; + return false; + } + + private static (ITypeSymbol Class, IMethodSymbol Method, IAssemblySymbol[] Assemblies)? GetSemanticTargetForGeneration(IMethodSymbol methodSymbol) + { + if (!methodSymbol.TryGetAttribute(GeneratedAllTypesAttributeName, out var attributeSyntax)) + { + return null; + } + + var typeSymbol = methodSymbol.ContainingType; + + var argval = attributeSyntax.NamedArguments.FirstOrDefault(x => x.Key == "AssembliesContainingTypes").Value; + var arg = !argval.IsNull ? argval.Values : []; + var assemblies = arg + .Select(x => x.Value) + .OfType() + .Select(e => e.ContainingAssembly) + .Distinct(SymbolEqualityComparer.Default) + .OfType() + .ToArray(); + return (typeSymbol, methodSymbol, assemblies); + } +} diff --git a/src/Hl7.Fhir.R4/Hl7.Fhir.R4.csproj b/src/Hl7.Fhir.R4/Hl7.Fhir.R4.csproj index 75f2de645c..993d1ae7cb 100644 --- a/src/Hl7.Fhir.R4/Hl7.Fhir.R4.csproj +++ b/src/Hl7.Fhir.R4/Hl7.Fhir.R4.csproj @@ -15,6 +15,7 @@ + \ No newline at end of file diff --git a/src/Hl7.Fhir.R4B/Hl7.Fhir.R4B.csproj b/src/Hl7.Fhir.R4B/Hl7.Fhir.R4B.csproj index 16d8565c55..739e8eab0a 100644 --- a/src/Hl7.Fhir.R4B/Hl7.Fhir.R4B.csproj +++ b/src/Hl7.Fhir.R4B/Hl7.Fhir.R4B.csproj @@ -15,6 +15,7 @@ + \ No newline at end of file diff --git a/src/Hl7.Fhir.R5/Hl7.Fhir.R5.csproj b/src/Hl7.Fhir.R5/Hl7.Fhir.R5.csproj index 6a199a286c..4547138c44 100644 --- a/src/Hl7.Fhir.R5/Hl7.Fhir.R5.csproj +++ b/src/Hl7.Fhir.R5/Hl7.Fhir.R5.csproj @@ -15,6 +15,7 @@ + \ No newline at end of file diff --git a/src/Hl7.Fhir.STU3/Hl7.Fhir.STU3.csproj b/src/Hl7.Fhir.STU3/Hl7.Fhir.STU3.csproj index f9f6fdff2b..3aaeae571c 100644 --- a/src/Hl7.Fhir.STU3/Hl7.Fhir.STU3.csproj +++ b/src/Hl7.Fhir.STU3/Hl7.Fhir.STU3.csproj @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/Hl7.Fhir.Shims.STU3AndUp.projitems b/src/Hl7.Fhir.Shims.STU3AndUp/Hl7.Fhir.Shims.STU3AndUp.projitems index f6cab5e389..d955600fdd 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/Hl7.Fhir.Shims.STU3AndUp.projitems +++ b/src/Hl7.Fhir.Shims.STU3AndUp/Hl7.Fhir.Shims.STU3AndUp.projitems @@ -29,6 +29,7 @@ + \ No newline at end of file diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/Model/ModelInfo.cs b/src/Hl7.Fhir.Shims.STU3AndUp/Model/ModelInfo.cs index 02a54c3061..4e99598545 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/Model/ModelInfo.cs +++ b/src/Hl7.Fhir.Shims.STU3AndUp/Model/ModelInfo.cs @@ -259,19 +259,7 @@ public static bool IsInstanceTypeFor(FHIRAllTypes superclass, FHIRAllTypes subcl /// Gets the providing metadata for the resources and /// datatypes in this release of FHIR. /// - public static ModelInspector ModelInspector - { - get - { - var inspector = ModelInspector.ForAssembly(typeof(ModelInfo).GetTypeInfo().Assembly); - if (inspector.FhirRelease != Specification.FhirRelease.STU3) - { - // In case of release 4 or higher, also load the assembly with common conformance resources, like StructureDefinition - inspector.Import(typeof(StructureDefinition).GetTypeInfo().Assembly); - } - return inspector; - } - } + public static ModelInspector ModelInspector => cachedModelInspector.Value; } public static class ModelInfoExtensions diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/SourceGeneration/ModelInfo..cs b/src/Hl7.Fhir.Shims.STU3AndUp/SourceGeneration/ModelInfo..cs new file mode 100644 index 0000000000..6a804e501f --- /dev/null +++ b/src/Hl7.Fhir.Shims.STU3AndUp/SourceGeneration/ModelInfo..cs @@ -0,0 +1,52 @@ +using Hl7.Fhir.Introspection; +using Hl7.Fhir.Specification; +using Hl7.Fhir.Utility; +using System; + +namespace Hl7.Fhir.Model +{ + /// + /// Contains partial stubs for generating source gen mappings. + /// + public partial class ModelInfo + { + private static readonly FhirRelease release = FhirReleaseParser.Parse(Version); + private static readonly Lazy cachedModelInspector = new(() => new ModelInspector(GenerateAllClassMappings(), GenerateAllEnumMappings()) { FhirRelease = release, FhirVersion = Version }); + + public static ModelInspector CachedModelInspector => cachedModelInspector.Value; + + /// + /// Only used for benchmark verification. + /// + /// + [GenerateAllFhirTypes] + public static partial Type[] GenerateAllFhirTypes(); + + /// + /// Only used for benchmark verification. + /// + /// + [GenerateAllFhirTypes] + public static partial EnumMapping[] GenerateAllEnumMappings(); + + /// + /// Only used for benchmark verification. + /// + /// + [GenerateAllFhirTypes] + public static partial ClassMapping[] GenerateAllClassMappings(); + } + + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class GenerateAllFhirTypesAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public GenerateAllFhirTypesAttribute() + { + } + + public Type[] AssembliesContainingTypes { get; set; } = []; + } +} \ No newline at end of file