• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20146244779

11 Dec 2025 08:19PM UTC coverage: 94.235% (-0.1%) from 94.334%
20146244779

push

github

tyevco
Remove reflection from bundle archetype pre-allocation for AOT compatibility

- Add static abstract Type[] ComponentTypes to IBundle interface
- Update BundleGenerator to implement static abstract member
- Replace reflection-based field access in PreallocateArchetypeFor<TBundle>
  with direct static interface member access
- Add comprehensive "No Reflection in Production Code" guidelines to CLAUDE.md
  documenting prohibited patterns and AOT-compatible alternatives

1219 of 1300 branches covered (93.77%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 2 files covered. (77.78%)

36 existing lines in 8 files now uncovered.

6644 of 7044 relevant lines covered (94.32%)

1.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.07
/src/KeenEyes.Generators/SerializationGenerator.cs
1
using System.Collections.Generic;
2
using System.Collections.Immutable;
3
using System.Linq;
4
using System.Text;
5
using Microsoft.CodeAnalysis;
6
using Microsoft.CodeAnalysis.CSharp.Syntax;
7
using Microsoft.CodeAnalysis.Text;
8

9
namespace KeenEyes.Generators;
10

11
/// <summary>
12
/// Generates AOT-compatible serialization code for components marked with [Component(Serializable = true)].
13
/// Eliminates runtime reflection by generating strongly-typed serialization methods.
14
/// </summary>
15
[Generator]
16
public sealed class SerializationGenerator : IIncrementalGenerator
17
{
18
    private const string ComponentAttribute = "KeenEyes.ComponentAttribute";
19

20
    /// <inheritdoc />
21
    public void Initialize(IncrementalGeneratorInitializationContext context)
22
    {
23
        // Find all structs with [Component(Serializable = true)]
24
        var serializableComponents = context.SyntaxProvider
1✔
25
            .ForAttributeWithMetadataName(
1✔
26
                ComponentAttribute,
1✔
27
                predicate: static (node, _) => node is StructDeclarationSyntax,
1✔
28
                transform: static (ctx, _) => GetSerializableComponentInfo(ctx))
1✔
29
            .Where(static info => info is not null);
1✔
30

31
        var collected = serializableComponents.Collect();
1✔
32

33
        // Generate the serialization registry only if there are serializable components
34
        context.RegisterSourceOutput(collected, static (ctx, components) =>
1✔
35
        {
1✔
36
            var validComponents = components
1✔
37
                .Where(c => c is not null)
1✔
38
                .Select(c => c!)
1✔
39
                .ToImmutableArray();
1✔
40

1✔
41
            // Don't generate anything if there are no serializable components
1✔
42
            // This avoids conflicts when multiple projects reference KeenEyes.Core
1✔
43
            if (validComponents.Length == 0)
1✔
44
            {
1✔
45
                return;
1✔
46
            }
1✔
47

1✔
48
            var source = GenerateSerializationRegistry(validComponents);
1✔
49
            ctx.AddSource("ComponentSerializer.g.cs", SourceText.From(source, Encoding.UTF8));
1✔
50
        });
1✔
51
    }
1✔
52

53
    private static SerializableComponentInfo? GetSerializableComponentInfo(GeneratorAttributeSyntaxContext context)
54
    {
55
        // TargetSymbol is guaranteed to be INamedTypeSymbol because we filter for StructDeclarationSyntax
56
        var typeSymbol = (INamedTypeSymbol)context.TargetSymbol;
1✔
57

58
        // Check for Serializable = true in attribute
59
        // Attributes is guaranteed non-empty because we use ForAttributeWithMetadataName
60
        var attr = context.Attributes.First();
1✔
61

62
        var serializableArg = attr.NamedArguments
1✔
63
            .FirstOrDefault(a => a.Key == "Serializable");
1✔
64

65
        if (serializableArg.Value.Value is not true)
1✔
66
        {
67
            return null;
1✔
68
        }
69

70
        // Collect fields for serialization
71
        var fields = new List<SerializableFieldInfo>();
1✔
72
        foreach (var member in typeSymbol.GetMembers())
1✔
73
        {
74
            if (member is not IFieldSymbol field)
1✔
75
            {
76
                continue;
77
            }
78

79
            if (field.IsStatic || field.IsConst)
1✔
80
            {
81
                continue;
82
            }
83

84
            fields.Add(new SerializableFieldInfo(
1✔
85
                field.Name,
1✔
86
                field.Type.ToDisplayString(),
1✔
87
                GetJsonTypeName(field.Type)));
1✔
88
        }
89

90
        return new SerializableComponentInfo(
1✔
91
            typeSymbol.Name,
1✔
92
            typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
93
            typeSymbol.ToDisplayString(),
1✔
94
            fields.ToImmutableArray());
1✔
95
    }
96

97
    private static string GetJsonTypeName(ITypeSymbol type)
98
    {
99
        // Map CLR types to JSON reader method names
100
        var displayString = type.ToDisplayString();
1✔
101
        return displayString switch
1✔
102
        {
1✔
103
            "int" or "System.Int32" => "Int32",
1✔
104
            "long" or "System.Int64" => "Int64",
1✔
105
            "short" or "System.Int16" => "Int16",
×
106
            "byte" or "System.Byte" => "Byte",
×
107
            "uint" or "System.UInt32" => "UInt32",
×
108
            "ulong" or "System.UInt64" => "UInt64",
×
109
            "ushort" or "System.UInt16" => "UInt16",
×
110
            "sbyte" or "System.SByte" => "SByte",
×
111
            "float" or "System.Single" => "Single",
1✔
112
            "double" or "System.Double" => "Double",
1✔
113
            "decimal" or "System.Decimal" => "Decimal",
1✔
114
            "bool" or "System.Boolean" => "Boolean",
1✔
115
            "string" or "System.String" => "String",
1✔
116
            _ => "Object" // For complex types, use generic deserialization
1✔
117
        };
1✔
118
    }
119

120
    private static string GenerateSerializationRegistry(ImmutableArray<SerializableComponentInfo> components)
121
    {
122
        var sb = new StringBuilder();
1✔
123

124
        sb.AppendLine("// <auto-generated />");
1✔
125
        sb.AppendLine("#nullable enable");
1✔
126
        sb.AppendLine();
1✔
127
        sb.AppendLine("using System;");
1✔
128
        sb.AppendLine("using System.Collections.Generic;");
1✔
129
        sb.AppendLine("using System.Text.Json;");
1✔
130
        sb.AppendLine("using KeenEyes.Serialization;");
1✔
131
        sb.AppendLine();
1✔
132
        sb.AppendLine("namespace KeenEyes.Generated;");
1✔
133
        sb.AppendLine();
1✔
134
        sb.AppendLine("/// <summary>");
1✔
135
        sb.AppendLine("/// Generated registry for AOT-compatible component serialization.");
1✔
136
        sb.AppendLine("/// Contains serialization methods for components marked with [Component(Serializable = true)].");
1✔
137
        sb.AppendLine("/// </summary>");
1✔
138
        sb.AppendLine("public sealed class ComponentSerializer : IComponentSerializer");
1✔
139
        sb.AppendLine("{");
1✔
140
        sb.AppendLine("    /// <summary>");
1✔
141
        sb.AppendLine("    /// Shared instance for convenience.");
1✔
142
        sb.AppendLine("    /// </summary>");
1✔
143
        sb.AppendLine("    public static readonly ComponentSerializer Instance = new();");
1✔
144
        sb.AppendLine();
1✔
145
        sb.AppendLine("    private static readonly Dictionary<string, Func<JsonElement, object>> Deserializers;");
1✔
146
        sb.AppendLine("    private static readonly Dictionary<Type, Func<object, JsonElement>> Serializers;");
1✔
147
        sb.AppendLine("    private static readonly Dictionary<string, Type> TypesByName;");
1✔
148
        sb.AppendLine("    private static readonly HashSet<Type> SerializableTypes;");
1✔
149
        sb.AppendLine("    private static readonly Dictionary<string, Func<World, bool, ComponentInfo>> Registrars;");
1✔
150
        sb.AppendLine("    private static readonly Dictionary<string, Action<World, object>> SingletonSetters;");
1✔
151
        sb.AppendLine();
1✔
152

153
        // Static constructor to initialize dictionaries
154
        sb.AppendLine("    static ComponentSerializer()");
1✔
155
        sb.AppendLine("    {");
1✔
156
        sb.AppendLine("        Deserializers = new Dictionary<string, Func<JsonElement, object>>");
1✔
157
        sb.AppendLine("        {");
1✔
158

159
        foreach (var component in components)
1✔
160
        {
161
            sb.AppendLine($"            [typeof({component.FullName}).AssemblyQualifiedName!] = Deserialize_{component.Name},");
1✔
162
            sb.AppendLine($"            [\"{component.FullName}\"] = Deserialize_{component.Name},");
1✔
163
        }
164

165
        sb.AppendLine("        };");
1✔
166
        sb.AppendLine();
1✔
167
        sb.AppendLine("        Serializers = new Dictionary<Type, Func<object, JsonElement>>");
1✔
168
        sb.AppendLine("        {");
1✔
169

170
        foreach (var component in components)
1✔
171
        {
172
            sb.AppendLine($"            [typeof({component.FullName})] = value => Serialize_{component.Name}(({component.FullName})value),");
1✔
173
        }
174

175
        sb.AppendLine("        };");
1✔
176
        sb.AppendLine();
1✔
177
        sb.AppendLine("        TypesByName = new Dictionary<string, Type>");
1✔
178
        sb.AppendLine("        {");
1✔
179

180
        foreach (var component in components)
1✔
181
        {
182
            sb.AppendLine($"            [typeof({component.FullName}).AssemblyQualifiedName!] = typeof({component.FullName}),");
1✔
183
            sb.AppendLine($"            [\"{component.FullName}\"] = typeof({component.FullName}),");
1✔
184
        }
185

186
        sb.AppendLine("        };");
1✔
187
        sb.AppendLine();
1✔
188
        sb.AppendLine("        SerializableTypes = new HashSet<Type>");
1✔
189
        sb.AppendLine("        {");
1✔
190

191
        foreach (var component in components)
1✔
192
        {
193
            sb.AppendLine($"            typeof({component.FullName}),");
1✔
194
        }
195

196
        sb.AppendLine("        };");
1✔
197
        sb.AppendLine();
1✔
198
        sb.AppendLine("        Registrars = new Dictionary<string, Func<World, bool, ComponentInfo>>");
1✔
199
        sb.AppendLine("        {");
1✔
200

201
        foreach (var component in components)
1✔
202
        {
203
            sb.AppendLine($"            [typeof({component.FullName}).AssemblyQualifiedName!] = (world, isTag) => world.Components.Register<{component.FullName}>(isTag),");
1✔
204
            sb.AppendLine($"            [\"{component.FullName}\"] = (world, isTag) => world.Components.Register<{component.FullName}>(isTag),");
1✔
205
        }
206

207
        sb.AppendLine("        };");
1✔
208
        sb.AppendLine();
1✔
209
        sb.AppendLine("        SingletonSetters = new Dictionary<string, Action<World, object>>");
1✔
210
        sb.AppendLine("        {");
1✔
211

212
        foreach (var component in components)
1✔
213
        {
214
            sb.AppendLine($"            [typeof({component.FullName}).AssemblyQualifiedName!] = (world, value) => world.SetSingleton(({component.FullName})value),");
1✔
215
            sb.AppendLine($"            [\"{component.FullName}\"] = (world, value) => world.SetSingleton(({component.FullName})value),");
1✔
216
        }
217

218
        sb.AppendLine("        };");
1✔
219
        sb.AppendLine("    }");
1✔
220
        sb.AppendLine();
1✔
221

222
        // Public API methods implementing IComponentSerializer
223
        sb.AppendLine("    /// <inheritdoc />");
1✔
224
        sb.AppendLine("    public bool IsSerializable(Type type) => SerializableTypes.Contains(type);");
1✔
225
        sb.AppendLine();
1✔
226
        sb.AppendLine("    /// <inheritdoc />");
1✔
227
        sb.AppendLine("    public bool IsSerializable(string typeName) => Deserializers.ContainsKey(typeName);");
1✔
228
        sb.AppendLine();
1✔
229
        sb.AppendLine("    /// <inheritdoc />");
1✔
230
        sb.AppendLine("    public object? Deserialize(string typeName, JsonElement json)");
1✔
231
        sb.AppendLine("    {");
1✔
232
        sb.AppendLine("        return Deserializers.TryGetValue(typeName, out var deserializer)");
1✔
233
        sb.AppendLine("            ? deserializer(json)");
1✔
234
        sb.AppendLine("            : null;");
1✔
235
        sb.AppendLine("    }");
1✔
236
        sb.AppendLine();
1✔
237
        sb.AppendLine("    /// <inheritdoc />");
1✔
238
        sb.AppendLine("    public JsonElement? Serialize(Type type, object value)");
1✔
239
        sb.AppendLine("    {");
1✔
240
        sb.AppendLine("        if (!Serializers.TryGetValue(type, out var serializer))");
1✔
241
        sb.AppendLine("        {");
1✔
242
        sb.AppendLine("            return null;");
1✔
243
        sb.AppendLine("        }");
1✔
244
        sb.AppendLine("        return serializer(value);");
1✔
245
        sb.AppendLine("    }");
1✔
246
        sb.AppendLine();
1✔
247
        sb.AppendLine("    /// <inheritdoc />");
1✔
248
        sb.AppendLine("    Type? IComponentSerializer.GetType(string typeName)");
1✔
249
        sb.AppendLine("    {");
1✔
250
        sb.AppendLine("        return TypesByName.TryGetValue(typeName, out var type) ? type : null;");
1✔
251
        sb.AppendLine("    }");
1✔
252
        sb.AppendLine();
1✔
253
        sb.AppendLine("    /// <inheritdoc />");
1✔
254
        sb.AppendLine("    public ComponentInfo? RegisterComponent(World world, string typeName, bool isTag)");
1✔
255
        sb.AppendLine("    {");
1✔
256
        sb.AppendLine("        return Registrars.TryGetValue(typeName, out var registrar) ? registrar(world, isTag) : null;");
1✔
257
        sb.AppendLine("    }");
1✔
258
        sb.AppendLine();
1✔
259
        sb.AppendLine("    /// <inheritdoc />");
1✔
260
        sb.AppendLine("    public bool SetSingleton(World world, string typeName, object value)");
1✔
261
        sb.AppendLine("    {");
1✔
262
        sb.AppendLine("        if (SingletonSetters.TryGetValue(typeName, out var setter))");
1✔
263
        sb.AppendLine("        {");
1✔
264
        sb.AppendLine("            setter(world, value);");
1✔
265
        sb.AppendLine("            return true;");
1✔
266
        sb.AppendLine("        }");
1✔
267
        sb.AppendLine("        return false;");
1✔
268
        sb.AppendLine("    }");
1✔
269
        sb.AppendLine();
1✔
270
        sb.AppendLine("    /// <summary>");
1✔
271
        sb.AppendLine("    /// Gets all registered serializable type names.");
1✔
272
        sb.AppendLine("    /// </summary>");
1✔
273
        sb.AppendLine("    public IEnumerable<string> GetSerializableTypeNames() => Deserializers.Keys;");
1✔
274
        sb.AppendLine();
1✔
275
        sb.AppendLine("    /// <summary>");
1✔
276
        sb.AppendLine("    /// Gets all registered serializable types.");
1✔
277
        sb.AppendLine("    /// </summary>");
1✔
278
        sb.AppendLine("    public IEnumerable<Type> GetSerializableTypes() => SerializableTypes;");
1✔
279
        sb.AppendLine();
1✔
280

281
        // Generate individual serialization/deserialization methods
282
        foreach (var component in components)
1✔
283
        {
284
            GenerateComponentMethods(sb, component);
1✔
285
        }
286

287
        sb.AppendLine("}");
1✔
288

289
        return sb.ToString();
1✔
290
    }
291

292
    private static void GenerateComponentMethods(StringBuilder sb, SerializableComponentInfo component)
293
    {
294
        // Deserialize method
295
        sb.AppendLine($"    private static object Deserialize_{component.Name}(JsonElement json)");
1✔
296
        sb.AppendLine("    {");
1✔
297
        sb.AppendLine($"        var result = new {component.FullName}();");
1✔
298

299
        foreach (var field in component.Fields)
1✔
300
        {
301
            var camelFieldName = ToCamelCase(field.Name);
1✔
302
            sb.AppendLine($"        if (json.TryGetProperty(\"{camelFieldName}\", out var {camelFieldName}Elem))");
1✔
303
            sb.AppendLine("        {");
1✔
304

305
            if (field.JsonTypeName == "Object")
1✔
306
            {
307
                // Complex type - use generic deserialization
308
                sb.AppendLine($"            result.{field.Name} = JsonSerializer.Deserialize<{field.Type}>({camelFieldName}Elem.GetRawText())!;");
1✔
309
            }
310
            else if (field.JsonTypeName == "String")
1✔
311
            {
312
                sb.AppendLine($"            result.{field.Name} = {camelFieldName}Elem.GetString()!;");
1✔
313
            }
314
            else
315
            {
316
                sb.AppendLine($"            result.{field.Name} = {camelFieldName}Elem.Get{field.JsonTypeName}();");
1✔
317
            }
318

319
            sb.AppendLine("        }");
1✔
320
        }
321

322
        sb.AppendLine("        return result;");
1✔
323
        sb.AppendLine("    }");
1✔
324
        sb.AppendLine();
1✔
325

326
        // Serialize method
327
        sb.AppendLine($"    private static JsonElement Serialize_{component.Name}({component.FullName} value)");
1✔
328
        sb.AppendLine("    {");
1✔
329
        sb.AppendLine("        using var stream = new System.IO.MemoryStream();");
1✔
330
        sb.AppendLine("        using var writer = new Utf8JsonWriter(stream);");
1✔
331
        sb.AppendLine("        writer.WriteStartObject();");
1✔
332

333
        foreach (var field in component.Fields)
1✔
334
        {
335
            var camelFieldName = ToCamelCase(field.Name);
1✔
336

337
            if (field.JsonTypeName == "Object")
1✔
338
            {
339
                sb.AppendLine($"        writer.WritePropertyName(\"{camelFieldName}\");");
1✔
340
                sb.AppendLine($"        JsonSerializer.Serialize(writer, value.{field.Name});");
1✔
341
            }
342
            else if (field.JsonTypeName == "String")
1✔
343
            {
344
                sb.AppendLine($"        writer.WriteString(\"{camelFieldName}\", value.{field.Name});");
1✔
345
            }
346
            else if (field.JsonTypeName == "Boolean")
1✔
347
            {
348
                sb.AppendLine($"        writer.WriteBoolean(\"{camelFieldName}\", value.{field.Name});");
1✔
349
            }
350
            else
351
            {
352
                sb.AppendLine($"        writer.WriteNumber(\"{camelFieldName}\", value.{field.Name});");
1✔
353
            }
354
        }
355

356
        sb.AppendLine("        writer.WriteEndObject();");
1✔
357
        sb.AppendLine("        writer.Flush();");
1✔
358
        sb.AppendLine("        stream.Position = 0;");
1✔
359
        sb.AppendLine("        return JsonDocument.Parse(stream).RootElement.Clone();");
1✔
360
        sb.AppendLine("    }");
1✔
361
        sb.AppendLine();
1✔
362
    }
1✔
363

364
    private static string ToCamelCase(string name)
365
    {
366
        if (string.IsNullOrEmpty(name))
1✔
367
        {
UNCOV
368
            return name;
×
369
        }
370

371
        if (name.Length == 1)
1✔
372
        {
373
            return name.ToLowerInvariant();
1✔
374
        }
375

376
        return char.ToLowerInvariant(name[0]) + name.Substring(1);
1✔
377
    }
378

379
    private sealed record SerializableComponentInfo(
1✔
380
        string Name,
1✔
UNCOV
381
        string Namespace,
×
382
        string FullName,
1✔
383
        ImmutableArray<SerializableFieldInfo> Fields);
1✔
384

385
    private sealed record SerializableFieldInfo(
1✔
386
        string Name,
1✔
387
        string Type,
1✔
388
        string JsonTypeName);
1✔
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc