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

orion-ecs / keen-eye / 20142704376

11 Dec 2025 06:04PM UTC coverage: 94.878% (+0.1%) from 94.761%
20142704376

push

github

tyevco
feat: Add With(TBundle) extension methods for EntityBuilder

Generate extension methods that allow EntityBuilder to accept bundle
instances directly via With(bundle), enabling cleaner syntax:

  var bundle = new TransformBundle(position, rotation, scale);
  var entity = world.Spawn().With(bundle).Build();

Generated code includes:
- Generic With<TSelf>(TSelf builder, TBundle bundle) for fluent chaining
- Non-generic With(IEntityBuilder builder, TBundle bundle) for interface usage
- Proper XML documentation for all generated methods

Tests verify:
- Method generation for various bundle types
- All bundle components are added correctly
- Both generic and non-generic overloads exist
- XML documentation is present
- Compatibility with existing WithBundleName() methods

Part of #238 - Bundle EntityBuilder Integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Tyler Coles <tyevco@users.noreply.github.com>

1150 of 1223 branches covered (94.03%)

Branch coverage included in aggregate %.

27 of 27 new or added lines in 1 file covered. (100.0%)

8 existing lines in 1 file now uncovered.

6334 of 6665 relevant lines covered (95.03%)

1.29 hits per line

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

98.19
/src/KeenEyes.Generators/BundleGenerator.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 bundle implementations and fluent builder methods for types marked with [Bundle].
13
/// Bundles are compositions of multiple components commonly used together.
14
/// </summary>
15
[Generator]
16
public sealed class BundleGenerator : IIncrementalGenerator
17
{
18
    private const string BundleAttribute = "KeenEyes.BundleAttribute";
19
    private const string IComponentInterface = "KeenEyes.IComponent";
20

21
    /// <inheritdoc />
22
    public void Initialize(IncrementalGeneratorInitializationContext context)
23
    {
24
        // Find all types with [Bundle] attribute (allow both structs and classes for validation)
25
        var bundleProvider = context.SyntaxProvider
1✔
26
            .ForAttributeWithMetadataName(
1✔
27
                BundleAttribute,
1✔
28
                predicate: static (node, _) => node is StructDeclarationSyntax or ClassDeclarationSyntax,
1✔
29
                transform: static (ctx, _) => GetBundleInfo(ctx))
1✔
30
            .Where(static info => info is not null);
1✔
31

32
        // Generate the code
33
        context.RegisterSourceOutput(bundleProvider, static (ctx, bundleInfo) =>
1✔
34
        {
1✔
35
            if (bundleInfo is null)
1✔
36
            {
1✔
37
                return;
×
38
            }
1✔
39

1✔
40
            // Report diagnostics
1✔
41
            foreach (var diag in bundleInfo.Diagnostics)
1✔
42
            {
1✔
43
                ctx.ReportDiagnostic(Diagnostic.Create(
1✔
44
                    diag.Descriptor,
1✔
45
                    diag.Location,
1✔
46
                    diag.MessageArgs));
1✔
47
            }
1✔
48

1✔
49
            // Only generate code for valid bundles
1✔
50
            if (!bundleInfo.IsValid)
1✔
51
            {
1✔
52
                return;
1✔
53
            }
1✔
54

1✔
55
            // Generate bundle partial struct with IBundle implementation and constructor
1✔
56
            var bundleSource = GenerateBundlePartial(bundleInfo);
1✔
57
            ctx.AddSource($"{bundleInfo.FullName}.g.cs", SourceText.From(bundleSource, Encoding.UTF8));
1✔
58
        });
1✔
59

60
        // Generate EntityBuilder extensions for all bundles
61
        var allBundles = bundleProvider.Collect();
1✔
62
        context.RegisterSourceOutput(allBundles, static (ctx, bundles) =>
1✔
63
        {
1✔
64
            // Filter to only valid bundles
1✔
65
            var validBundles = bundles.Where(b => b is not null && b.IsValid).ToImmutableArray();
1✔
66

1✔
67
            if (validBundles.Length == 0)
1✔
68
            {
1✔
69
                return;
1✔
70
            }
1✔
71

1✔
72
            var builderSource = GenerateEntityBuilderExtensions(validBundles!);
1✔
73
            ctx.AddSource("EntityBuilder.Bundles.g.cs", SourceText.From(builderSource, Encoding.UTF8));
1✔
74

1✔
75
            var getBundleSource = GenerateGetBundleExtensions(validBundles!);
1✔
76
            ctx.AddSource("World.GetBundle.g.cs", SourceText.From(getBundleSource, Encoding.UTF8));
1✔
77

1✔
78
            var queryBundleSource = GenerateQueryBundleExtensions(validBundles!);
1✔
79
            ctx.AddSource("World.QueryBundle.g.cs", SourceText.From(queryBundleSource, Encoding.UTF8));
1✔
80
        });
1✔
81

82
        // Generate ref structs for each bundle
83
        context.RegisterSourceOutput(bundleProvider, static (ctx, bundleInfo) =>
1✔
84
        {
1✔
85
            if (bundleInfo is null || !bundleInfo.IsValid)
1✔
86
            {
1✔
87
                return;
1✔
88
            }
1✔
89

1✔
90
            var refStructSource = GenerateBundleRefStruct(bundleInfo);
1✔
91
            ctx.AddSource($"{bundleInfo.FullName}Ref.g.cs", SourceText.From(refStructSource, Encoding.UTF8));
1✔
92
        });
1✔
93
    }
1✔
94

95
    private static BundleInfo? GetBundleInfo(GeneratorAttributeSyntaxContext context)
96
    {
97
        if (context.TargetSymbol is not INamedTypeSymbol typeSymbol)
1✔
98
        {
UNCOV
99
            return null;
×
100
        }
101

102
        var diagnostics = new List<DiagnosticInfo>();
1✔
103

104
        // Validate: must be a struct
105
        if (typeSymbol.TypeKind != TypeKind.Struct)
1✔
106
        {
107
            var location = typeSymbol.Locations.FirstOrDefault();
1✔
108
            if (location is not null)
1✔
109
            {
110
                diagnostics.Add(new DiagnosticInfo(
1✔
111
                    Diagnostics.BundleMustBeStruct,
1✔
112
                    location,
1✔
113
                    [typeSymbol.Name]));
1✔
114
            }
115
            return new BundleInfo(
1✔
116
                typeSymbol.Name,
1✔
117
                typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
118
                typeSymbol.ToDisplayString(),
1✔
119
                ImmutableArray<ComponentFieldInfo>.Empty,
1✔
120
                diagnostics.ToImmutableArray(),
1✔
121
                IsValid: false);
1✔
122
        }
123

124
        var fields = new List<ComponentFieldInfo>();
1✔
125
        var compilation = context.SemanticModel.Compilation;
1✔
126

127
        foreach (var member in typeSymbol.GetMembers())
1✔
128
        {
129
            if (member is not IFieldSymbol field)
1✔
130
            {
131
                continue;
132
            }
133

134
            if (field.IsStatic || field.IsConst)
1✔
135
            {
136
                continue;
137
            }
138

139
            // Check for circular reference first (bundle containing itself)
140
            if (field.Type.ToDisplayString() == typeSymbol.ToDisplayString())
1✔
141
            {
142
                var location = field.Locations.FirstOrDefault();
1✔
143
                if (location is not null)
1✔
144
                {
145
                    diagnostics.Add(new DiagnosticInfo(
1✔
146
                        Diagnostics.BundleCircularReference,
1✔
147
                        location,
1✔
148
                        [typeSymbol.Name, field.Name]));
1✔
149
                }
150
                return new BundleInfo(
1✔
151
                    typeSymbol.Name,
1✔
152
                    typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
153
                    typeSymbol.ToDisplayString(),
1✔
154
                    ImmutableArray<ComponentFieldInfo>.Empty,
1✔
155
                    diagnostics.ToImmutableArray(),
1✔
156
                    IsValid: false);
1✔
157
            }
158

159
            // Validate: field must be a component type
160
            if (!IsComponentType(field.Type, compilation))
1✔
161
            {
162
                var location = field.Locations.FirstOrDefault();
1✔
163
                if (location is not null)
1✔
164
                {
165
                    diagnostics.Add(new DiagnosticInfo(
1✔
166
                        Diagnostics.BundleFieldMustBeComponent,
1✔
167
                        location,
1✔
168
                        [field.Name, typeSymbol.Name, field.Type.ToDisplayString()]));
1✔
169
                }
170
                continue; // Skip invalid field but continue processing others
1✔
171
            }
172

173
            fields.Add(new ComponentFieldInfo(
1✔
174
                field.Name,
1✔
175
                field.Type.ToDisplayString()));
1✔
176
        }
177

178
        // Validate: must have at least one field
179
        if (fields.Count == 0)
1✔
180
        {
181
            var location = typeSymbol.Locations.FirstOrDefault();
1✔
182
            if (location is not null)
1✔
183
            {
184
                diagnostics.Add(new DiagnosticInfo(
1✔
185
                    Diagnostics.BundleMustHaveFields,
1✔
186
                    location,
1✔
187
                    [typeSymbol.Name]));
1✔
188
            }
189
            return new BundleInfo(
1✔
190
                typeSymbol.Name,
1✔
191
                typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
192
                typeSymbol.ToDisplayString(),
1✔
193
                ImmutableArray<ComponentFieldInfo>.Empty,
1✔
194
                diagnostics.ToImmutableArray(),
1✔
195
                IsValid: false);
1✔
196
        }
197

198
        return new BundleInfo(
1✔
199
            typeSymbol.Name,
1✔
200
            typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
201
            typeSymbol.ToDisplayString(),
1✔
202
            fields.ToImmutableArray(),
1✔
203
            diagnostics.ToImmutableArray(),
1✔
204
            IsValid: true);
1✔
205
    }
206

207
    private static bool IsComponentType(ITypeSymbol typeSymbol, Compilation compilation)
208
    {
209
        // Must be a struct
210
        if (typeSymbol.TypeKind != TypeKind.Struct)
1✔
211
        {
UNCOV
212
            return false;
×
213
        }
214

215
        // Check if has [Component] or [TagComponent] attribute (will implement IComponent after generation)
216
        const string componentAttr = "KeenEyes.ComponentAttribute";
217
        const string tagComponentAttr = "KeenEyes.TagComponentAttribute";
218

219
        var hasComponentAttribute = typeSymbol.GetAttributes()
1✔
220
            .Any(a => a.AttributeClass?.ToDisplayString() is componentAttr or tagComponentAttr);
1✔
221

222
        if (hasComponentAttribute)
1✔
223
        {
224
            return true;
1✔
225
        }
226

227
        // Check if implements IComponent interface directly
228
        var iComponentType = compilation.GetTypeByMetadataName(IComponentInterface);
1✔
229
        if (iComponentType is not null &&
1✔
230
            typeSymbol.AllInterfaces.Any(iface =>
1✔
UNCOV
231
                SymbolEqualityComparer.Default.Equals(iface, iComponentType)))
×
232
        {
UNCOV
233
            return true;
×
234
        }
235

236
        return false;
1✔
237
    }
238

239
    private static string GenerateBundlePartial(BundleInfo info)
240
    {
241
        var sb = new StringBuilder();
1✔
242

243
        sb.AppendLine("// <auto-generated />");
1✔
244
        sb.AppendLine("#nullable enable");
1✔
245
        sb.AppendLine();
1✔
246

247
        if (!string.IsNullOrEmpty(info.Namespace) && info.Namespace != "<global namespace>")
1✔
248
        {
249
            sb.AppendLine($"namespace {info.Namespace};");
1✔
250
            sb.AppendLine();
1✔
251
        }
252

253
        // Generate partial struct with IBundle implementation and constructor
254
        sb.AppendLine($"partial struct {info.Name} : global::KeenEyes.IBundle");
1✔
255
        sb.AppendLine("{");
1✔
256

257
        // Generate constructor
258
        sb.AppendLine("    /// <summary>");
1✔
259
        sb.AppendLine($"    /// Initializes a new instance of the <see cref=\"{info.Name}\"/> bundle.");
1✔
260
        sb.AppendLine("    /// </summary>");
1✔
261

262
        foreach (var field in info.Fields)
1✔
263
        {
264
            var paramName = ToCamelCase(field.Name);
1✔
265
            sb.AppendLine($"    /// <param name=\"{paramName}\">The {field.Name} component.</param>");
1✔
266
        }
267

268
        var constructorParams = string.Join(", ", info.Fields.Select(f =>
1✔
269
            $"{f.Type} {ToCamelCase(f.Name)}"));
1✔
270

271
        sb.AppendLine($"    public {info.Name}({constructorParams})");
1✔
272
        sb.AppendLine("    {");
1✔
273

274
        foreach (var field in info.Fields)
1✔
275
        {
276
            var paramName = ToCamelCase(field.Name);
1✔
277
            sb.AppendLine($"        {field.Name} = {paramName};");
1✔
278
        }
279

280
        sb.AppendLine("    }");
1✔
281
        sb.AppendLine("}");
1✔
282

283
        return sb.ToString();
1✔
284
    }
285

286
    private static string GenerateEntityBuilderExtensions(ImmutableArray<BundleInfo?> bundles)
287
    {
288
        var sb = new StringBuilder();
1✔
289

290
        sb.AppendLine("// <auto-generated />");
1✔
291
        sb.AppendLine("#nullable enable");
1✔
292
        sb.AppendLine();
1✔
293
        sb.AppendLine("namespace KeenEyes;");
1✔
294
        sb.AppendLine();
1✔
295
        sb.AppendLine("/// <summary>");
1✔
296
        sb.AppendLine("/// Generated fluent builder methods for bundles.");
1✔
297
        sb.AppendLine("/// </summary>");
1✔
298
        sb.AppendLine("public static partial class EntityBuilderExtensions");
1✔
299
        sb.AppendLine("{");
1✔
300

301
        foreach (var info in bundles)
1✔
302
        {
303
            if (info is null)
1✔
304
            {
305
                continue;
306
            }
307

308
            // Generate With(TBundle bundle) method - accepts bundle instance
309
            sb.AppendLine($"    /// <summary>");
1✔
310
            sb.AppendLine($"    /// Adds all components from a <see cref=\"{info.FullName}\"/> bundle to the entity.");
1✔
311
            sb.AppendLine($"    /// </summary>");
1✔
312
            sb.AppendLine($"    /// <param name=\"builder\">The entity builder.</param>");
1✔
313
            sb.AppendLine($"    /// <param name=\"bundle\">The bundle containing components to add.</param>");
1✔
314
            sb.AppendLine($"    /// <returns>The builder for method chaining.</returns>");
1✔
315
            sb.AppendLine($"    public static TSelf With<TSelf>(this TSelf builder, {info.FullName} bundle)");
1✔
316
            sb.AppendLine($"        where TSelf : global::KeenEyes.IEntityBuilder<TSelf>");
1✔
317
            sb.AppendLine($"    {{");
1✔
318

319
            // Add each component from the bundle
320
            foreach (var field in info.Fields)
1✔
321
            {
322
                sb.AppendLine($"        builder = builder.With(bundle.{field.Name});");
1✔
323
            }
324

325
            sb.AppendLine($"        return builder;");
1✔
326
            sb.AppendLine($"    }}");
1✔
327
            sb.AppendLine();
1✔
328

329
            // Generate non-generic version for interface usage
330
            sb.AppendLine($"    /// <summary>");
1✔
331
            sb.AppendLine($"    /// Adds all components from a <see cref=\"{info.FullName}\"/> bundle to the entity.");
1✔
332
            sb.AppendLine($"    /// </summary>");
1✔
333
            sb.AppendLine($"    /// <param name=\"builder\">The entity builder.</param>");
1✔
334
            sb.AppendLine($"    /// <param name=\"bundle\">The bundle containing components to add.</param>");
1✔
335
            sb.AppendLine($"    /// <returns>The builder for method chaining.</returns>");
1✔
336
            sb.AppendLine($"    public static global::KeenEyes.IEntityBuilder With(this global::KeenEyes.IEntityBuilder builder, {info.FullName} bundle)");
1✔
337
            sb.AppendLine($"    {{");
1✔
338

339
            foreach (var field in info.Fields)
1✔
340
            {
341
                sb.AppendLine($"        builder = builder.With(bundle.{field.Name});");
1✔
342
            }
343

344
            sb.AppendLine($"        return builder;");
1✔
345
            sb.AppendLine($"    }}");
1✔
346
            sb.AppendLine();
1✔
347

348
            // Generate parameters from bundle fields
349
            var parameters = new List<string>();
1✔
350

351
            foreach (var field in info.Fields)
1✔
352
            {
353
                var paramName = ToCamelCase(field.Name);
1✔
354
                parameters.Add($"{field.Type} {paramName}");
1✔
355
            }
356

357
            var paramList = string.Join(", ", parameters);
1✔
358
            var argList = string.Join(", ", info.Fields.Select(f => ToCamelCase(f.Name)));
1✔
359

360
            sb.AppendLine($"    /// <summary>Adds a <see cref=\"{info.FullName}\"/> bundle to the entity.</summary>");
1✔
361

362
            // Generate generic version for fluent chaining
363
            sb.AppendLine($"    public static TSelf With{info.Name}<TSelf>(this TSelf builder, {paramList})");
1✔
364
            sb.AppendLine($"        where TSelf : global::KeenEyes.IEntityBuilder<TSelf>");
1✔
365
            sb.AppendLine($"    {{");
1✔
366
            sb.AppendLine($"        var bundle = new {info.FullName}({argList});");
1✔
367

368
            // Add each component from the bundle
369
            foreach (var field in info.Fields)
1✔
370
            {
371
                sb.AppendLine($"        builder = builder.With(bundle.{field.Name});");
1✔
372
            }
373

374
            sb.AppendLine($"        return builder;");
1✔
375
            sb.AppendLine($"    }}");
1✔
376
            sb.AppendLine();
1✔
377

378
            // Generate non-generic version for interface usage
379
            sb.AppendLine($"    /// <summary>Adds a <see cref=\"{info.FullName}\"/> bundle to the entity.</summary>");
1✔
380
            sb.AppendLine($"    public static global::KeenEyes.IEntityBuilder With{info.Name}(this global::KeenEyes.IEntityBuilder builder, {paramList})");
1✔
381
            sb.AppendLine($"    {{");
1✔
382
            sb.AppendLine($"        var bundle = new {info.FullName}({argList});");
1✔
383

384
            foreach (var field in info.Fields)
1✔
385
            {
386
                sb.AppendLine($"        builder = builder.With(bundle.{field.Name});");
1✔
387
            }
388

389
            sb.AppendLine($"        return builder;");
1✔
390
            sb.AppendLine($"    }}");
1✔
391
            sb.AppendLine();
1✔
392
        }
393

394
        sb.AppendLine("}");
1✔
395

396
        return sb.ToString();
1✔
397
    }
398

399
    private static string GenerateBundleRefStruct(BundleInfo info)
400
    {
401
        var sb = new StringBuilder();
1✔
402

403
        sb.AppendLine("// <auto-generated />");
1✔
404
        sb.AppendLine("#nullable enable");
1✔
405
        sb.AppendLine();
1✔
406

407
        if (!string.IsNullOrEmpty(info.Namespace) && info.Namespace != "<global namespace>")
1✔
408
        {
409
            sb.AppendLine($"namespace {info.Namespace};");
1✔
410
            sb.AppendLine();
1✔
411
        }
412

413
        // Generate ref struct with refs to all components
414
        sb.AppendLine("/// <summary>");
1✔
415
        sb.AppendLine($"/// Ref struct providing zero-copy access to all components in <see cref=\"{info.Name}\"/>.");
1✔
416
        sb.AppendLine("/// </summary>");
1✔
417
        sb.AppendLine($"public ref struct {info.Name}Ref");
1✔
418
        sb.AppendLine("{");
1✔
419

420
        // Generate ref fields for each component
421
        foreach (var field in info.Fields)
1✔
422
        {
423
            sb.AppendLine($"    /// <summary>Reference to the {field.Name} component.</summary>");
1✔
424
            sb.AppendLine($"    public ref {field.Type} {field.Name};");
1✔
425
            sb.AppendLine();
1✔
426
        }
427

428
        // Generate constructor
429
        sb.AppendLine("    /// <summary>");
1✔
430
        sb.AppendLine($"    /// Initializes a new instance of the <see cref=\"{info.Name}Ref\"/> ref struct.");
1✔
431
        sb.AppendLine("    /// </summary>");
1✔
432
        foreach (var field in info.Fields)
1✔
433
        {
434
            var paramName = ToCamelCase(field.Name);
1✔
435
            sb.AppendLine($"    /// <param name=\"{paramName}\">Reference to the {field.Name} component.</param>");
1✔
436
        }
437

438
        var constructorParams = string.Join(", ", info.Fields.Select(f =>
1✔
439
            $"ref {f.Type} {ToCamelCase(f.Name)}"));
1✔
440

441
        sb.AppendLine($"    public {info.Name}Ref({constructorParams})");
1✔
442
        sb.AppendLine("    {");
1✔
443

444
        foreach (var field in info.Fields)
1✔
445
        {
446
            var paramName = ToCamelCase(field.Name);
1✔
447
            sb.AppendLine($"        {field.Name} = ref {paramName};");
1✔
448
        }
449

450
        sb.AppendLine("    }");
1✔
451
        sb.AppendLine("}");
1✔
452

453
        return sb.ToString();
1✔
454
    }
455

456
    private static string GenerateGetBundleExtensions(ImmutableArray<BundleInfo?> bundles)
457
    {
458
        var sb = new StringBuilder();
1✔
459

460
        sb.AppendLine("// <auto-generated />");
1✔
461
        sb.AppendLine("#nullable enable");
1✔
462
        sb.AppendLine();
1✔
463
        sb.AppendLine("namespace KeenEyes;");
1✔
464
        sb.AppendLine();
1✔
465
        sb.AppendLine("/// <summary>");
1✔
466
        sb.AppendLine("/// Generated GetBundle extension methods for accessing bundle components.");
1✔
467
        sb.AppendLine("/// </summary>");
1✔
468
        sb.AppendLine("public static partial class WorldBundleExtensions");
1✔
469
        sb.AppendLine("{");
1✔
470

471
        foreach (var info in bundles)
1✔
472
        {
473
            if (info is null)
1✔
474
            {
475
                continue;
476
            }
477

478
            // Generate GetBundle extension method as a generic method with type inference
479
            // Note: The type parameter isn't actually used at runtime, it's for type safety
480
            sb.AppendLine($"    /// <summary>");
1✔
481
            sb.AppendLine($"    /// Gets a ref struct with references to all components in the bundle.");
1✔
482
            sb.AppendLine($"    /// </summary>");
1✔
483
            sb.AppendLine($"    /// <param name=\"world\">The world instance.</param>");
1✔
484
            sb.AppendLine($"    /// <param name=\"entity\">The entity to get components from.</param>");
1✔
485
            sb.AppendLine($"    /// <param name=\"bundle\">The bundle type (for type inference, use default).</param>");
1✔
486
            sb.AppendLine($"    /// <returns>A ref struct containing references to all bundle components.</returns>");
1✔
487
            sb.AppendLine($"    /// <exception cref=\"System.InvalidOperationException\">Thrown when the entity is not alive or does not have all required components.</exception>");
1✔
488
            sb.AppendLine($"    /// <example>");
1✔
489
            sb.AppendLine($"    /// <code>");
1✔
490
            sb.AppendLine($"    /// ref var bundle = ref world.GetBundle(entity, default({info.FullName}));");
1✔
491
            sb.AppendLine($"    /// bundle.{info.Fields[0].Name}./* modify component */;");
1✔
492
            sb.AppendLine($"    /// </code>");
1✔
493
            sb.AppendLine($"    /// </example>");
1✔
494
            sb.AppendLine($"    [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]");
1✔
495
            sb.AppendLine($"    public static {info.FullName}Ref GetBundle(this global::KeenEyes.World world, global::KeenEyes.Entity entity, {info.FullName} bundle = default)");
1✔
496
            sb.AppendLine($"    {{");
1✔
497
            sb.AppendLine($"        return new {info.FullName}Ref(");
1✔
498

499
            var refParams = info.Fields.Select(f => $"            ref world.Get<{f.Type}>(entity)");
1✔
500
            sb.AppendLine(string.Join(",\r\n", refParams));
1✔
501

502
            sb.AppendLine($"        );");
1✔
503
            sb.AppendLine($"    }}");
1✔
504
            sb.AppendLine();
1✔
505
        }
506

507
        sb.AppendLine("}");
1✔
508

509
        return sb.ToString();
1✔
510
    }
511

512
    private static string GenerateQueryBundleExtensions(ImmutableArray<BundleInfo?> bundles)
513
    {
514
        var sb = new StringBuilder();
1✔
515

516
        sb.AppendLine("// <auto-generated />");
1✔
517
        sb.AppendLine("#nullable enable");
1✔
518
        sb.AppendLine();
1✔
519
        sb.AppendLine("namespace KeenEyes;");
1✔
520
        sb.AppendLine();
1✔
521
        sb.AppendLine("public sealed partial class World");
1✔
522
        sb.AppendLine("{");
1✔
523

524
        // Generate Query<TBundle>() methods for 1-4 bundle parameters
525
        foreach (var info in bundles)
1✔
526
        {
527
            if (info is null)
1✔
528
            {
529
                continue;
530
            }
531

532
            // Generate Query<TBundle>() that expands to Query<C1, C2, ...>()
533
            sb.AppendLine($"    /// <summary>");
1✔
534
            sb.AppendLine($"    /// Creates a query for entities with all components in <see cref=\"{info.FullName}\"/>.");
1✔
535
            sb.AppendLine($"    /// </summary>");
1✔
536
            sb.AppendLine($"    /// <typeparam name=\"T\">The bundle type.</typeparam>");
1✔
537
            sb.AppendLine($"    /// <example>");
1✔
538
            sb.AppendLine($"    /// <code>");
1✔
539
            sb.AppendLine($"    /// foreach (var entity in world.Query&lt;{info.Name}&gt;())");
1✔
540
            sb.AppendLine($"    /// {{");
1✔
541
            sb.AppendLine($"    ///     // Process entities with all bundle components");
1✔
542
            sb.AppendLine($"    /// }}");
1✔
543
            sb.AppendLine($"    /// </code>");
1✔
544
            sb.AppendLine($"    /// </example>");
1✔
545

546
            var componentTypeParams = string.Join(", ", info.Fields.Select(f => f.Type));
1✔
547
            var queryBuilderType = info.Fields.Length switch
1✔
548
            {
1✔
549
                1 => $"QueryBuilder<{componentTypeParams}>",
1✔
550
                2 => $"QueryBuilder<{componentTypeParams}>",
1✔
551
                3 => $"QueryBuilder<{componentTypeParams}>",
1✔
UNCOV
552
                4 => $"QueryBuilder<{componentTypeParams}>",
×
UNCOV
553
                _ => null
×
554
            };
1✔
555

556
            if (queryBuilderType is null)
1✔
557
            {
558
                // Skip bundles with more than 4 components (not supported by current QueryBuilder)
559
                continue;
560
            }
561

562
            sb.AppendLine($"    public {queryBuilderType} Query<T>() where T : struct, global::KeenEyes.IBundle");
1✔
563
            sb.AppendLine($"    {{");
1✔
564
            sb.AppendLine($"        return Query<{componentTypeParams}>();");
1✔
565
            sb.AppendLine($"    }}");
1✔
566
            sb.AppendLine();
1✔
567
        }
568

569
        sb.AppendLine("}");
1✔
570
        sb.AppendLine();
1✔
571

572
        // Generate With<TBundle>() and Without<TBundle>() extension methods for QueryBuilder
573
        sb.AppendLine("/// <summary>");
1✔
574
        sb.AppendLine("/// Bundle filter extensions for QueryBuilder.");
1✔
575
        sb.AppendLine("/// </summary>");
1✔
576
        sb.AppendLine("public static partial class QueryBuilderBundleExtensions");
1✔
577
        sb.AppendLine("{");
1✔
578

579
        foreach (var info in bundles)
1✔
580
        {
581
            if (info is null)
1✔
582
            {
583
                continue;
584
            }
585

586
            // Generate With<TBundle>() for each QueryBuilder arity (1-4)
587
            for (int arity = 1; arity <= 4; arity++)
1✔
588
            {
589
                var typeParams = string.Join(", ", Enumerable.Range(1, arity).Select(i => $"T{i}"));
1✔
590
                var whereConstraints = string.Join("\r\n        ",
1✔
591
                    Enumerable.Range(1, arity).Select(i => $"where T{i} : struct, global::KeenEyes.IComponent"));
1✔
592

593
                sb.AppendLine($"    /// <summary>");
1✔
594
                sb.AppendLine($"    /// Requires the entity to have all components in the bundle.");
1✔
595
                sb.AppendLine($"    /// </summary>");
1✔
596
                sb.AppendLine($"    /// <typeparam name=\"TBundle\">The bundle type.</typeparam>");
1✔
597
                sb.AppendLine($"    public static QueryBuilder<{typeParams}> With{info.Name}<TBundle, {typeParams}>(this QueryBuilder<{typeParams}> builder)");
1✔
598
                sb.AppendLine($"        where TBundle : struct, global::KeenEyes.IBundle");
1✔
599
                sb.AppendLine($"        {whereConstraints}");
1✔
600
                sb.AppendLine($"    {{");
1✔
601

602
                foreach (var field in info.Fields)
1✔
603
                {
604
                    sb.AppendLine($"        builder = builder.With<{field.Type}>();");
1✔
605
                }
606

607
                sb.AppendLine($"        return builder;");
1✔
608
                sb.AppendLine($"    }}");
1✔
609
                sb.AppendLine();
1✔
610

611
                sb.AppendLine($"    /// <summary>");
1✔
612
                sb.AppendLine($"    /// Excludes entities that have all components in the bundle.");
1✔
613
                sb.AppendLine($"    /// </summary>");
1✔
614
                sb.AppendLine($"    /// <typeparam name=\"TBundle\">The bundle type.</typeparam>");
1✔
615
                sb.AppendLine($"    public static QueryBuilder<{typeParams}> Without{info.Name}<TBundle, {typeParams}>(this QueryBuilder<{typeParams}> builder)");
1✔
616
                sb.AppendLine($"        where TBundle : struct, global::KeenEyes.IBundle");
1✔
617
                sb.AppendLine($"        {whereConstraints}");
1✔
618
                sb.AppendLine($"    {{");
1✔
619

620
                foreach (var field in info.Fields)
1✔
621
                {
622
                    sb.AppendLine($"        builder = builder.Without<{field.Type}>();");
1✔
623
                }
624

625
                sb.AppendLine($"        return builder;");
1✔
626
                sb.AppendLine($"    }}");
1✔
627
                sb.AppendLine();
1✔
628
            }
629
        }
630

631
        sb.AppendLine("}");
1✔
632

633
        return sb.ToString();
1✔
634
    }
635

636
    private static string ToCamelCase(string name)
637
    {
638
        if (string.IsNullOrEmpty(name))
1✔
639
        {
UNCOV
640
            return name;
×
641
        }
642

643
        if (name.Length == 1)
1✔
644
        {
UNCOV
645
            return name.ToLowerInvariant();
×
646
        }
647

648
        return char.ToLowerInvariant(name[0]) + name.Substring(1);
1✔
649
    }
650

651
    private sealed record BundleInfo(
1✔
652
        string Name,
1✔
653
        string Namespace,
1✔
654
        string FullName,
1✔
655
        ImmutableArray<ComponentFieldInfo> Fields,
1✔
656
        ImmutableArray<DiagnosticInfo> Diagnostics,
1✔
657
        bool IsValid);
1✔
658

659
    private sealed record ComponentFieldInfo(
1✔
660
        string Name,
1✔
661
        string Type);
1✔
662

663
    private sealed record DiagnosticInfo(
1✔
664
        DiagnosticDescriptor Descriptor,
1✔
665
        Location Location,
1✔
666
        object[] MessageArgs);
1✔
667
}
668

669
/// <summary>
670
/// Diagnostic descriptors for bundle generation errors.
671
/// </summary>
672
internal static class Diagnostics
673
{
674
    /// <summary>
675
    /// KEEN020: Bundle must be a struct.
676
    /// </summary>
677
    public static readonly DiagnosticDescriptor BundleMustBeStruct = new(
1✔
678
        id: "KEEN020",
1✔
679
        title: "Bundle must be a struct",
1✔
680
        messageFormat: "Bundle '{0}' must be a struct, not a class or other type",
1✔
681
        category: "KeenEyes.Bundle",
1✔
682
        defaultSeverity: DiagnosticSeverity.Error,
1✔
683
        isEnabledByDefault: true,
1✔
684
        description: "Bundles must be value types (structs) to maintain consistency with component semantics.");
1✔
685

686
    /// <summary>
687
    /// KEEN021: Bundle field must be a component type.
688
    /// </summary>
689
    public static readonly DiagnosticDescriptor BundleFieldMustBeComponent = new(
1✔
690
        id: "KEEN021",
1✔
691
        title: "Bundle field must be a component type",
1✔
692
        messageFormat: "Field '{0}' in bundle '{1}' must be a component type (struct implementing IComponent), but is '{2}'",
1✔
693
        category: "KeenEyes.Bundle",
1✔
694
        defaultSeverity: DiagnosticSeverity.Error,
1✔
695
        isEnabledByDefault: true,
1✔
696
        description: "All fields in a bundle must be valid component types. Components must be structs implementing IComponent.");
1✔
697

698
    /// <summary>
699
    /// KEEN022: Bundle must have at least one field.
700
    /// </summary>
701
    public static readonly DiagnosticDescriptor BundleMustHaveFields = new(
1✔
702
        id: "KEEN022",
1✔
703
        title: "Bundle must have at least one component field",
1✔
704
        messageFormat: "Bundle '{0}' must contain at least one component field",
1✔
705
        category: "KeenEyes.Bundle",
1✔
706
        defaultSeverity: DiagnosticSeverity.Error,
1✔
707
        isEnabledByDefault: true,
1✔
708
        description: "A bundle without fields serves no purpose. Define at least one component field in the bundle.");
1✔
709

710
    /// <summary>
711
    /// KEEN023: Circular reference in bundle.
712
    /// </summary>
713
    public static readonly DiagnosticDescriptor BundleCircularReference = new(
1✔
714
        id: "KEEN023",
1✔
715
        title: "Circular reference in bundle",
1✔
716
        messageFormat: "Bundle '{0}' cannot contain a field '{1}' of its own type (circular reference)",
1✔
717
        category: "KeenEyes.Bundle",
1✔
718
        defaultSeverity: DiagnosticSeverity.Error,
1✔
719
        isEnabledByDefault: true,
1✔
720
        description: "A bundle cannot contain itself as a field. This would create an infinite recursion.");
1✔
721
}
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