From 1fdc20708ff2a59091b786f2888f271a67756a80 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 20 Sep 2023 14:51:56 +0200 Subject: [PATCH] Allow customizing json ignore attribute for serialized classes (#3451) Co-authored-by: Ferdinando Papale <4850119+papafe@users.noreply.github.com> --- CHANGELOG.md | 7 +++- .../Realm.SourceGenerator/ClassCodeBuilder.cs | 33 +++++++++++----- Realm/Realm.SourceGenerator/CodeEmitter.cs | 11 +++--- Realm/Realm.SourceGenerator/Diagnostics.cs | 10 +++++ .../DiagnosticsEmitter.cs | 24 ++++++++---- .../Realm.SourceGenerator/GeneratorConfig.cs | 10 +++-- Realm/Realm.SourceGenerator/Parser.cs | 6 +-- Realm/Realm.SourceGenerator/RealmGenerator.cs | 4 +- .../DatabaseTypes/RealmCollectionBase.cs | 30 ++++++++++++--- Realm/Realm/DatabaseTypes/RealmObjectBase.cs | 38 +++++++++++++++---- .../.globalconfig | 3 +- 11 files changed, 128 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73e876cda..a4a21ffa01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ ## vNext (TBD) ### Enhancements -* None +* Added support for customizing the ignore attribute applied on certain generated properties of Realm models. The configuration option is called `realm.custom_ignore_attribute` and can be set in a global configuration file (more information about global configuration files can be found in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files)). The Realm generator will treat this as an opaque string, that will be appended to the `IgnoreDataMember` and `XmlIgnore` attributes already applied on these members. The attributes must be fully qualified unless the namespace they reside in is added to a global usings file. For example, this is how you would add `JsonIgnore` from `System.Text.Json`: + + ``` + realm.custom_ignore_attribute = [System.Text.Json.Serialization.JsonIgnore] + ``` + (Issue [#2579](https://github.com/realm/realm-dotnet/issues/2579)) ### Fixed * None diff --git a/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs b/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs index 68d2f6c36f..51d4d84ffc 100644 --- a/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs +++ b/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs @@ -43,16 +43,29 @@ internal class ClassCodeBuilder }; private readonly ClassInfo _classInfo; + private readonly Lazy _ignoreFieldAttribute; private readonly string _helperClassName; private readonly string _accessorInterfaceName; private readonly string _managedAccessorClassName; private readonly string _unmanagedAccessorClassName; - public ClassCodeBuilder(ClassInfo classInfo) + public ClassCodeBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig) { _classInfo = classInfo; + _ignoreFieldAttribute = new(() => + { + var result = "[IgnoreDataMember, XmlIgnore]"; + var customAttribute = generatorConfig.CustomIgnoreAttribute; + if (!string.IsNullOrEmpty(customAttribute)) + { + result += customAttribute; + } + + return result; + }); + var className = _classInfo.Name; _helperClassName = $"{className}ObjectHelper"; @@ -287,36 +300,36 @@ private string GeneratePartialClass(string interfaceString, string managedAccess private {_accessorInterfaceName} Accessor => _accessor ??= new {_unmanagedAccessorClassName}(typeof({_classInfo.Name})); /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public bool IsManaged => Accessor.IsManaged; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public bool IsValid => Accessor.IsValid; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public bool IsFrozen => Accessor.IsFrozen; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public Realms.Realm? Realm => Accessor.Realm; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; /// -[IgnoreDataMember, XmlIgnore] +{_ignoreFieldAttribute.Value} public int BacklinksCount => Accessor.BacklinksCount; {(_classInfo.ObjectType != ObjectType.EmbeddedObject ? string.Empty : -@"/// -[IgnoreDataMember, XmlIgnore] +$@"/// +{_ignoreFieldAttribute.Value} public Realms.IRealmObjectBase? Parent => Accessor.GetParent();")} void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) diff --git a/Realm/Realm.SourceGenerator/CodeEmitter.cs b/Realm/Realm.SourceGenerator/CodeEmitter.cs index 92d26c39d4..218a15a9a4 100644 --- a/Realm/Realm.SourceGenerator/CodeEmitter.cs +++ b/Realm/Realm.SourceGenerator/CodeEmitter.cs @@ -28,10 +28,12 @@ namespace Realms.SourceGenerator internal class CodeEmitter { private readonly GeneratorExecutionContext _context; + private readonly GeneratorConfig _generatorConfig; - public CodeEmitter(GeneratorExecutionContext context) + public CodeEmitter(GeneratorExecutionContext context, GeneratorConfig generatorConfig) { _context = context; + _generatorConfig = generatorConfig; } public void Emit(ParsingResults parsingResults) @@ -45,7 +47,7 @@ public void Emit(ParsingResults parsingResults) try { - var generatedSource = new ClassCodeBuilder(classInfo).GenerateSource(); + var generatedSource = new ClassCodeBuilder(classInfo, _generatorConfig).GenerateSource(); // Replace all occurrences of at least 3 newlines with only 2 var formattedSource = Regex.Replace(generatedSource, @$"[{Environment.NewLine}]{{3,}}", $"{Environment.NewLine}{Environment.NewLine}"); @@ -65,9 +67,6 @@ public void Emit(ParsingResults parsingResults) } } - private static bool ShouldEmit(ClassInfo classInfo) - { - return !classInfo.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error); - } + private static bool ShouldEmit(ClassInfo classInfo) => classInfo.Diagnostics.All(d => d.Severity != DiagnosticSeverity.Error); } } diff --git a/Realm/Realm.SourceGenerator/Diagnostics.cs b/Realm/Realm.SourceGenerator/Diagnostics.cs index 986ced9c9e..3695895d49 100644 --- a/Realm/Realm.SourceGenerator/Diagnostics.cs +++ b/Realm/Realm.SourceGenerator/Diagnostics.cs @@ -51,10 +51,20 @@ private enum Id RealmObjectWithoutAutomaticProperty = 25, ParentOfNestedClassIsNotPartial = 27, IndexedPrimaryKey = 28, + InvalidGeneratorConfiguration = 1000, } #region Errors + public static Diagnostic InvalidConfiguration(string field, string description) + { + return CreateDiagnosticError( + Id.InvalidGeneratorConfiguration, + "Invalid source generator configuration", + $"The generator configuration for {field} is invalid: {description}", + Location.None); + } + public static Diagnostic UnexpectedError(string className, string message, string stackTrace) { return CreateDiagnosticError( diff --git a/Realm/Realm.SourceGenerator/DiagnosticsEmitter.cs b/Realm/Realm.SourceGenerator/DiagnosticsEmitter.cs index f993604e08..4b73f92e4a 100644 --- a/Realm/Realm.SourceGenerator/DiagnosticsEmitter.cs +++ b/Realm/Realm.SourceGenerator/DiagnosticsEmitter.cs @@ -24,22 +24,30 @@ namespace Realms.SourceGenerator { internal class DiagnosticsEmitter { - private GeneratorExecutionContext _context; + private readonly GeneratorExecutionContext _context; - public DiagnosticsEmitter(GeneratorExecutionContext context) + public DiagnosticsEmitter(GeneratorExecutionContext context, GeneratorConfig generatorConfig) { _context = context; - } - public void Emit(ParsingResults parsingResults) - { - foreach (var classInfo in parsingResults.ClassInfo) + var customIgnoreAttribute = generatorConfig.CustomIgnoreAttribute; + if (!string.IsNullOrEmpty(customIgnoreAttribute)) { - if (!classInfo.Diagnostics.Any()) + if (!customIgnoreAttribute!.StartsWith("[") || !customIgnoreAttribute.EndsWith("]")) { - continue; + _context.ReportDiagnostic(Diagnostics.InvalidConfiguration( + field: "realm.custom_ignore_attribute", + description: $"The attribute(s) string should start with '[' and end with ']'. Actual value: {customIgnoreAttribute}.")); + + generatorConfig.CustomIgnoreAttribute = null; } + } + } + public void Emit(ParsingResults parsingResults) + { + foreach (var classInfo in parsingResults.ClassInfo.Where(classInfo => classInfo.Diagnostics.Any())) + { try { SerializeDiagnostics(_context, classInfo); diff --git a/Realm/Realm.SourceGenerator/GeneratorConfig.cs b/Realm/Realm.SourceGenerator/GeneratorConfig.cs index 4aa6c4794d..b3da75e51c 100644 --- a/Realm/Realm.SourceGenerator/GeneratorConfig.cs +++ b/Realm/Realm.SourceGenerator/GeneratorConfig.cs @@ -20,18 +20,20 @@ namespace Realms.SourceGenerator { - internal class GeneratorConfig + internal record GeneratorConfig(bool IgnoreObjectsNullability) { - public bool IgnoreObjectsNullability { get; private set; } + public string? CustomIgnoreAttribute { get; set; } public static GeneratorConfig ParseConfig(AnalyzerConfigOptions analyzerConfigOptions) { analyzerConfigOptions.TryGetValue("realm.ignore_objects_nullability", out var ignoreObjectsNullabilityString); var ignoreObjectsNullability = ignoreObjectsNullabilityString == "true"; - return new GeneratorConfig + analyzerConfigOptions.TryGetValue("realm.custom_ignore_attribute", out var customIgnoreAttribute); + + return new GeneratorConfig(ignoreObjectsNullability) { - IgnoreObjectsNullability = ignoreObjectsNullability + CustomIgnoreAttribute = customIgnoreAttribute }; } } diff --git a/Realm/Realm.SourceGenerator/Parser.cs b/Realm/Realm.SourceGenerator/Parser.cs index 30777aff69..b419854a9f 100644 --- a/Realm/Realm.SourceGenerator/Parser.cs +++ b/Realm/Realm.SourceGenerator/Parser.cs @@ -288,11 +288,11 @@ private PropertyTypeInfo GetPropertyTypeInfo(ClassInfo classInfo, PropertyInfo p return propertyTypeInfo; } - if (propertySymbol is INamedTypeSymbol { SpecialType: SpecialType.System_DateTime }) + if (typeSymbol is INamedTypeSymbol { SpecialType: SpecialType.System_DateTime }) { classInfo.Diagnostics.Add(Diagnostics.DateTimeNotSupported(classInfo.Name, propertySymbol.Name, propertyLocation)); } - else if (propertySymbol.Type.Name == "List") + else if (typeSymbol.Name == "List") { classInfo.Diagnostics.Add(Diagnostics.ListWithoutInterface(classInfo.Name, propertySymbol.Name, propertyLocation)); } @@ -434,7 +434,7 @@ INamedTypeSymbol when typeSymbol.IsValidIntegerType() => PropertyTypeInfo.Int, INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_Double => PropertyTypeInfo.Double, INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_String => PropertyTypeInfo.String, INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_Decimal || typeSymbol.Name == "Decimal128" => PropertyTypeInfo.Decimal, - ITypeSymbol when typeSymbol.ToDisplayString() == "byte[]" => PropertyTypeInfo.Data, + _ when typeSymbol.ToDisplayString() == "byte[]" => PropertyTypeInfo.Data, INamedTypeSymbol when typeSymbol.Name == "ObjectId" => PropertyTypeInfo.ObjectId, INamedTypeSymbol when typeSymbol.Name == "Guid" => PropertyTypeInfo.Guid, INamedTypeSymbol when typeSymbol.Name == "DateTimeOffset" => PropertyTypeInfo.Date, diff --git a/Realm/Realm.SourceGenerator/RealmGenerator.cs b/Realm/Realm.SourceGenerator/RealmGenerator.cs index 62796a39ec..9f4d026de9 100644 --- a/Realm/Realm.SourceGenerator/RealmGenerator.cs +++ b/Realm/Realm.SourceGenerator/RealmGenerator.cs @@ -39,10 +39,10 @@ public void Execute(GeneratorExecutionContext context) var parser = new Parser(context, generatorConfig); var parsingResults = parser.Parse(scr.RealmClasses); - var diagnosticsEmitter = new DiagnosticsEmitter(context); + var diagnosticsEmitter = new DiagnosticsEmitter(context, generatorConfig); diagnosticsEmitter.Emit(parsingResults); - var codeEmitter = new CodeEmitter(context); + var codeEmitter = new CodeEmitter(context, generatorConfig); codeEmitter.Emit(parsingResults); } } diff --git a/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs b/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs index 30755fb319..e8c1c09666 100644 --- a/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs +++ b/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs @@ -93,27 +93,45 @@ public event PropertyChangedEventHandler? PropertyChanged } } - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public int Count { get => IsValid ? Handle.Value.Count() : 0; } - [IgnoreDataMember, XmlIgnore] // XmlIgnore seems to be needed here as IgnoreDataMember is not sufficient for XmlSerializer. + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public ObjectSchema? ObjectSchema => Metadata?.Schema; Metadata? IMetadataObject.Metadata => Metadata; - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsManaged => Realm != null; - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsValid => Handle.Value.IsValid; - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsFrozen => Realm?.IsFrozen == true; - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public Realm Realm { get; } IThreadConfinedHandle IThreadConfined.Handle => Handle.Value; diff --git a/Realm/Realm/DatabaseTypes/RealmObjectBase.cs b/Realm/Realm/DatabaseTypes/RealmObjectBase.cs index ea2b1f9602..674b4bdb5f 100644 --- a/Realm/Realm/DatabaseTypes/RealmObjectBase.cs +++ b/Realm/Realm/DatabaseTypes/RealmObjectBase.cs @@ -78,6 +78,9 @@ public event PropertyChangedEventHandler? PropertyChanged /// Gets the accessor that encapsulates the methods and properties used by the object for its functioning. /// [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif IRealmAccessor IRealmObjectBase.Accessor => _accessor; /// @@ -85,14 +88,20 @@ public event PropertyChangedEventHandler? PropertyChanged /// . /// /// true if object belongs to a Realm; false if standalone. - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsManaged => _accessor.IsManaged; /// /// Gets an object encompassing the dynamic API for this RealmObjectBase instance. /// /// A instance that wraps this RealmObject. - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public DynamicObjectApi DynamicApi => _accessor.DynamicApi; /// @@ -102,7 +111,10 @@ public event PropertyChangedEventHandler? PropertyChanged /// Unmanaged objects are always considered valid. /// /// true if managed and part of the Realm or unmanaged; false if managed but deleted. - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsValid => _accessor.IsValid; /// @@ -112,21 +124,30 @@ public event PropertyChangedEventHandler? PropertyChanged /// /// true if the object is frozen and immutable; false otherwise. /// - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public bool IsFrozen => _accessor.IsFrozen; /// /// Gets the instance this object belongs to, or null if it is unmanaged. /// /// The instance this object belongs to. - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public Realm? Realm => _accessor.Realm; /// /// Gets the instance that describes how the this object belongs to sees it. /// /// A collection of properties describing the underlying schema of this object. - [IgnoreDataMember, XmlIgnore] // XmlIgnore seems to be needed here as IgnoreDataMember is not sufficient for XmlSerializer. + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public ObjectSchema? ObjectSchema => _accessor.ObjectSchema; /// @@ -136,7 +157,10 @@ public event PropertyChangedEventHandler? PropertyChanged /// This property is not observable so the event will not fire when its value changes. /// /// The number of objects referring to this one. - [IgnoreDataMember] + [IgnoreDataMember, XmlIgnore] +#if NET6_0_OR_GREATER + [System.Text.Json.Serialization.JsonIgnore] +#endif public int BacklinksCount => _accessor.BacklinksCount; internal RealmObjectBase() diff --git a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/.globalconfig b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/.globalconfig index 7177b99637..1193c5fe0c 100644 --- a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/.globalconfig +++ b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/.globalconfig @@ -2,4 +2,5 @@ is_global = true # This document can be used to experiment with the source generator configurations -# realm.ignore_objects_nullability = true \ No newline at end of file +# realm.ignore_objects_nullability = true +# realm.custom_ignore_attribute = [System.Text.Json.Serialization.JsonIgnore] \ No newline at end of file