• 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

80.43
/src/FluentCommand.Generators/DataReaderFactoryAnalyzer.cs
1
using System.Collections.Immutable;
2

3
using Microsoft.CodeAnalysis;
4
using Microsoft.CodeAnalysis.Diagnostics;
5

6
namespace FluentCommand.Generators;
7

8
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9
public sealed class DataReaderFactoryAnalyzer : DiagnosticAnalyzer
10
{
11
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
12
        ImmutableArray.Create(
8✔
13
            DiagnosticDescriptors.NoMatchingConstructor,
8✔
14
            DiagnosticDescriptors.ConstructorParameterNotMatched,
8✔
15
            DiagnosticDescriptors.NoMappableProperties,
8✔
16
            DiagnosticDescriptors.UnsupportedPropertyType,
8✔
17
            DiagnosticDescriptors.InvalidGenerateReaderArgument,
8✔
18
            DiagnosticDescriptors.TableAttributeOnInvalidType,
8✔
19
            DiagnosticDescriptors.UnknownGenerateReaderProperty
8✔
20
        );
8✔
21

22
    public override void Initialize(AnalysisContext context)
23
    {
24
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
8✔
25
        context.EnableConcurrentExecution();
8✔
26

27
        context.RegisterCompilationAction(AnalyzeCompilation);
8✔
28
        context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
8✔
29
    }
8✔
30

31
    private static void AnalyzeCompilation(CompilationAnalysisContext context)
32
    {
33
        AnalyzeGenerateReaderAttributes(
8✔
34
            context.ReportDiagnostic,
8✔
35
            context.CancellationToken,
8✔
36
            context.Compilation.Assembly.GetAttributes(),
8✔
37
            context.Compilation.Assembly.Name,
8✔
38
            Location.None);
8✔
39

40
        AnalyzeGenerateReaderAttributes(
8✔
41
            context.ReportDiagnostic,
8✔
42
            context.CancellationToken,
8✔
43
            context.Compilation.SourceModule.GetAttributes(),
8✔
44
            context.Compilation.SourceModule.Name,
8✔
45
            Location.None);
8✔
46
    }
8✔
47

48
    private static void AnalyzeNamedType(SymbolAnalysisContext context)
49
    {
50
        if (context.Symbol is not INamedTypeSymbol typeSymbol)
11!
51
            return;
×
52

53
        var attributes = typeSymbol.GetAttributes();
11✔
54

55
        // Check [Table] attribute path
56
        var tableAttribute = FindSchemaAttribute(attributes, "TableAttribute");
11✔
57
        if (tableAttribute != null)
11✔
58
        {
59
            if (typeSymbol.IsStatic || typeSymbol.IsAbstract)
2!
60
            {
61
                var modifier = typeSymbol.IsStatic ? "static" : "abstract";
2✔
62

63
                var location = tableAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation()
2!
64
                    ?? typeSymbol.Locations.FirstOrDefault()
2✔
65
                    ?? Location.None;
2✔
66

67
                context.ReportDiagnostic(Diagnostic.Create(
2✔
68
                    DiagnosticDescriptors.TableAttributeOnInvalidType,
2✔
69
                    location,
2✔
70
                    typeSymbol.Name,
2✔
71
                    modifier));
2✔
72
            }
73
            else
74
            {
75
                AnalyzeEntityType(context.ReportDiagnostic, context.CancellationToken, typeSymbol);
×
76
            }
77
        }
78

79
        AnalyzeGenerateReaderAttributes(
11!
80
            context.ReportDiagnostic,
11✔
81
            context.CancellationToken,
11✔
82
            attributes,
11✔
83
            typeSymbol.Name,
11✔
84
            typeSymbol.Locations.FirstOrDefault() ?? Location.None);
11✔
85
    }
11✔
86

87
    private static void AnalyzeGenerateReaderAttributes(
88
        Action<Diagnostic> reportDiagnostic,
89
        CancellationToken cancellationToken,
90
        ImmutableArray<AttributeData> attributes,
91
        string ownerName,
92
        Location fallbackLocation)
93
    {
94
        foreach (var attr in attributes)
72✔
95
        {
96
            if (!IsGenerateReaderAttribute(attr))
9✔
97
                continue;
98

99
            if (attr.ConstructorArguments.Length != 1 ||
7!
100
                GetTypeArgument(attr.ConstructorArguments[0]) is not INamedTypeSymbol targetSymbol)
7✔
101
            {
102
                var location = attr.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation() ?? fallbackLocation;
1!
103

104
                var diagnostic = Diagnostic.Create(
1✔
105
                    descriptor: DiagnosticDescriptors.InvalidGenerateReaderArgument,
1✔
106
                    location: location,
1✔
107
                    messageArgs: ownerName);
1✔
108

109
                reportDiagnostic(diagnostic);
1✔
110

111
                continue;
1✔
112
            }
113

114
            var ignoreProperties = GetNamedStringArray(attr, "IgnoreProperties");
6✔
115
            var jsonProperties = GetNamedStringArray(attr, "JsonProperties");
6✔
116

117
            AnalyzeEntityType(reportDiagnostic, cancellationToken, targetSymbol, ignoreProperties, jsonProperties, attr);
6✔
118
        }
119
    }
27✔
120

121
    private static ITypeSymbol? GetTypeArgument(TypedConstant argument)
122
    {
123
        return argument.Kind == TypedConstantKind.Type
7!
124
            ? argument.Value as ITypeSymbol
7✔
125
            : null;
7✔
126
    }
127

128
    private static void AnalyzeEntityType(
129
        Action<Diagnostic> reportDiagnostic,
130
        CancellationToken cancellationToken,
131
        INamedTypeSymbol targetSymbol,
132
        string[]? ignoreProperties = null,
133
        string[]? jsonPropertyNames = null,
134
        AttributeData? generateReaderAttribute = null)
135
    {
136
        var typeAttributes = targetSymbol.GetAttributes();
6✔
137
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
6✔
138

139
        if (ignoreProperties != null)
6!
140
        {
141
            foreach (var ignoredProperty in ignoreProperties)
16✔
142
                classIgnored.Add(ignoredProperty);
2✔
143
        }
144

145
        var jsonProperties = jsonPropertyNames == null
6!
146
            ? new HashSet<string>(StringComparer.Ordinal)
6✔
147
            : new HashSet<string>(jsonPropertyNames, StringComparer.Ordinal);
6✔
148

149
        var propertySymbols = GetProperties(targetSymbol);
6✔
150

151
        ignoreProperties ??= [];
6!
152
        jsonPropertyNames ??= [];
6!
153

154
        if (generateReaderAttribute != null)
6!
155
        {
156
            AnalyzeGenerateReaderOptions(
6✔
157
                reportDiagnostic: reportDiagnostic,
6✔
158
                cancellationToken: cancellationToken,
6✔
159
                targetSymbol: targetSymbol,
6✔
160
                propertySymbols: propertySymbols,
6✔
161
                ignoreProperties: ignoreProperties,
6✔
162
                jsonProperties: jsonPropertyNames,
6✔
163
                attribute: generateReaderAttribute);
6✔
164
        }
165

166
        var hasParameterlessCtor = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0);
6✔
167

168
        // Count mappable properties
169
        var mappableProperties = propertySymbols
6✔
170
            .Where(p => IsMappableProperty(p, classIgnored, jsonProperties))
6✔
171
            .ToList();
6✔
172

173
        // Report unsupported property types
174
        foreach (var prop in propertySymbols)
36✔
175
        {
176
            var propertyAttributes = prop.GetAttributes();
12✔
177

178
            if (classIgnored.Contains(prop.Name) || HasIgnorePropertyAttribute(propertyAttributes) || IsNotMapped(propertyAttributes))
12✔
179
                continue;
180

181
            var jsonColumn = GetJsonColumnAttribute(propertyAttributes);
11✔
182
            if (jsonColumn != null)
11✔
183
                continue;
184

185
            if (jsonProperties.Contains(prop.Name))
11✔
186
                continue;
187

188
            if (!IsSupportedType(prop.Type))
9✔
189
            {
190
                var diagnostic = Diagnostic.Create(
2!
191
                    DiagnosticDescriptors.UnsupportedPropertyType,
2✔
192
                    prop.Locations.FirstOrDefault() ?? Location.None,
2✔
193
                    prop.Name,
2✔
194
                    targetSymbol.Name,
2✔
195
                    prop.Type.ToDisplayString());
2✔
196

197
                reportDiagnostic(diagnostic);
2✔
198
            }
199
        }
200

201
        // Report no mappable properties
202
        if (mappableProperties.Count == 0)
6✔
203
        {
204
            var diagnostic = Diagnostic.Create(
1!
205
                DiagnosticDescriptors.NoMappableProperties,
1✔
206
                targetSymbol.Locations.FirstOrDefault() ?? Location.None,
1✔
207
                targetSymbol.Name);
1✔
208

209
            reportDiagnostic(diagnostic);
1✔
210
            return;
1✔
211
        }
212

213
        // Constructor mode analysis
214
        if (!hasParameterlessCtor)
5✔
215
        {
216
            var mappableCount = propertySymbols
3✔
217
                .Count(p => IsMappableProperty(p, classIgnored, jsonProperties));
3✔
218

219
            var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
3✔
220

221
            if (constructor == null)
3✔
222
            {
223
                var diagnostic = Diagnostic.Create(
1!
224
                    DiagnosticDescriptors.NoMatchingConstructor,
1✔
225
                    targetSymbol.Locations.FirstOrDefault() ?? Location.None,
1✔
226
                    targetSymbol.Name,
1✔
227
                    mappableCount);
1✔
228

229
                reportDiagnostic(diagnostic);
1✔
230
                return;
1✔
231
            }
232

233
            // Check for unmatched constructor parameters
234
            foreach (var parameter in constructor.Parameters)
12✔
235
            {
236
                var hasMatch = propertySymbols.Any(p =>
4✔
237
                    string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
4✔
238

239
                if (!hasMatch)
4✔
240
                {
241
                    var diagnostic = Diagnostic.Create(
1!
242
                        DiagnosticDescriptors.ConstructorParameterNotMatched,
1✔
243
                        parameter.Locations.FirstOrDefault() ?? constructor.Locations.FirstOrDefault() ?? Location.None,
1✔
244
                        parameter.Name,
1✔
245
                        targetSymbol.Name);
1✔
246

247
                    reportDiagnostic(diagnostic);
1✔
248
                }
249
            }
250
        }
251
    }
4✔
252

253
    private static void AnalyzeGenerateReaderOptions(
254
        Action<Diagnostic> reportDiagnostic,
255
        CancellationToken cancellationToken,
256
        INamedTypeSymbol targetSymbol,
257
        List<IPropertySymbol> propertySymbols,
258
        string[] ignoreProperties,
259
        string[] jsonProperties,
260
        AttributeData attribute)
261
    {
262
        var location = attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation()
6!
263
            ?? targetSymbol.Locations.FirstOrDefault()
6✔
264
            ?? Location.None;
6✔
265

266
        var propertyNames = new HashSet<string>(propertySymbols.Select(static p => p.Name), StringComparer.Ordinal);
6✔
267

268
        ReportUnknownGenerateReaderProperties(reportDiagnostic, targetSymbol, location, propertyNames, "IgnoreProperties", ignoreProperties);
6✔
269
        ReportUnknownGenerateReaderProperties(reportDiagnostic, targetSymbol, location, propertyNames, "JsonProperties", jsonProperties);
6✔
270
    }
6✔
271

272
    private static void ReportUnknownGenerateReaderProperties(
273
        Action<Diagnostic> reportDiagnostic,
274
        INamedTypeSymbol targetSymbol,
275
        Location location,
276
        HashSet<string> propertyNames,
277
        string optionName,
278
        string[] configuredNames)
279
    {
280
        foreach (var configuredName in configuredNames)
34✔
281
        {
282
            if (propertyNames.Contains(configuredName))
5✔
283
                continue;
284

285
            var diagnostic = Diagnostic.Create(
2✔
286
                DiagnosticDescriptors.UnknownGenerateReaderProperty,
2✔
287
                location,
2✔
288
                optionName,
2✔
289
                configuredName,
2✔
290
                targetSymbol.Name);
2✔
291

292
            reportDiagnostic(diagnostic);
2✔
293
        }
294
    }
12✔
295

296
    #region Attribute helpers (mirrors generator logic)
297

298
    private static bool IsGenerateReaderAttribute(AttributeData attr)
299
    {
300
        return attr.AttributeClass is
9✔
301
        {
9✔
302
            Name: "GenerateReaderAttribute",
9✔
303
            ContainingNamespace:
9✔
304
            {
9✔
305
                Name: "Attributes",
9✔
306
                ContainingNamespace.Name: "FluentCommand"
9✔
307
            }
9✔
308
        };
9✔
309
    }
310

311
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
312
    {
313
        return attributes.FirstOrDefault(a =>
39✔
314
            a.AttributeClass is
39✔
315
            {
39✔
316
                ContainingNamespace:
39✔
317
                {
39✔
318
                    Name: "Schema",
39✔
319
                    ContainingNamespace:
39✔
320
                    {
39✔
321
                        Name: "DataAnnotations",
39✔
322
                        ContainingNamespace:
39✔
323
                        {
39✔
324
                            Name: "ComponentModel",
39✔
325
                            ContainingNamespace.Name: "System"
39✔
326
                        }
39✔
327
                    }
39✔
328
                }
39✔
329
            }
39✔
330
            && a.AttributeClass.Name == name
39✔
331
        );
39✔
332
    }
333

334
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
335
    {
336
        return attributes.Any(a => a.AttributeClass is
28✔
337
        {
28✔
338
            Name: "IgnorePropertyAttribute",
28✔
339
            ContainingNamespace:
28✔
340
            {
28✔
341
                Name: "Attributes",
28✔
342
                ContainingNamespace.Name: "FluentCommand"
28✔
343
            }
28✔
344
        });
28✔
345
    }
346

347
    private static bool HasJsonColumnAttribute(ImmutableArray<AttributeData> attributes)
348
    {
349
        return GetJsonColumnAttribute(attributes) != null;
14✔
350
    }
351

352
    private static AttributeData? GetJsonColumnAttribute(ImmutableArray<AttributeData> attributes)
353
    {
354
        return attributes.FirstOrDefault(a => a.AttributeClass is
25✔
355
        {
25✔
356
            Name: "JsonColumnAttribute",
25✔
357
            ContainingNamespace:
25✔
358
            {
25✔
359
                Name: "Attributes",
25✔
360
                ContainingNamespace.Name: "FluentCommand"
25✔
361
            }
25✔
362
        });
25✔
363
    }
364

365
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
366
    {
367
        return FindSchemaAttribute(attributes, "NotMappedAttribute") != null;
28✔
368
    }
369

370
    private static bool IsMappableProperty(IPropertySymbol propertySymbol, HashSet<string> classIgnored, HashSet<string>? jsonProperties = null)
371
    {
372
        var attributes = propertySymbol.GetAttributes();
19✔
373
        if (classIgnored.Contains(propertySymbol.Name) || HasIgnorePropertyAttribute(attributes) || IsNotMapped(attributes))
19!
374
            return false;
2✔
375

376
        return jsonProperties?.Contains(propertySymbol.Name) == true || HasJsonColumnAttribute(attributes) || IsSupportedType(propertySymbol.Type);
17!
377
    }
378

379
    private static string[] GetNamedStringArray(AttributeData attribute, string argName)
380
    {
381
        foreach (var namedArg in attribute.NamedArguments)
32✔
382
        {
383
            if (namedArg.Key != argName || namedArg.Value.Kind != TypedConstantKind.Array)
6✔
384
                continue;
385

386
            return namedArg.Value.Values
4✔
387
                .Select(static v => v.Value)
4✔
388
                .OfType<string>()
4✔
389
                .ToArray();
4✔
390
        }
391

392
        return [];
8✔
393
    }
394

395
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
396
    {
397
        var ignored = new HashSet<string>(StringComparer.Ordinal);
6✔
398

399
        foreach (var attr in attributes)
12!
400
        {
401
            if (attr.AttributeClass is not
×
402
                {
×
403
                    Name: "IgnorePropertyAttribute",
×
404
                    ContainingNamespace:
×
405
                    {
×
406
                        Name: "Attributes",
×
407
                        ContainingNamespace.Name: "FluentCommand"
×
408
                    }
×
409
                })
×
410
            {
411
                continue;
412
            }
413

414
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
415
            {
416
                ignored.Add(ctorName);
×
417
                continue;
×
418
            }
419

420
            foreach (var namedArg in attr.NamedArguments)
×
421
            {
422
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
423
                    ignored.Add(namedValue);
×
424
            }
425
        }
426

427
        return ignored;
6✔
428
    }
429

430
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
431
    {
432
        var properties = new Dictionary<string, IPropertySymbol>();
6✔
433
        var currentSymbol = targetSymbol;
6✔
434

435
        while (currentSymbol != null)
18✔
436
        {
437
            var propertySymbols = currentSymbol
12✔
438
                .GetMembers()
12✔
439
                .Where(m => m.Kind == SymbolKind.Property)
12✔
440
                .OfType<IPropertySymbol>()
12✔
441
                .Where(p => !p.IsIndexer
12✔
442
                    && !p.IsAbstract
12✔
443
                    && p.DeclaredAccessibility == Accessibility.Public
12✔
444
                    && !properties.ContainsKey(p.Name)
12✔
445
                );
12✔
446

447
            foreach (var propertySymbol in propertySymbols)
48✔
448
                properties.Add(propertySymbol.Name, propertySymbol);
12✔
449

450
            currentSymbol = currentSymbol.BaseType;
12✔
451
        }
452

453
        return properties.Values.ToList();
6✔
454
    }
455

456
    private static bool IsSupportedType(ITypeSymbol type)
457
    {
458
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
23!
459
            return IsSupportedType(namedType.TypeArguments[0]);
×
460

461
        if (type.TypeKind == TypeKind.Enum)
23!
462
            return true;
×
463

464
        switch (type.SpecialType)
23✔
465
        {
466
            case SpecialType.System_Boolean:
467
            case SpecialType.System_Byte:
468
            case SpecialType.System_Char:
469
            case SpecialType.System_Decimal:
470
            case SpecialType.System_Double:
471
            case SpecialType.System_Single:
472
            case SpecialType.System_Int16:
473
            case SpecialType.System_Int32:
474
            case SpecialType.System_Int64:
475
            case SpecialType.System_String:
476
                return true;
19✔
477
        }
478

479
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
4!
480
            return true;
×
481

482
        var fullName = type.ToDisplayString();
4✔
483

484
        return fullName is
4!
485
            "System.DateTime" or
4✔
486
            "System.DateTimeOffset" or
4✔
487
            "System.Guid" or
4✔
488
            "System.TimeSpan" or
4✔
489
            "System.DateOnly" or
4✔
490
            "System.TimeOnly" or
4✔
491
            "FluentCommand.ConcurrencyToken";
4✔
492
    }
493

494
    #endregion
495
}
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