diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5ced01b..88c7860 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,8 @@ "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")", - "Bash(timeout 30 dotnet run:*)" + "Bash(timeout 30 dotnet run:*)", + "Bash(dotnet exec vstest:*)" ] } } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs index 4a00c4e..088ec0a 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs @@ -17,15 +17,13 @@ public static partial class AcToonSerializer var underlyingType = Nullable.GetUnderlyingType(type); var isNullable = underlyingType != null || !type.IsValueType; - // Nullable vs Required - if (isNullable) - constraints.Add("nullable"); - else - constraints.Add("required"); + // Nullable/Required: NE írjuk ki automatikusan, mert redundáns (a típusból látszik) + // Csak explicit [Required] attribute esetén írjuk ki (azt az ExtractDataAnnotationConstraints kezeli) + + // Numeric/Boolean: NE írjuk ki, mert redundáns (a típusból látszik) + // Range-eket megtartjuk, mert azok hasznos extra információk var baseType = underlyingType ?? type; - - // Numeric type ranges var typeCode = Type.GetTypeCode(baseType); switch (typeCode) { @@ -41,37 +39,39 @@ public static partial class AcToonSerializer case TypeCode.UInt16: constraints.Add("range: 0-65535"); break; - case TypeCode.Int32: - constraints.Add("numeric"); - break; case TypeCode.UInt32: constraints.Add("range: 0-4294967295"); break; - case TypeCode.Int64: - constraints.Add("numeric"); - break; - case TypeCode.UInt64: - constraints.Add("numeric, non-negative"); - break; - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - constraints.Add("numeric"); - break; - case TypeCode.Boolean: - constraints.Add("boolean: true|false"); - break; + // Int32, Int64, UInt64, Float, Double, Decimal, Boolean: nincs constraint (redundáns lenne) } - // Enum values - if (baseType.IsEnum) + // Enum values: NE írjuk ki a constraint-ben, mert már a type metadata-ban benne van + + return constraints.Count > 0 ? string.Join(", ", constraints) : string.Empty; + } + + /// + /// Detect if property is readonly (no setter or init-only). + /// + private static bool IsReadOnlyProperty(PropertyInfo prop) + { + // No setter at all → readonly + if (!prop.CanWrite) return true; + + // Has setter but it's init-only → readonly after construction + var setMethod = prop.GetSetMethod(nonPublic: true); + if (setMethod != null) { - var values = string.Join("|", Enum.GetNames(baseType).Take(5)); // Limit to 5 values - var more = Enum.GetNames(baseType).Length > 5 ? "..." : ""; - constraints.Add($"enum: {values}{more}"); + // Check for init-only (C# 9+ feature) + var returnParameter = setMethod.ReturnParameter; + var requiredModifiers = returnParameter.GetRequiredCustomModifiers(); + if (requiredModifiers.Any(x => x.FullName == "System.Runtime.CompilerServices.IsExternalInit")) + { + return true; + } } - return string.Join(", ", constraints); + return false; } /// diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs index ad5cdec..75d8348 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Reflection; using static AyCode.Core.Helpers.JsonUtilities; @@ -73,9 +74,17 @@ public static partial class AcToonSerializer context.Write('='); } + // Szűrés: csak komplex típusok (class/struct) és enum-ok, primitívek nélkül + var filteredTypes = typesToDocument + .Where(t => t.IsEnum || !IsPrimitiveOrStringFast(t)) // Enum-ok megtartása, primitívek és string kizárása + .Where(t => !typeof(System.Collections.IEnumerable).IsAssignableFrom(t) || t == typeof(string)) + .Where(t => !t.IsGenericType || t.IsGenericTypeDefinition) + .Where(t => !t.Name.StartsWith("List`", StringComparison.Ordinal) && !t.Name.StartsWith("ICollection`", StringComparison.Ordinal) && !t.Name.StartsWith("IEnumerable`", StringComparison.Ordinal) && !t.Name.StartsWith("Dictionary`", StringComparison.Ordinal)) + .ToList(); + context.Write("["); var first = true; - foreach (var t in typesToDocument) + foreach (var t in filteredTypes) { if (!first) context.Write(", "); context.Write($"\"{t.Name}\""); @@ -96,7 +105,7 @@ public static partial class AcToonSerializer context.WriteLine("@types {"); context.CurrentIndentLevel++; - foreach (var t in typesToDocument) + foreach (var t in filteredTypes) { WriteTypeDefinition(t, context); } @@ -111,10 +120,18 @@ public static partial class AcToonSerializer /// private static void CollectTypes(Type type, HashSet types) { - if (IsPrimitiveOrStringFast(type)) return; - var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + // Enum-ok: hozzáadjuk és return (nem traversáljuk tovább) + if (underlyingType.IsEnum) + { + types.Add(underlyingType); + return; + } + + // Primitívek és string: kihagyjuk + if (IsPrimitiveOrStringFast(underlyingType)) return; + if (!types.Add(underlyingType)) return; // Already processed // Handle dictionaries FIRST (before generic IEnumerable check) @@ -138,12 +155,69 @@ public static partial class AcToonSerializer // Handle object properties (traverse type graph) var metadata = GetTypeMetadata(underlyingType); + + // Detektáljuk az enum backing field-eket (NotMapped enum property-k) + DetectAndCollectEnumBackingFields(underlyingType, types); + foreach (var prop in metadata.Properties) { CollectTypes(prop.PropertyType, types); } } + /// + /// Detektálja az enum backing field-eket és hozzáadja az enum típusokat a types hash set-hez. + /// Logika: Ha van egy enum property (bármilyen) és van egy {PropertyName}Id int property, + /// akkor az enum típust hozzáadjuk. + /// + private static void DetectAndCollectEnumBackingFields(Type type, HashSet types) + { + var allProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var prop in allProperties) + { + // Csak enum property-ket vizsgálunk + if (!prop.PropertyType.IsEnum) continue; + + // Keressük meg a {PropertyName}Id backing field-et + var backingFieldName = $"{prop.Name}Id"; + var backingField = allProperties.FirstOrDefault(p => + p.Name == backingFieldName && + (p.PropertyType == typeof(int) || p.PropertyType == typeof(int?))); + + if (backingField != null) + { + // Megtaláltuk az enum backing field párt → hozzáadjuk az enum típust + types.Add(prop.PropertyType); + } + } + } + + /// + /// Megkeresi a property-hez tartozó enum típust, ha az egy enum backing field. + /// Visszaadja az enum típust, ha van, különben null. + /// + private static Type? FindEnumTypeForBackingField(PropertyInfo backingFieldProperty, Type declaringType) + { + // Csak int/int? típusú property-k lehetnek backing field-ek + if (backingFieldProperty.PropertyType != typeof(int) && backingFieldProperty.PropertyType != typeof(int?)) + return null; + + // Ha a property neve "...Id"-re végződik, keressük meg az enum property-t + if (!backingFieldProperty.Name.EndsWith("Id", StringComparison.Ordinal)) + return null; + + var enumPropertyName = backingFieldProperty.Name.Substring(0, backingFieldProperty.Name.Length - 2); + var allProperties = declaringType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + var enumProperty = allProperties.FirstOrDefault(p => + p.Name == enumPropertyName && + p.PropertyType.IsEnum); + + // Ha van enum property, visszaadjuk a típusát (függetlenül a NotMapped attribútumtól) + return enumProperty?.PropertyType; + } + /// /// Write type definition with property descriptions. /// @@ -162,11 +236,12 @@ public static partial class AcToonSerializer var typeDescription = GetFinalTypeDescription(type, metadata); context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\""); + // Növeljük az indent-et a type body-hoz (type-level metadata ÉS property-k) + context.CurrentIndentLevel++; + // Type-level metadata (enhanced mode only) if (context.Options.UseEnhancedMetadata) { - context.CurrentIndentLevel++; - // Table name var tableName = DetectTableName(type, metadata.CustomDescription); if (!string.IsNullOrEmpty(tableName)) @@ -180,14 +255,15 @@ public static partial class AcToonSerializer { context.WriteIndentedLine($"purpose: \"{typePurpose}\""); } - - context.CurrentIndentLevel--; } - if (metadata.Properties.Length == 0) return; + if (metadata.Properties.Length == 0) + { + context.CurrentIndentLevel--; + return; + } - // Property descriptions - context.CurrentIndentLevel++; + // Property descriptions (már indent szint 1-en vagyunk, property-k is ide kerülnek) foreach (var prop in metadata.Properties) { // Get final values with fallback chain + placeholder resolution @@ -206,7 +282,11 @@ public static partial class AcToonSerializer // Enhanced format with constraints and purpose context.WriteIndentedLine($"{prop.Name}: {typeHint}"); context.CurrentIndentLevel++; - context.WriteIndentedLine($"description: \"{propDescription}\""); + + // Only write description if explicitly provided (not null/empty) + if (!string.IsNullOrEmpty(propDescription)) + context.WriteIndentedLine($"description: \"{propDescription}\""); + if (!string.IsNullOrEmpty(purpose)) context.WriteIndentedLine($"purpose: \"{purpose}\""); if (!string.IsNullOrEmpty(constraints)) @@ -234,10 +314,22 @@ public static partial class AcToonSerializer { // Simple format var relationshipHint = GetRelationshipHint(relationshipMetadata); - var fullDescription = string.IsNullOrEmpty(relationshipHint) - ? propDescription - : $"{propDescription} ({relationshipHint})"; - context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\""); + + // Only write description if there's explicit description OR relationship metadata + if (!string.IsNullOrEmpty(propDescription) || !string.IsNullOrEmpty(relationshipHint)) + { + var fullDescription = string.IsNullOrEmpty(relationshipHint) + ? propDescription + : string.IsNullOrEmpty(propDescription) + ? relationshipHint + : $"{propDescription} ({relationshipHint})"; + context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\""); + } + else + { + // No description and no relationship metadata - just write type hint + context.WriteIndentedLine($"{prop.Name}: {typeHint}"); + } } } context.CurrentIndentLevel--; @@ -366,9 +458,10 @@ public static partial class AcToonSerializer /// /// Get final enum description with fallback chain. - /// Priority: ToonDescription.Description -> Microsoft [Description] -> Smart inference + /// Priority: ToonDescription.Description -> Microsoft [Description] -> null (no fallback) + /// Returns null if no explicit description to avoid redundancy (values already in values: section). /// - private static string GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription) + private static string? GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription) { // 1. ToonDescription.Description if (!string.IsNullOrWhiteSpace(customDescription?.Description)) @@ -383,8 +476,8 @@ public static partial class AcToonSerializer return msDesc.Description; } - // 3. Smart inference - return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}"; + // 3. No fallback - avoid redundancy (values already listed in values: section) + return null; } /// @@ -439,9 +532,10 @@ public static partial class AcToonSerializer if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType)) return $"Count of {propertyName.Replace("Count", "")}"; - // Collection types - if (GetCollectionElementType(propertyType) != null) - return $"Collection of {GetCollectionElementType(propertyType)?.Name ?? "items"} for {declaringType.Name}"; + // Collection types (csak ha tényleg nem object az elem) + var elementType = GetCollectionElementType(propertyType); + if (elementType != null && elementType != typeof(object)) + return $"Collection of {elementType.Name} for {declaringType.Name}"; // Dictionary types if (IsDictionaryType(propertyType, out _, out _)) @@ -522,9 +616,10 @@ public static partial class AcToonSerializer /// /// Get final property description with fallback chain and placeholder resolution. - /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference + /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > null (no fallback) + /// Returns null if no explicit description is provided (to avoid redundant output). /// - private static string GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType) + private static string? GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType) { // 1. ToonDescription.Description (if not empty) var customDesc = prop.CustomDescription?.Description; @@ -545,8 +640,8 @@ public static partial class AcToonSerializer if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description)) return msDesc.Description; - // 3. Smart inference (fallback) - return GetPropertyDescription(declaringType, prop.Name, prop.PropertyType); + // 3. No fallback - return null to avoid redundant output + return null; } /// @@ -579,6 +674,21 @@ public static partial class AcToonSerializer { var customConstraints = prop.CustomDescription?.Constraints; + // --- NotMapped/NotColumn detection --- + var notMapped = false; + var attrs = prop.PropertyInfo.GetCustomAttributes(inherit: true); + foreach (var attr in attrs) + { + var attrName = attr.GetType().Name; + if (attrName == "NotMappedAttribute" || attrName == "NotColumnAttribute") + { + notMapped = true; + break; + } + } + + string result; + if (!string.IsNullOrWhiteSpace(customConstraints)) { // Has [#...] placeholder? → Resolve and merge @@ -592,23 +702,49 @@ public static partial class AcToonSerializer { var typeConstraints = ExtractTypeConstraints(prop.PropertyType); var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name); - - return MergeConstraints(typeConstraints, null, inferredConstraints, resolved); + result = MergeConstraints(typeConstraints, null, inferredConstraints, resolved); + } + else + { + result = string.Empty; } } else { // No placeholder → custom wins (replace mode) - return customConstraints; + result = customConstraints; } } + else + { + // No custom constraint → Fallback chain + var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo); + var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType); + var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name); + result = MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null); + } - // No custom constraint → Fallback chain - var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo); - var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType); - var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name); + // Add readonly constraint if needed + if (IsReadOnlyProperty(prop.PropertyInfo) && !result.Contains("readonly")) + { + result = string.IsNullOrEmpty(result) ? "readonly" : result + ", readonly"; + } - return MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null); + // Add not-mapped constraint if needed (only once, at the end) + if (notMapped && !result.Contains("not-mapped")) + { + result = string.IsNullOrEmpty(result) ? "not-mapped" : result + ", not-mapped"; + } + + // Add enum-type constraint if this is an enum backing field + var enumType = FindEnumTypeForBackingField(prop.PropertyInfo, declaringType); + if (enumType != null && !result.Contains("enum-type:")) + { + var enumConstraint = $"enum-type: {enumType.Name}"; + result = string.IsNullOrEmpty(result) ? enumConstraint : result + ", " + enumConstraint; + } + + return result; } ///