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

orion-ecs / keen-eye / 20214681870

14 Dec 2025 09:52PM UTC coverage: 92.008% (-0.007%) from 92.015%
20214681870

push

github

tyevco
fix: Add bounds check for bundle field access in BundleGenerator

Prevents IndexOutOfRangeException when generating XML documentation
examples for bundles with no fields. Added conditional check before
accessing info.Fields[0].Name in GenerateGetBundleExtensions method.

Fixes #333

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

1820 of 1965 branches covered (92.62%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 1 file covered. (66.67%)

4 existing lines in 2 files now uncovered.

9992 of 10873 relevant lines covered (91.9%)

1.46 hits per line

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

93.6
/src/KeenEyes.Generators/QueryGenerator.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 efficient query iterators for types marked with [Query].
13
/// </summary>
14
[Generator]
15
public sealed class QueryGenerator : IIncrementalGenerator
16
{
17
    private const string QueryAttribute = "KeenEyes.QueryAttribute";
18
    private const string WithAttribute = "KeenEyes.WithAttribute";
19
    private const string WithoutAttribute = "KeenEyes.WithoutAttribute";
20
    private const string OptionalAttribute = "KeenEyes.OptionalAttribute";
21

22
    /// <inheritdoc />
23
    public void Initialize(IncrementalGeneratorInitializationContext context)
24
    {
25
        var queryProvider = context.SyntaxProvider
1✔
26
            .ForAttributeWithMetadataName(
1✔
27
                QueryAttribute,
1✔
28
                predicate: static (node, _) => node is StructDeclarationSyntax,
1✔
29
                transform: static (ctx, _) => GetQueryInfo(ctx))
1✔
30
            .Where(static info => info is not null);
1✔
31

32
        context.RegisterSourceOutput(queryProvider, static (ctx, info) =>
1✔
33
        {
1✔
34
            if (info is null)
1✔
35
            {
1✔
36
                return;
×
37
            }
1✔
38

1✔
39
            var source = GenerateQueryImplementation(info);
1✔
40
            ctx.AddSource($"{info.FullName}.Query.g.cs", SourceText.From(source, Encoding.UTF8));
1✔
41
        });
1✔
42
    }
1✔
43

44
    private static QueryInfo? GetQueryInfo(GeneratorAttributeSyntaxContext context)
45
    {
46
        if (context.TargetSymbol is not INamedTypeSymbol typeSymbol)
1✔
47
        {
48
            return null;
×
49
        }
50

51
        var fields = new List<QueryFieldInfo>();
1✔
52

53
        foreach (var member in typeSymbol.GetMembers())
1✔
54
        {
55
            if (member is not IFieldSymbol field)
1✔
56
            {
57
                continue;
58
            }
59

60
            if (field.IsStatic || field.IsConst)
1✔
61
            {
62
                continue;
63
            }
64

65
            var fieldType = field.Type;
1✔
66
            var isRef = false;
1✔
67
            var isReadOnly = false;
1✔
68

69
            // Check for ref/ref readonly field types
70
            if (fieldType is IPointerTypeSymbol || field.RefKind != RefKind.None)
1✔
71
            {
72
                isRef = true;
×
73
                isReadOnly = field.RefKind == RefKind.RefReadOnly;
×
74
            }
75

76
            // Get the actual component type
77
            var componentType = fieldType.ToDisplayString();
1✔
78

79
            // Check for filter attributes
80
            var hasWithAttr = field.GetAttributes()
1✔
81
                .Any(a => a.AttributeClass?.ToDisplayString() == WithAttribute);
1✔
82
            var hasWithoutAttr = field.GetAttributes()
1✔
83
                .Any(a => a.AttributeClass?.ToDisplayString() == WithoutAttribute);
1✔
84
            var hasOptionalAttr = field.GetAttributes()
1✔
85
                .Any(a => a.AttributeClass?.ToDisplayString() == OptionalAttribute);
1✔
86

87
            var accessType = hasWithAttr ? QueryAccessType.With
1✔
88
                : hasWithoutAttr ? QueryAccessType.Without
1✔
89
                : hasOptionalAttr ? QueryAccessType.Optional
1✔
90
                : isReadOnly ? QueryAccessType.Read
1✔
91
                : QueryAccessType.Write;
1✔
92

93
            fields.Add(new QueryFieldInfo(
1✔
94
                field.Name,
1✔
95
                componentType,
1✔
96
                accessType,
1✔
97
                isRef));
1✔
98
        }
99

100
        return new QueryInfo(
1✔
101
            typeSymbol.Name,
1✔
102
            typeSymbol.ContainingNamespace.ToDisplayString(),
1✔
103
            typeSymbol.ToDisplayString(),
1✔
104
            fields.ToImmutableArray());
1✔
105
    }
106

107
    private static string GenerateQueryImplementation(QueryInfo info)
108
    {
109
        var sb = new StringBuilder();
1✔
110

111
        sb.AppendLine("// <auto-generated />");
1✔
112
        sb.AppendLine("#nullable enable");
1✔
113
        sb.AppendLine();
1✔
114

115
        if (!string.IsNullOrEmpty(info.Namespace) && info.Namespace != "<global namespace>")
1✔
116
        {
117
            sb.AppendLine($"namespace {info.Namespace};");
1✔
118
            sb.AppendLine();
1✔
119
        }
120

121
        // Generate the query struct partial
122
        sb.AppendLine($"partial struct {info.Name}");
1✔
123
        sb.AppendLine("{");
1✔
124

125
        // Generate static description property
126
        sb.AppendLine("    /// <summary>Gets the query description for matching entities.</summary>");
1✔
127
        sb.AppendLine("    public static global::KeenEyes.QueryDescription CreateDescription()");
1✔
128
        sb.AppendLine("    {");
1✔
129
        sb.AppendLine("        var desc = new global::KeenEyes.QueryDescription();");
1✔
130

131
        foreach (var field in info.Fields)
1✔
132
        {
133
            var methodName = field.AccessType switch
1✔
134
            {
1✔
135
                QueryAccessType.Read => "AddRead",
×
136
                QueryAccessType.Write => "AddWrite",
1✔
137
                QueryAccessType.With => "AddWith",
1✔
138
                QueryAccessType.Without => "AddWithout",
1✔
139
                QueryAccessType.Optional => null, // Optional doesn't add to description
1✔
140
                _ => null
×
141
            };
1✔
142

143
            if (methodName is not null)
1✔
144
            {
145
                sb.AppendLine($"        desc.{methodName}<{field.ComponentType}>();");
1✔
146
            }
147
        }
148

149
        sb.AppendLine("        return desc;");
1✔
150
        sb.AppendLine("    }");
1✔
151

152
        sb.AppendLine("}");
1✔
153

154
        // Generate extension method for World
155
        sb.AppendLine();
1✔
156
        sb.AppendLine("namespace KeenEyes");
1✔
157
        sb.AppendLine("{");
1✔
158
        sb.AppendLine($"    /// <summary>Query extensions for {info.Name}.</summary>");
1✔
159
        sb.AppendLine("    public static partial class QueryExtensions");
1✔
160
        sb.AppendLine("    {");
1✔
161
        sb.AppendLine($"        /// <summary>Creates a query using the {info.Name} definition.</summary>");
1✔
162
        sb.AppendLine($"        public static global::System.Collections.Generic.IEnumerable<global::KeenEyes.Entity> Query(");
1✔
163
        sb.AppendLine($"            this global::KeenEyes.World world,");
1✔
164
        sb.AppendLine($"            {info.FullName} _)");
1✔
165
        sb.AppendLine("        {");
1✔
166
        sb.AppendLine($"            var description = {info.FullName}.CreateDescription();");
1✔
167
        sb.AppendLine("            return world.GetMatchingEntities(description);");
1✔
168
        sb.AppendLine("        }");
1✔
169
        sb.AppendLine("    }");
1✔
170
        sb.AppendLine("}");
1✔
171

172
        return sb.ToString();
1✔
173
    }
174

175
    private enum QueryAccessType
176
    {
177
        Read,
178
        Write,
179
        With,
180
        Without,
181
        Optional
182
    }
183

184
    private sealed record QueryInfo(
1✔
185
        string Name,
1✔
186
        string Namespace,
1✔
187
        string FullName,
1✔
188
        ImmutableArray<QueryFieldInfo> Fields);
1✔
189

190
    private sealed record QueryFieldInfo(
1✔
UNCOV
191
        string Name,
×
192
        string ComponentType,
1✔
193
        QueryAccessType AccessType,
1✔
194
        bool IsRef);
×
195
}
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