// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.SourceGenerator; using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using MUnique.OpenMU.Annotations; /// /// A generator for the plain and simple objects for the persistence project. /// [Generator] public class BasicModelGenerator : IIncrementalGenerator, IUnboundSourceGenerator { /// /// Holds the Assembly-Name which is the target of this generator. /// internal const string TargetAssemblyName = "MUnique.OpenMU.Persistence"; /// public void Initialize(IncrementalGeneratorInitializationContext context) { var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName); context.RegisterSourceOutput(assemblyNameProvider, (sourceProductionContext, assemblyName) => { if (!(assemblyName?.EndsWith("Persistence") ?? false)) { return; } try { foreach (var (name, source) in this.GenerateSources()) { sourceProductionContext.AddSource(name, SourceText.From(source, Encoding.UTF8)); } } catch (Exception e) { sourceProductionContext.ReportDiagnostic( Diagnostic.Create( new DiagnosticDescriptor( "BASICGEN001", "Source generation failed", $"{e.GetType()}: {e.Message}", "SourceGeneration", DiagnosticSeverity.Error, true), Location.None)); } }); } /// /// Generates the source files. /// /// The created source files. public IEnumerable<(string Name, string Source)> GenerateSources() { foreach (var type in ModelGeneratorHelper.CustomTypes) { var className = type.Name; var fullName = type.FullName; var isCloneable = type.GetCustomAttribute(true) is not null; var classSource = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, className)} namespace MUnique.OpenMU.Persistence.BasicModel; using MUnique.OpenMU.Persistence.Json; /// /// A plain implementation of . /// public partial class {className} : {fullName}, IIdentifiable, IConvertibleTo<{className}> {{ {this.CreateConstructors(type)} {this.CreateIdPropertyIfRequired(type)} {this.CreateNavigationProperties(type)} {(isCloneable ? ModelGeneratorHelper.OverrideClonable(type, className) : null)} /// public override bool Equals(object obj) {{ var baseObject = obj as IIdentifiable; if (baseObject != null) {{ return baseObject.Id == this.Id; }} return base.Equals(obj); }} /// public override int GetHashCode() {{ return this.Id.GetHashCode(); }} /// public {className} Convert() => this; }} "; yield return (className, classSource); } } /// /// Builds the wrapper code for the navigation properties. /// /// The type whose properties should be handled. /// The generated code of the properties. private string CreateNavigationProperties(Type type) { var result = new StringBuilder(); var virtualNavigationProperties = type.GetProperties() .Where(p => p.GetGetMethod() is { IsVirtual: true, IsFinal: false } && !p.PropertyType.IsValueType && !p.PropertyType.IsArray).ToList(); var collectionProperties = virtualNavigationProperties .Where(p => p.PropertyType.IsGenericType && (p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) || p.PropertyType.GetGenericTypeDefinition() == typeof(IList<>)) && !p.PropertyType.GenericTypeArguments[0].IsPrimitive); foreach (var property in collectionProperties) { result.AppendLine(this.BuildCollectionCode(property)); } var navigationProperties = virtualNavigationProperties.Where(p => !p.PropertyType.IsGenericType); foreach (var property in navigationProperties) { result.AppendLine(this.BuildNavigationCode(property)); } return result.ToString(); } /// /// Builds the wrapper code for a simple navigation property. /// /// The handled original property. /// The created code. private string BuildNavigationCode(PropertyInfo property) { var propertyTypeName = property.PropertyType.Name.Split('.').Last(); var propertyType = property.PropertyType; return $@" /// /// Gets the raw object of . /// [System.Text.Json.Serialization.JsonPropertyName(""{property.Name.ToCamelCase()}"")] public {propertyTypeName} Raw{property.Name} {{ get => base.{property.Name} as {propertyTypeName}; {(property.GetSetMethod(true) is { } ? $"set => base.{property.Name} = value;" : null)} }} /// [System.Text.Json.Serialization.JsonIgnore] public override {propertyType.FullName} {property.Name} {{ get => base.{property.Name}; {(property.GetSetMethod(true) is { } ? $"{(property.GetSetMethod() is null ? "protected " : null)}set => base.{property.Name} = value;" : null)} }}"; } /// /// Builds the wrapper code for a collection navigation property. /// /// The handled original property. /// The created code. private string BuildCollectionCode(PropertyInfo property) { var propertyType = property.PropertyType; var persistentClassName = propertyType.GetGenericArguments()[0].Name; var originalClassName = propertyType.GetGenericArguments()[0].FullName; var originalPropertyTypeName = propertyType.GetCSharpFullName(); var propertyTypeName = propertyType.GetCSharpName(); var adapterClass = propertyType.GetGenericTypeDefinition() == typeof(IList<>) ? "ListAdapter" : "CollectionAdapter"; return $@" /// /// Gets the raw collection of . /// [System.Text.Json.Serialization.JsonPropertyName(""{property.Name.ToCamelCase()}"")] public {propertyTypeName} Raw{property.Name} {{ get; }} = new List<{persistentClassName}>(); /// [System.Text.Json.Serialization.JsonIgnore] public override {originalPropertyTypeName} {property.Name} {{ get => base.{property.Name} ??= new {adapterClass}<{originalClassName}, {persistentClassName}>(this.Raw{property.Name}); protected set {{ this.{property.Name}.Clear(); foreach (var item in value) {{ this.{property.Name}.Add(item); }} }} }}"; } /// /// Builds the code for an Id-Property, if the type has none yet. /// /// The handled type. /// The created code. private string CreateIdPropertyIfRequired(Type type) { if (type.GetProperty("Id") is null) { return @"/// /// Gets or sets the identifier of this instance. /// public Guid Id { get; set; }"; } return string.Empty; } /// /// Creates the constructors for the new type, if required. /// /// The inherited type. /// The constructors. private string CreateConstructors(Type type) { var stringBuilder = new StringBuilder(); var className = type.Name; if (type.GetConstructors().Any(c => c.IsPublic && c.GetParameters().Length > 0) && type.GetConstructors().Any(c => c.GetParameters().Length == 0)) { stringBuilder.AppendLine(@$"/// public {className}() {{ }}"); } foreach (var constructor in type.GetConstructors() .Where(c => c.IsPublic && c.GetParameters().Length > 0)) { var parameters = constructor.GetParameters(); stringBuilder.AppendLine(@$" /// public {className}({ModelGeneratorHelper.GetParameterDefinitions(parameters)}) : base({ModelGeneratorHelper.GetParameters(parameters)}) {{ }}"); } return stringBuilder.ToString(); } }