//
// 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();
}
}