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

loresoft / FluentCommand / 24681616811

20 Apr 2026 05:44PM UTC coverage: 54.554%. First build
24681616811

push

github

pwelter34
Add DataReaderFactory analyzer and refactor generator

1418 of 3381 branches covered (41.94%)

Branch coverage included in aggregate %.

55 of 344 new or added lines in 4 files covered. (15.99%)

4344 of 7181 relevant lines covered (60.49%)

317.64 hits per line

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

0.0
/src/FluentCommand.Generators/DataReaderFactoryGenerator.cs
1
using System.Collections.Immutable;
2

3
using FluentCommand.Generators.Models;
4

5
using Microsoft.CodeAnalysis;
6
using Microsoft.CodeAnalysis.CSharp;
7
using Microsoft.CodeAnalysis.CSharp.Syntax;
8

9
namespace FluentCommand.Generators;
10

11
[Generator(LanguageNames.CSharp)]
12
public sealed class DataReaderFactoryGenerator : IIncrementalGenerator
13
{
14
    public void Initialize(IncrementalGeneratorInitializationContext context)
15
    {
16
        // Pipeline for [GenerateReader(typeof(T))] attribute
NEW
17
        var generateAttributeClasses = context.SyntaxProvider.ForAttributeWithMetadataName(
×
NEW
18
            fullyQualifiedMetadataName: "FluentCommand.Attributes.GenerateReaderAttribute",
×
NEW
19
            predicate: static (_, __) => true,
×
NEW
20
            transform: static (context, _) =>
×
NEW
21
            {
×
NEW
22
                if (context.Attributes.Length == 0)
×
NEW
23
                    return [];
×
NEW
24

×
NEW
25
                var classes = new List<EntityClass>();
×
NEW
26

×
NEW
27
                foreach (var attribute in context.Attributes)
×
NEW
28
                {
×
NEW
29
                    if (attribute == null)
×
NEW
30
                        return [];
×
NEW
31

×
NEW
32
                    if (attribute.ConstructorArguments.Length != 1)
×
NEW
33
                        return [];
×
NEW
34

×
NEW
35
                    var comparerArgument = attribute.ConstructorArguments[0];
×
NEW
36
                    if (comparerArgument.Value is not INamedTypeSymbol targetSymbol)
×
NEW
37
                        return [];
×
NEW
38

×
NEW
39
                    var entityClass = CreateClass(targetSymbol);
×
NEW
40
                    if (entityClass != null)
×
NEW
41
                        classes.Add(entityClass);
×
NEW
42
                }
×
NEW
43

×
NEW
44
                return new EquatableArray<EntityClass>(classes);
×
NEW
45
            }
×
NEW
46
        )
×
NEW
47
        .Where(static context => context.Count > 0)
×
NEW
48
        .SelectMany(static (item, _) => item)
×
NEW
49
        .WithTrackingName("GenerateAttributeGenerator");
×
50

NEW
51
        context.RegisterSourceOutput(generateAttributeClasses, WriteDataReaderSource);
×
NEW
52
        context.RegisterSourceOutput(generateAttributeClasses, WriteTypeAccessorSource);
×
53

54
        // Pipeline for [Table] attribute
NEW
55
        var tableAttributeClasses = context.SyntaxProvider.ForAttributeWithMetadataName(
×
NEW
56
            fullyQualifiedMetadataName: "System.ComponentModel.DataAnnotations.Schema.TableAttribute",
×
NEW
57
            predicate: static (syntaxNode, _) =>
×
NEW
58
            {
×
NEW
59
                return
×
NEW
60
                    (
×
NEW
61
                        syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } classDeclaration
×
NEW
62
                            && !classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword)
×
NEW
63
                            && !classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)
×
NEW
64
                    )
×
NEW
65
                    ||
×
NEW
66
                    (
×
NEW
67
                        syntaxNode is RecordDeclarationSyntax { AttributeLists.Count: > 0 } recordDeclaration
×
NEW
68
                            && !recordDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword)
×
NEW
69
                            && !recordDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)
×
NEW
70
                    );
×
NEW
71
            },
×
NEW
72
            transform: static (context, _) =>
×
NEW
73
            {
×
NEW
74
                if (context.TargetSymbol is not INamedTypeSymbol targetSymbol)
×
NEW
75
                    return null;
×
NEW
76

×
NEW
77
                return CreateClass(targetSymbol);
×
NEW
78
            }
×
NEW
79
        )
×
NEW
80
        .Where(static context => context is not null)
×
NEW
81
        .Select(static (context, _) => context!)
×
NEW
82
        .WithTrackingName("TableAttributeGenerator");
×
83

NEW
84
        context.RegisterSourceOutput(tableAttributeClasses, WriteDataReaderSource);
×
NEW
85
        context.RegisterSourceOutput(tableAttributeClasses, WriteTypeAccessorSource);
×
NEW
86
    }
×
87

88
    private static void WriteDataReaderSource(SourceProductionContext context, EntityClass entityClass)
89
    {
90
        var qualifiedName = entityClass.EntityNamespace is null
×
91
            ? entityClass.EntityName
×
92
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
93

94
        var source = DataReaderFactoryWriter.Generate(entityClass);
×
95

96
        context.AddSource($"{qualifiedName}DataReaderExtensions.g.cs", source);
×
97
    }
×
98

99
    private static void WriteTypeAccessorSource(SourceProductionContext context, EntityClass entityClass)
100
    {
101
        var qualifiedName = entityClass.EntityNamespace is null
×
102
            ? entityClass.EntityName
×
103
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
×
104

105
        var source = TypeAccessorWriter.Generate(entityClass);
×
106

107
        context.AddSource($"{qualifiedName}TypeAccessor.g.cs", source);
×
108
    }
×
109

110

111
    private static EntityClass? CreateClass(INamedTypeSymbol targetSymbol)
112
    {
113
        if (targetSymbol == null)
×
114
            return null;
×
115

116
        var fullyQualified = targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
117
        var classNamespace = targetSymbol.ContainingNamespace.ToDisplayString();
×
118
        var className = targetSymbol.Name;
×
119

120
        // extract table mapping info
121
        var typeAttributes = targetSymbol.GetAttributes();
×
122
        var tableAttribute = FindSchemaAttribute(typeAttributes, "TableAttribute");
×
123

124
        string? tableName = null;
×
125
        string? tableSchema = null;
×
126

127
        if (tableAttribute != null)
×
128
        {
129
            if (tableAttribute.ConstructorArguments.Length > 0 && tableAttribute.ConstructorArguments[0].Value is string name)
×
130
                tableName = name;
×
131

132
            tableSchema = GetNamedString(tableAttribute, "Schema");
×
133
        }
134

135
        var mode = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0)
×
136
            ? InitializationMode.ObjectInitializer
×
137
            : InitializationMode.Constructor;
×
138

139
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
×
140
        var propertySymbols = GetProperties(targetSymbol);
×
141

142
        if (mode == InitializationMode.ObjectInitializer)
×
143
        {
144
            var propertyArray = propertySymbols
×
145
                .Select(p => CreateProperty(p, classIgnored: classIgnored))
×
146
                .ToArray();
×
147

148
            return new EntityClass(
×
149
                InitializationMode: mode,
×
150
                FullyQualified: fullyQualified,
×
151
                EntityNamespace: classNamespace,
×
152
                EntityName: className,
×
153
                Properties: propertyArray,
×
154
                TableName: tableName,
×
155
                TableSchema: tableSchema
×
156
            );
×
157
        }
158

159
        // constructor initialization
160

161
        // constructor with same number of parameters as mappable properties
162
        var mappableCount = propertySymbols
×
163
            .Count(p => !classIgnored.Contains(p.Name) && !HasIgnorePropertyAttribute(p.GetAttributes()));
×
164

165
        var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
×
166
        if (constructor == null)
×
167
            return null;
×
168

169
        var properties = new List<EntityProperty>();
×
170
        foreach (var propertySymbol in propertySymbols)
×
171
        {
172
            // find matching constructor name
173
            var parameter = constructor
×
174
                .Parameters
×
175
                .FirstOrDefault(p => string.Equals(p.Name, propertySymbol.Name, StringComparison.InvariantCultureIgnoreCase));
×
176

177
            if (parameter == null)
×
178
                continue;
179

180
            var property = CreateProperty(propertySymbol, parameter.Name, classIgnored: classIgnored);
×
181
            properties.Add(property);
×
182
        }
183

184
        return new EntityClass(
×
185
            InitializationMode: mode,
×
186
            FullyQualified: fullyQualified,
×
187
            EntityNamespace: classNamespace,
×
188
            EntityName: className,
×
189
            Properties: properties,
×
190
            TableName: tableName,
×
191
            TableSchema: tableSchema
×
192
        );
×
193
    }
194

195
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
196
    {
197
        var properties = new Dictionary<string, IPropertySymbol>();
×
198

199
        var currentSymbol = targetSymbol;
×
200

201
        // get nested properties
202
        while (currentSymbol != null)
×
203
        {
204
            var propertySymbols = currentSymbol
×
205
                .GetMembers()
×
206
                .Where(m => m.Kind == SymbolKind.Property)
×
207
                .OfType<IPropertySymbol>()
×
208
                .Where(IsIncluded)
×
209
                .Where(p => !properties.ContainsKey(p.Name));
×
210

211
            foreach (var propertySymbol in propertySymbols)
×
212
                properties.Add(propertySymbol.Name, propertySymbol);
×
213

214
            currentSymbol = currentSymbol.BaseType;
×
215
        }
216

217
        return [.. properties.Values];
×
218
    }
219

220
    private static EntityProperty CreateProperty(IPropertySymbol propertySymbol, string? parameterName = null, HashSet<string>? classIgnored = null)
221
    {
222
        var propertyType = propertySymbol.Type.ToDisplayString();
×
223
        var memberTypeName = propertySymbol.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
×
224
        var propertyName = propertySymbol.Name;
×
225
        var hasGetter = propertySymbol.GetMethod != null;
×
226
        var hasSetter = propertySymbol.SetMethod?.IsInitOnly == false;
×
227
        var isNotMapped = (classIgnored?.Contains(propertyName) == true) || !IsSupportedType(propertySymbol.Type);
×
228

229
        var attributes = propertySymbol.GetAttributes();
×
230
        if (attributes == default || attributes.Length == 0)
×
231
        {
232
            return new EntityProperty(
×
233
                PropertyName: propertyName,
×
234
                ColumnName: propertyName,
×
235
                PropertyType: propertyType,
×
236
                MemberTypeName: memberTypeName,
×
237
                ParameterName: parameterName,
×
238
                IsNotMapped: isNotMapped,
×
239
                HasGetter: hasGetter,
×
240
                HasSetter: hasSetter
×
241
            );
×
242
        }
243

244
        var columnName = GetColumnName(attributes) ?? propertyName;
×
245
        var converterName = GetConverterName(attributes);
×
246

247
        var isKey = HasDataAnnotationAttribute(attributes, "KeyAttribute");
×
248

249
        isNotMapped = isNotMapped
×
250
            || IsNotMapped(attributes)
×
251
            || HasIgnorePropertyAttribute(attributes);
×
252

253
        var isDatabaseGenerated = GetIsDatabaseGenerated(attributes);
×
254
        var isConcurrencyCheck = HasDataAnnotationAttribute(attributes, "ConcurrencyCheckAttribute");
×
255
        var foreignKey = GetSchemaAttributeConstructorStringArg(attributes, "ForeignKeyAttribute");
×
256
        var isRequired = HasDataAnnotationAttribute(attributes, "RequiredAttribute");
×
257
        var displayName = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayAttribute"), "Name");
×
258
        var dataFormatString = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayFormatAttribute"), "DataFormatString");
×
259
        var columnType = GetNamedString(FindSchemaAttribute(attributes, "ColumnAttribute"), "TypeName");
×
260
        var columnOrder = GetNamedNumber(FindSchemaAttribute(attributes, "ColumnAttribute"), "Order");
×
261

262
        return new EntityProperty(
×
263
            PropertyName: propertyName,
×
264
            ColumnName: columnName,
×
265
            PropertyType: propertyType,
×
266
            MemberTypeName: memberTypeName,
×
267
            ParameterName: parameterName,
×
268
            ConverterName: converterName,
×
269
            IsKey: isKey,
×
270
            IsNotMapped: isNotMapped,
×
271
            IsDatabaseGenerated: isDatabaseGenerated,
×
272
            IsConcurrencyCheck: isConcurrencyCheck,
×
273
            ForeignKey: foreignKey,
×
274
            IsRequired: isRequired,
×
275
            HasGetter: hasGetter,
×
276
            HasSetter: hasSetter,
×
277
            DisplayName: displayName,
×
278
            DataFormatString: dataFormatString,
×
279
            ColumnType: columnType,
×
280
            ColumnOrder: columnOrder
×
281
        );
×
282
    }
283

284
    private static string? GetColumnName(ImmutableArray<AttributeData> attributes)
285
    {
286
        var columnAttribute = FindSchemaAttribute(attributes, "ColumnAttribute");
×
287

288
        if (columnAttribute == null)
×
289
            return null;
×
290

291
        // attribute constructor [Column("Name")]
292
        var converterType = columnAttribute.ConstructorArguments.FirstOrDefault();
×
293
        if (converterType.Value is string stringValue)
×
294
            return stringValue;
×
295

296
        return null;
×
297
    }
298

299

300
    private static string? GetConverterName(ImmutableArray<AttributeData> attributes)
301
    {
302
        var converter = attributes
×
303
            .FirstOrDefault(a => a.AttributeClass is
×
304
            {
×
305
                Name: "DataFieldConverterAttribute",
×
306
                ContainingNamespace.Name: "FluentCommand"
×
307
            });
×
308

309
        if (converter == null)
×
310
            return null;
×
311

312
        // attribute constructor
313
        var converterType = converter.ConstructorArguments.FirstOrDefault();
×
314
        if (converterType.Value is INamedTypeSymbol converterSymbol)
×
315
            return converterSymbol.ToDisplayString();
×
316

317
        // generic attribute
318
        var attributeClass = converter.AttributeClass;
×
319
        if (attributeClass is { IsGenericType: true }
×
320
            && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length
×
321
            && attributeClass.TypeArguments.Length == 1)
×
322
        {
323
            var typeArgument = attributeClass.TypeArguments[0];
×
324
            return typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
325
        }
326

327
        return null;
×
328
    }
329

330
    private static AttributeData? FindDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
331
    {
332
        return attributes.FirstOrDefault(a => a.AttributeClass is
×
333
        {
×
334
            ContainingNamespace:
×
335
            {
×
336
                Name: "DataAnnotations",
×
337
                ContainingNamespace:
×
338
                {
×
339
                    Name: "ComponentModel",
×
340
                    ContainingNamespace.Name: "System"
×
341
                }
×
342
            }
×
343
        } && a.AttributeClass.Name == name);
×
344
    }
345

346
    private static bool HasDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
347
    {
348
        return FindDataAnnotationAttribute(attributes, name) != null;
×
349
    }
350

351
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
352
    {
353
        return attributes.FirstOrDefault(a =>
×
354
            a.AttributeClass is
×
355
            {
×
356
                ContainingNamespace:
×
357
                {
×
358
                    Name: "Schema",
×
359
                    ContainingNamespace:
×
360
                    {
×
361
                        Name: "DataAnnotations",
×
362
                        ContainingNamespace:
×
363
                        {
×
364
                            Name: "ComponentModel",
×
365
                            ContainingNamespace.Name: "System"
×
366
                        }
×
367
                    }
×
368
                }
×
369
            }
×
370
            && a.AttributeClass.Name == name
×
371
        );
×
372
    }
373

374
    private static string? GetSchemaAttributeConstructorStringArg(ImmutableArray<AttributeData> attributes, string name)
375
    {
376
        var attribute = FindSchemaAttribute(attributes, name);
×
377
        if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string value)
×
378
            return value;
×
379

380
        return null;
×
381
    }
382

383
    private static bool GetIsDatabaseGenerated(ImmutableArray<AttributeData> attributes)
384
    {
385
        var attribute = FindSchemaAttribute(attributes, "DatabaseGeneratedAttribute");
×
386
        if (attribute == null)
×
387
            return false;
×
388

389
        if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int option)
×
390
            return option != 0; // DatabaseGeneratedOption.None = 0
×
391

392
        return false;
×
393
    }
394

395
    private static string? GetNamedString(AttributeData? attribute, string argName)
396
    {
397
        if (attribute == null)
×
398
            return null;
×
399

400
        foreach (var namedArg in attribute.NamedArguments)
×
401
        {
402
            if (namedArg.Key == argName && namedArg.Value.Value is string value)
×
403
                return value;
×
404
        }
405

406
        return null;
×
407
    }
408

409
    private static int? GetNamedNumber(AttributeData? attribute, string argName)
410
    {
411
        if (attribute == null)
×
412
            return null;
×
413

414
        foreach (var namedArg in attribute.NamedArguments)
×
415
        {
416
            if (namedArg.Key == argName && namedArg.Value.Value is int value)
×
417
                return value;
×
418
        }
419

420
        return null;
×
421
    }
422

423
    private static bool IsIncluded(IPropertySymbol propertySymbol)
424
    {
425
        return !propertySymbol.IsIndexer
×
426
            && !propertySymbol.IsAbstract
×
427
            && propertySymbol.DeclaredAccessibility == Accessibility.Public;
×
428
    }
429

430
    private static bool IsSupportedType(ITypeSymbol type)
431
    {
432
        // handle nullable value types
433
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
434
            return IsSupportedType(namedType.TypeArguments[0]);
×
435

436
        // enums are stored as their underlying integer type
437
        if (type.TypeKind == TypeKind.Enum)
×
438
            return true;
×
439

440
        // primitives and string
441
        switch (type.SpecialType)
×
442
        {
443
            case SpecialType.System_Boolean:
444
            case SpecialType.System_Byte:
445
            case SpecialType.System_Char:
446
            case SpecialType.System_Decimal:
447
            case SpecialType.System_Double:
448
            case SpecialType.System_Single:
449
            case SpecialType.System_Int16:
450
            case SpecialType.System_Int32:
451
            case SpecialType.System_Int64:
452
            case SpecialType.System_String:
453
                return true;
×
454
        }
455

456
        // byte[]
457
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
458
            return true;
×
459

460
        // well-known struct types and FluentCommand.ConcurrencyToken
461
        var fullName = type.ToDisplayString();
×
462
        return fullName is
×
463
            "System.DateTime" or
×
464
            "System.DateTimeOffset" or
×
465
            "System.Guid" or
×
466
            "System.TimeSpan" or
×
467
            "System.DateOnly" or
×
468
            "System.TimeOnly" or
×
469
            "FluentCommand.ConcurrencyToken";
×
470
    }
471

472
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
473
    {
474
        return attributes.Any(
×
475
            a => a.AttributeClass is
×
476
            {
×
477
                Name: "NotMappedAttribute",
×
478
                ContainingNamespace:
×
479
                {
×
480
                    Name: "Schema",
×
481
                    ContainingNamespace:
×
482
                    {
×
483
                        Name: "DataAnnotations",
×
484
                        ContainingNamespace:
×
485
                        {
×
486
                            Name: "ComponentModel",
×
487
                            ContainingNamespace.Name: "System"
×
488
                        }
×
489
                    }
×
490
                }
×
491
            });
×
492
    }
493

494
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
495
    {
496
        return attributes.Any(a => a.AttributeClass is
×
497
        {
×
498
            Name: "IgnorePropertyAttribute",
×
499
            ContainingNamespace:
×
500
            {
×
501
                Name: "Attributes",
×
502
                ContainingNamespace.Name: "FluentCommand"
×
503
            }
×
504
        });
×
505
    }
506

507
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
508
    {
509
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
510

511
        foreach (var attr in attributes)
×
512
        {
513
            if (attr.AttributeClass is not
×
514
                {
×
515
                    Name: "IgnorePropertyAttribute",
×
516
                    ContainingNamespace:
×
517
                    {
×
518
                        Name: "Attributes",
×
519
                        ContainingNamespace.Name: "FluentCommand"
×
520
                    }
×
521
                })
×
522
            {
523
                continue;
524
            }
525

526
            // constructor argument: [IgnoreProperty("Name")] or [IgnoreProperty(nameof(T.Name))]
527
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
528
            {
529
                ignored.Add(ctorName);
×
530
                continue;
×
531
            }
532

533
            // named argument: [IgnoreProperty(PropertyName = "Name")]
534
            foreach (var namedArg in attr.NamedArguments)
×
535
            {
536
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
537
                    ignored.Add(namedValue);
×
538
            }
539
        }
540

541
        return ignored;
×
542
    }
543
}
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