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

loresoft / FluentCommand / 26923300515

04 Jun 2026 01:03AM UTC coverage: 65.014% (+9.9%) from 55.157%
26923300515

push

github

pwelter34
Merge branch 'master' of https://github.com/loresoft/FluentCommand

1728 of 3450 branches covered (50.09%)

Branch coverage included in aggregate %.

5510 of 7683 relevant lines covered (71.72%)

297.61 hits per line

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

78.32
/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
    private static readonly SymbolDisplayFormat FullyQualifiedNullableFormat = SymbolDisplayFormat.FullyQualifiedFormat
1✔
15
        .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
1✔
16

17
    public void Initialize(IncrementalGeneratorInitializationContext context)
18
    {
19
        // Pipeline for [GenerateReader(typeof(T))] attribute
20
        var generateAttributeClasses = context.SyntaxProvider
5✔
21
            .ForAttributeWithMetadataName(
5✔
22
                fullyQualifiedMetadataName: "FluentCommand.Attributes.GenerateReaderAttribute",
5✔
23
                predicate: static (_, __) => true,
5✔
24
                transform: static (context, _) =>
5✔
25
                {
5✔
26
                    if (context.Attributes.Length == 0)
5✔
27
                        return [];
5✔
28

5✔
29
                    var classes = new List<EntityClass>();
5✔
30

5✔
31
                    foreach (var attribute in context.Attributes)
5✔
32
                    {
5✔
33
                        if (attribute == null)
5✔
34
                            return [];
5✔
35

5✔
36
                        if (attribute.ConstructorArguments.Length != 1)
5✔
37
                            return [];
5✔
38

5✔
39
                        if (GetTypeArgument(attribute.ConstructorArguments[0]) is not INamedTypeSymbol targetSymbol)
5✔
40
                            return [];
5✔
41

5✔
42
                        var ignoreProperties = GetNamedStringArray(attribute, "IgnoreProperties");
5✔
43
                        var jsonProperties = GetNamedStringArray(attribute, "JsonProperties");
5✔
44

5✔
45
                        var entityClass = CreateClass(targetSymbol, ignoreProperties, jsonProperties);
5✔
46
                        if (entityClass != null)
5✔
47
                            classes.Add(entityClass);
5✔
48
                    }
5✔
49

5✔
50
                    return new EquatableArray<EntityClass>(classes);
5✔
51
                }
5✔
52
            )
5✔
53
            .Where(static context => context.Count > 0)
5✔
54
            .SelectMany(static (item, _) => item)
5✔
55
            .WithTrackingName("GenerateAttributeGenerator");
5✔
56

57
        context.RegisterSourceOutput(generateAttributeClasses, WriteDataReaderSource);
5✔
58
        context.RegisterSourceOutput(generateAttributeClasses, WriteTypeAccessorSource);
5✔
59

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

5✔
84
                    return CreateClass(targetSymbol);
5✔
85
                }
5✔
86
            )
5✔
87
            .Where(static context => context is not null)
5✔
88
            .Select(static (context, _) => context!)
5✔
89
            .WithTrackingName("TableAttributeGenerator");
5✔
90

91
        context.RegisterSourceOutput(tableAttributeClasses, WriteDataReaderSource);
5✔
92
        context.RegisterSourceOutput(tableAttributeClasses, WriteTypeAccessorSource);
5✔
93
    }
5✔
94

95
    private static void WriteDataReaderSource(SourceProductionContext context, EntityClass entityClass)
96
    {
97
        var qualifiedName = entityClass.EntityNamespace is null
5!
98
            ? entityClass.EntityName
5✔
99
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
5✔
100

101
        var source = DataReaderFactoryWriter.Generate(entityClass);
5✔
102

103
        context.AddSource($"{qualifiedName}DataReaderExtensions.g.cs", source);
5✔
104
    }
5✔
105

106
    private static void WriteTypeAccessorSource(SourceProductionContext context, EntityClass entityClass)
107
    {
108
        var qualifiedName = entityClass.EntityNamespace is null
5!
109
            ? entityClass.EntityName
5✔
110
            : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
5✔
111

112
        var source = TypeAccessorWriter.Generate(entityClass);
5✔
113

114
        context.AddSource($"{qualifiedName}TypeAccessor.g.cs", source);
5✔
115
    }
5✔
116

117

118
    private static EntityClass? CreateClass(INamedTypeSymbol targetSymbol)
119
    {
120
        return CreateClass(targetSymbol, [], []);
2✔
121
    }
122

123
    private static EntityClass? CreateClass(
124
        INamedTypeSymbol targetSymbol,
125
        IEnumerable<string> ignoreProperties,
126
        IEnumerable<string> jsonPropertyNames)
127
    {
128
        if (targetSymbol == null)
5!
129
            return null;
×
130

131
        var fullyQualified = targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
5✔
132
        var classNamespace = targetSymbol.ContainingNamespace.ToDisplayString();
5✔
133
        var className = targetSymbol.Name;
5✔
134

135
        // extract table mapping info
136
        var typeAttributes = targetSymbol.GetAttributes();
5✔
137
        var tableAttribute = FindSchemaAttribute(typeAttributes, "TableAttribute");
5✔
138

139
        string? tableName = null;
5✔
140
        string? tableSchema = null;
5✔
141

142
        if (tableAttribute != null)
5✔
143
        {
144
            if (tableAttribute.ConstructorArguments.Length > 0 && tableAttribute.ConstructorArguments[0].Value is string name)
2!
145
                tableName = name;
2✔
146

147
            tableSchema = GetNamedString(tableAttribute, "Schema");
2✔
148
        }
149

150
        var mode = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0)
5✔
151
            ? InitializationMode.ObjectInitializer
5✔
152
            : InitializationMode.Constructor;
5✔
153

154
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
5✔
155
        foreach (var ignoredProperty in ignoreProperties)
12✔
156
            classIgnored.Add(ignoredProperty);
1✔
157

158
        var jsonProperties = new HashSet<string>(jsonPropertyNames, StringComparer.Ordinal);
5✔
159

160
        var propertySymbols = GetProperties(targetSymbol);
5✔
161

162
        if (mode == InitializationMode.ObjectInitializer)
5✔
163
        {
164
            var propertyArray = propertySymbols
3✔
165
                .Select(propertySymbol => CreateProperty(
3✔
166
                    propertySymbol: propertySymbol,
3✔
167
                    classIgnored: classIgnored,
3✔
168
                    jsonProperties: jsonProperties)
3✔
169
                )
3✔
170
                .ToArray();
3✔
171

172
            return new EntityClass
3✔
173
            {
3✔
174
                InitializationMode = mode,
3✔
175
                FullyQualified = fullyQualified,
3✔
176
                EntityNamespace = classNamespace,
3✔
177
                EntityName = className,
3✔
178
                Properties = propertyArray,
3✔
179
                TableName = tableName,
3✔
180
                TableSchema = tableSchema
3✔
181
            };
3✔
182
        }
183

184
        // constructor initialization
185

186
        // constructor with same number of parameters as mappable properties
187
        var mappableCount = propertySymbols
2✔
188
            .Count(p => IsMappableProperty(p, classIgnored, jsonProperties));
2✔
189

190
        var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
2✔
191
        if (constructor == null)
2!
192
            return null;
×
193

194
        var properties = new List<EntityProperty>();
2✔
195
        foreach (var propertySymbol in propertySymbols)
16✔
196
        {
197
            // find matching constructor name
198
            var parameter = constructor
6✔
199
                .Parameters
6✔
200
                .FirstOrDefault(p => string.Equals(p.Name, propertySymbol.Name, StringComparison.InvariantCultureIgnoreCase));
6✔
201

202
            if (parameter == null)
6✔
203
                continue;
204

205
            var property = CreateProperty(
6✔
206
                propertySymbol: propertySymbol,
6✔
207
                parameterName: parameter.Name,
6✔
208
                classIgnored: classIgnored,
6✔
209
                jsonProperties: jsonProperties,
6✔
210
                parameterAttributes: parameter.GetAttributes());
6✔
211

212
            properties.Add(property);
6✔
213
        }
214

215
        return new EntityClass
2✔
216
        {
2✔
217
            InitializationMode = mode,
2✔
218
            FullyQualified = fullyQualified,
2✔
219
            EntityNamespace = classNamespace,
2✔
220
            EntityName = className,
2✔
221
            Properties = properties,
2✔
222
            TableName = tableName,
2✔
223
            TableSchema = tableSchema
2✔
224
        };
2✔
225
    }
226

227
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
228
    {
229
        var properties = new Dictionary<string, IPropertySymbol>();
5✔
230

231
        var currentSymbol = targetSymbol;
5✔
232

233
        // get nested properties
234
        while (currentSymbol != null)
15✔
235
        {
236
            var propertySymbols = currentSymbol
10✔
237
                .GetMembers()
10✔
238
                .Where(m => m.Kind == SymbolKind.Property)
10✔
239
                .OfType<IPropertySymbol>()
10✔
240
                .Where(IsIncluded)
10✔
241
                .Where(p => !properties.ContainsKey(p.Name));
10✔
242

243
            foreach (var propertySymbol in propertySymbols)
52✔
244
                properties.Add(propertySymbol.Name, propertySymbol);
16✔
245

246
            currentSymbol = currentSymbol.BaseType;
10✔
247
        }
248

249
        return [.. properties.Values];
5✔
250
    }
251

252
    private static EntityProperty CreateProperty(
253
        IPropertySymbol propertySymbol,
254
        string? parameterName = null,
255
        HashSet<string>? classIgnored = null,
256
        HashSet<string>? jsonProperties = null,
257
        ImmutableArray<AttributeData> parameterAttributes = default)
258
    {
259
        var propertyType = propertySymbol.Type.ToDisplayString(FullyQualifiedNullableFormat);
16✔
260
        var memberTypeName = propertySymbol.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString(FullyQualifiedNullableFormat);
16✔
261
        var propertyName = propertySymbol.Name;
16✔
262
        var hasGetter = propertySymbol.GetMethod != null;
16✔
263
        var hasSetter = propertySymbol.SetMethod?.IsInitOnly == false;
16✔
264

265
        // Merge property attributes with constructor parameter attributes (parameter attributes take precedence for converters)
266
        var propertyAttributes = propertySymbol.GetAttributes();
16✔
267
        var attributes = (!parameterAttributes.IsDefaultOrEmpty && !propertyAttributes.IsDefaultOrEmpty)
16!
268
            ? propertyAttributes.AddRange(parameterAttributes)
16✔
269
            : !parameterAttributes.IsDefaultOrEmpty ? parameterAttributes : propertyAttributes;
16✔
270

271
        var jsonColumn = GetJsonColumn(attributes);
16✔
272
        var isConfiguredJsonColumn = jsonProperties?.Contains(propertyName) == true;
16!
273
        var isJsonColumn = jsonColumn != null || isConfiguredJsonColumn;
16✔
274
        var enumInfo = GetEnumInfo(propertySymbol.Type);
16✔
275
        var isNullable = IsNullableType(propertySymbol.Type);
16✔
276
        var isNotMapped = (classIgnored?.Contains(propertyName) == true) || (!isJsonColumn && !IsSupportedType(propertySymbol.Type));
16!
277

278
        if (attributes == default || attributes.Length == 0)
16!
279
        {
280
            return new EntityProperty
12✔
281
            {
12✔
282
                PropertyName = propertyName,
12✔
283
                ColumnName = propertyName,
12✔
284
                PropertyType = propertyType,
12✔
285
                MemberTypeName = memberTypeName,
12✔
286
                ParameterName = parameterName,
12✔
287
                IsNotMapped = isNotMapped,
12✔
288
                HasGetter = hasGetter,
12✔
289
                HasSetter = hasSetter,
12✔
290
                IsNullable = isNullable,
12✔
291
                IsJsonColumn = isJsonColumn,
12✔
292
                IsEnum = enumInfo.IsEnum,
12✔
293
                IsNullableEnum = enumInfo.IsNullableEnum,
12✔
294
                EnumUnderlyingType = enumInfo.UnderlyingType
12✔
295
            };
12✔
296
        }
297

298
        var columnName = GetColumnName(attributes) ?? propertyName;
4✔
299
        var converterName = GetConverterName(attributes);
4✔
300

301
        var isKey = HasDataAnnotationAttribute(attributes, "KeyAttribute");
4✔
302

303
        isNotMapped = isNotMapped
4✔
304
            || IsNotMapped(attributes)
4✔
305
            || HasIgnorePropertyAttribute(attributes);
4✔
306

307
        var isDatabaseGenerated = GetIsDatabaseGenerated(attributes);
4✔
308
        var isConcurrencyCheck = HasDataAnnotationAttribute(attributes, "ConcurrencyCheckAttribute");
4✔
309
        var foreignKey = GetSchemaAttributeConstructorStringArg(attributes, "ForeignKeyAttribute");
4✔
310
        var isRequired = HasDataAnnotationAttribute(attributes, "RequiredAttribute");
4✔
311
        var displayName = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayAttribute"), "Name");
4✔
312
        var dataFormatString = GetNamedString(FindDataAnnotationAttribute(attributes, "DisplayFormatAttribute"), "DataFormatString");
4✔
313
        var columnType = GetNamedString(FindSchemaAttribute(attributes, "ColumnAttribute"), "TypeName");
4✔
314
        var columnOrder = GetNamedNumber(FindSchemaAttribute(attributes, "ColumnAttribute"), "Order");
4✔
315

316
        return new EntityProperty
4✔
317
        {
4✔
318
            PropertyName = propertyName,
4✔
319
            ColumnName = columnName,
4✔
320
            PropertyType = propertyType,
4✔
321
            MemberTypeName = memberTypeName,
4✔
322
            ParameterName = parameterName,
4✔
323
            ConverterName = converterName,
4✔
324
            IsKey = isKey,
4✔
325
            IsNotMapped = isNotMapped,
4✔
326
            IsDatabaseGenerated = isDatabaseGenerated,
4✔
327
            IsConcurrencyCheck = isConcurrencyCheck,
4✔
328
            ForeignKey = foreignKey,
4✔
329
            IsRequired = isRequired,
4✔
330
            IsNullable = isNullable,
4✔
331
            HasGetter = hasGetter,
4✔
332
            HasSetter = hasSetter,
4✔
333
            DisplayName = displayName,
4✔
334
            DataFormatString = dataFormatString,
4✔
335
            ColumnType = columnType,
4✔
336
            ColumnOrder = columnOrder,
4✔
337
            IsJsonColumn = isJsonColumn,
4✔
338
            IsEnum = enumInfo.IsEnum,
4✔
339
            IsNullableEnum = enumInfo.IsNullableEnum,
4✔
340
            EnumUnderlyingType = enumInfo.UnderlyingType
4✔
341
        };
4✔
342
    }
343

344
    private static (bool IsEnum, bool IsNullableEnum, string? UnderlyingType) GetEnumInfo(ITypeSymbol type)
345
    {
346
        var isNullableEnum = false;
16✔
347
        var enumType = type;
16✔
348

349
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
16!
350
        {
351
            isNullableEnum = true;
×
352
            enumType = namedType.TypeArguments[0];
×
353
        }
354

355
        if (enumType is not INamedTypeSymbol { TypeKind: TypeKind.Enum } namedEnum)
16!
356
            return (false, false, null);
16✔
357

358
        return (true, isNullableEnum, namedEnum.EnumUnderlyingType?.ToDisplayString());
×
359
    }
360

361
    private static bool IsNullableType(ITypeSymbol type)
362
    {
363
        return type.NullableAnnotation == NullableAnnotation.Annotated
16!
364
            || type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T };
16✔
365
    }
366

367
    private static AttributeData? GetJsonColumn(ImmutableArray<AttributeData> attributes)
368
    {
369
        return attributes.FirstOrDefault(a => a.AttributeClass is
22✔
370
        {
22✔
371
            Name: "JsonColumnAttribute",
22✔
372
            ContainingNamespace:
22✔
373
            {
22✔
374
                Name: "Attributes",
22✔
375
                ContainingNamespace.Name: "FluentCommand"
22✔
376
            }
22✔
377
        });
22✔
378
    }
379

380
    private static string? GetColumnName(ImmutableArray<AttributeData> attributes)
381
    {
382
        var columnAttribute = FindSchemaAttribute(attributes, "ColumnAttribute");
4✔
383

384
        if (columnAttribute == null)
4✔
385
            return null;
1✔
386

387
        // attribute constructor [Column("Name")]
388
        var converterType = columnAttribute.ConstructorArguments.FirstOrDefault();
3✔
389
        if (converterType.Value is string stringValue)
3!
390
            return stringValue;
3✔
391

392
        return null;
×
393
    }
394

395
    private static string? GetConverterName(ImmutableArray<AttributeData> attributes)
396
    {
397
        var converter = attributes
4✔
398
            .FirstOrDefault(a => a.AttributeClass is
4✔
399
            {
4✔
400
                Name: "DataFieldConverterAttribute",
4✔
401
                ContainingNamespace.Name: "FluentCommand"
4✔
402
            });
4✔
403

404
        if (converter == null)
4!
405
            return null;
4✔
406

407
        // attribute constructor
408
        var converterType = converter.ConstructorArguments.FirstOrDefault();
×
409
        if (converterType.Value is INamedTypeSymbol converterSymbol)
×
410
            return converterSymbol.ToDisplayString(FullyQualifiedNullableFormat);
×
411

412
        // generic attribute
413
        var attributeClass = converter.AttributeClass;
×
414
        if (attributeClass is { IsGenericType: true }
×
415
            && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length
×
416
            && attributeClass.TypeArguments.Length == 1)
×
417
        {
418
            var typeArgument = attributeClass.TypeArguments[0];
×
419
            return typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
×
420
        }
421

422
        return null;
×
423
    }
424

425
    private static AttributeData? FindDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
426
    {
427
        return attributes.FirstOrDefault(a => a.AttributeClass is
20✔
428
        {
20✔
429
            ContainingNamespace:
20✔
430
            {
20✔
431
                Name: "DataAnnotations",
20✔
432
                ContainingNamespace:
20✔
433
                {
20✔
434
                    Name: "ComponentModel",
20✔
435
                    ContainingNamespace.Name: "System"
20✔
436
                }
20✔
437
            }
20✔
438
        } && a.AttributeClass.Name == name);
20✔
439
    }
440

441
    private static bool HasDataAnnotationAttribute(ImmutableArray<AttributeData> attributes, string name)
442
    {
443
        return FindDataAnnotationAttribute(attributes, name) != null;
12✔
444
    }
445

446
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
447
    {
448
        return attributes.FirstOrDefault(a =>
25✔
449
            a.AttributeClass is
25✔
450
            {
25✔
451
                ContainingNamespace:
25✔
452
                {
25✔
453
                    Name: "Schema",
25✔
454
                    ContainingNamespace:
25✔
455
                    {
25✔
456
                        Name: "DataAnnotations",
25✔
457
                        ContainingNamespace:
25✔
458
                        {
25✔
459
                            Name: "ComponentModel",
25✔
460
                            ContainingNamespace.Name: "System"
25✔
461
                        }
25✔
462
                    }
25✔
463
                }
25✔
464
            }
25✔
465
            && a.AttributeClass.Name == name
25✔
466
        );
25✔
467
    }
468

469
    private static string? GetSchemaAttributeConstructorStringArg(ImmutableArray<AttributeData> attributes, string name)
470
    {
471
        var attribute = FindSchemaAttribute(attributes, name);
4✔
472
        if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string value)
4!
473
            return value;
×
474

475
        return null;
4✔
476
    }
477

478
    private static bool GetIsDatabaseGenerated(ImmutableArray<AttributeData> attributes)
479
    {
480
        var attribute = FindSchemaAttribute(attributes, "DatabaseGeneratedAttribute");
4✔
481
        if (attribute == null)
4!
482
            return false;
4✔
483

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

487
        return false;
×
488
    }
489

490
    private static string? GetNamedString(AttributeData? attribute, string argName)
491
    {
492
        if (attribute == null)
14✔
493
            return null;
9✔
494

495
        foreach (var namedArg in attribute.NamedArguments)
11✔
496
        {
497
            if (namedArg.Key == argName && namedArg.Value.Value is string value)
1✔
498
                return value;
1✔
499
        }
500

501
        return null;
4✔
502
    }
503

504
    private static string[] GetNamedStringArray(AttributeData attribute, string argName)
505
    {
506
        foreach (var namedArg in attribute.NamedArguments)
16✔
507
        {
508
            if (namedArg.Key != argName || namedArg.Value.Kind != TypedConstantKind.Array)
3✔
509
                continue;
510

511
            return namedArg.Value.Values
2✔
512
                .Select(static v => v.Value)
2✔
513
                .OfType<string>()
2✔
514
                .ToArray();
2✔
515
        }
516

517
        return [];
4✔
518
    }
519

520
    private static ITypeSymbol? GetTypeArgument(TypedConstant argument)
521
    {
522
        return argument.Kind == TypedConstantKind.Type
3!
523
            ? argument.Value as ITypeSymbol
3✔
524
            : null;
3✔
525
    }
526

527
    private static int? GetNamedNumber(AttributeData? attribute, string argName)
528
    {
529
        if (attribute == null)
4✔
530
            return null;
1✔
531

532
        foreach (var namedArg in attribute.NamedArguments)
6!
533
        {
534
            if (namedArg.Key == argName && namedArg.Value.Value is int value)
×
535
                return value;
×
536
        }
537

538
        return null;
3✔
539
    }
540

541
    private static bool IsIncluded(IPropertySymbol propertySymbol)
542
    {
543
        return !propertySymbol.IsIndexer
17!
544
            && !propertySymbol.IsAbstract
17✔
545
            && propertySymbol.DeclaredAccessibility == Accessibility.Public;
17✔
546
    }
547

548
    private static bool IsSupportedType(ITypeSymbol type)
549
    {
550
        // handle nullable value types
551
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
20!
552
            return IsSupportedType(namedType.TypeArguments[0]);
×
553

554
        // enums are stored as their underlying integer type
555
        if (type.TypeKind == TypeKind.Enum)
20!
556
            return true;
×
557

558
        // primitives and string
559
        switch (type.SpecialType)
20✔
560
        {
561
            case SpecialType.System_Boolean:
562
            case SpecialType.System_Byte:
563
            case SpecialType.System_Char:
564
            case SpecialType.System_Decimal:
565
            case SpecialType.System_Double:
566
            case SpecialType.System_Single:
567
            case SpecialType.System_Int16:
568
            case SpecialType.System_Int32:
569
            case SpecialType.System_Int64:
570
            case SpecialType.System_String:
571
                return true;
18✔
572
        }
573

574
        // byte[]
575
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
2!
576
            return true;
×
577

578
        // well-known struct types and FluentCommand.ConcurrencyToken
579
        var fullName = type.ToDisplayString();
2✔
580
        return fullName is
2!
581
            "System.DateTime" or
2✔
582
            "System.DateTimeOffset" or
2✔
583
            "System.Guid" or
2✔
584
            "System.TimeSpan" or
2✔
585
            "System.DateOnly" or
2✔
586
            "System.TimeOnly" or
2✔
587
            "FluentCommand.ConcurrencyToken";
2✔
588
    }
589

590
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
591
    {
592
        return attributes.Any(
10✔
593
            a => a.AttributeClass is
10✔
594
            {
10✔
595
                Name: "NotMappedAttribute",
10✔
596
                ContainingNamespace:
10✔
597
                {
10✔
598
                    Name: "Schema",
10✔
599
                    ContainingNamespace:
10✔
600
                    {
10✔
601
                        Name: "DataAnnotations",
10✔
602
                        ContainingNamespace:
10✔
603
                        {
10✔
604
                            Name: "ComponentModel",
10✔
605
                            ContainingNamespace.Name: "System"
10✔
606
                        }
10✔
607
                    }
10✔
608
                }
10✔
609
            });
10✔
610
    }
611

612
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
613
    {
614
        return attributes.Any(a => a.AttributeClass is
9✔
615
        {
9✔
616
            Name: "IgnorePropertyAttribute",
9✔
617
            ContainingNamespace:
9✔
618
            {
9✔
619
                Name: "Attributes",
9✔
620
                ContainingNamespace.Name: "FluentCommand"
9✔
621
            }
9✔
622
        });
9✔
623
    }
624

625
    private static bool IsMappableProperty(IPropertySymbol propertySymbol, HashSet<string> classIgnored, HashSet<string>? jsonProperties = null)
626
    {
627
        var attributes = propertySymbol.GetAttributes();
6✔
628
        if (classIgnored.Contains(propertySymbol.Name) || HasIgnorePropertyAttribute(attributes) || IsNotMapped(attributes))
6!
629
            return false;
×
630

631
        return jsonProperties?.Contains(propertySymbol.Name) == true || GetJsonColumn(attributes) != null || IsSupportedType(propertySymbol.Type);
6!
632
    }
633

634
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
635
    {
636
        var ignored = new HashSet<string>(StringComparer.Ordinal);
5✔
637

638
        foreach (var attr in attributes)
14✔
639
        {
640
            if (attr.AttributeClass is not
2!
641
                {
2✔
642
                    Name: "IgnorePropertyAttribute",
2✔
643
                    ContainingNamespace:
2✔
644
                    {
2✔
645
                        Name: "Attributes",
2✔
646
                        ContainingNamespace.Name: "FluentCommand"
2✔
647
                    }
2✔
648
                })
2✔
649
            {
650
                continue;
651
            }
652

653
            // constructor argument: [IgnoreProperty("Name")] or [IgnoreProperty(nameof(T.Name))]
654
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
655
            {
656
                ignored.Add(ctorName);
×
657
                continue;
×
658
            }
659

660
            // named argument: [IgnoreProperty(PropertyName = "Name")]
661
            foreach (var namedArg in attr.NamedArguments)
×
662
            {
663
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
664
                    ignored.Add(namedValue);
×
665
            }
666
        }
667

668
        return ignored;
5✔
669
    }
670
}
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