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

loresoft / FluentCommand / 26620734448

29 May 2026 05:53AM UTC coverage: 54.913% (-0.9%) from 55.778%
26620734448

push

github

pwelter34
Validate JsonColumn and use fully-qualified types

1412 of 3414 branches covered (41.36%)

Branch coverage included in aggregate %.

13 of 102 new or added lines in 4 files covered. (12.75%)

12 existing lines in 3 files now uncovered.

4478 of 7312 relevant lines covered (61.24%)

306.51 hits per line

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

0.0
/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(
×
13
            DiagnosticDescriptors.NoMatchingConstructor,
×
14
            DiagnosticDescriptors.ConstructorParameterNotMatched,
×
15
            DiagnosticDescriptors.NoMappableProperties,
×
16
            DiagnosticDescriptors.UnsupportedPropertyType,
×
17
            DiagnosticDescriptors.InvalidGenerateReaderArgument,
×
NEW
18
            DiagnosticDescriptors.TableAttributeOnInvalidType,
×
NEW
19
            DiagnosticDescriptors.InvalidJsonColumnOptionsProvider,
×
NEW
20
            DiagnosticDescriptors.InvalidJsonColumnSerializerContext
×
UNCOV
21
        );
×
22

23
    public override void Initialize(AnalysisContext context)
24
    {
25
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
×
26
        context.EnableConcurrentExecution();
×
27

28
        context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
×
29
    }
×
30

31
    private static void AnalyzeNamedType(SymbolAnalysisContext context)
32
    {
33
        if (context.Symbol is not INamedTypeSymbol typeSymbol)
×
34
            return;
×
35

36
        var attributes = typeSymbol.GetAttributes();
×
37

38
        // Check [Table] attribute path
39
        var tableAttribute = FindSchemaAttribute(attributes, "TableAttribute");
×
40
        if (tableAttribute != null)
×
41
        {
42
            if (typeSymbol.IsStatic || typeSymbol.IsAbstract)
×
43
            {
44
                var modifier = typeSymbol.IsStatic ? "static" : "abstract";
×
45
                context.ReportDiagnostic(Diagnostic.Create(
×
46
                    DiagnosticDescriptors.TableAttributeOnInvalidType,
×
47
                    tableAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation()
×
48
                        ?? typeSymbol.Locations.FirstOrDefault() ?? Location.None,
×
49
                    typeSymbol.Name,
×
50
                    modifier));
×
51
            }
52
            else
53
            {
54
                AnalyzeEntityType(context, typeSymbol);
×
55
            }
56
        }
57

58
        // Check [GenerateReader] attribute path
59
        foreach (var attr in attributes)
×
60
        {
61
            if (!IsGenerateReaderAttribute(attr))
×
62
                continue;
63

64
            if (attr.ConstructorArguments.Length != 1 ||
×
65
                attr.ConstructorArguments[0].Value is not INamedTypeSymbol targetSymbol)
×
66
            {
67
                context.ReportDiagnostic(Diagnostic.Create(
×
68
                    DiagnosticDescriptors.InvalidGenerateReaderArgument,
×
69
                    attr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation()
×
70
                        ?? typeSymbol.Locations.FirstOrDefault() ?? Location.None,
×
71
                    typeSymbol.Name));
×
72
                continue;
×
73
            }
74

75
            AnalyzeEntityType(context, targetSymbol);
×
76
        }
77
    }
×
78

79
    private static void AnalyzeEntityType(SymbolAnalysisContext context, INamedTypeSymbol targetSymbol)
80
    {
81
        var typeAttributes = targetSymbol.GetAttributes();
×
82
        var classIgnored = GetClassIgnoredProperties(typeAttributes);
×
83
        var propertySymbols = GetProperties(targetSymbol);
×
84

85
        var hasParameterlessCtor = targetSymbol.Constructors.Any(c => c.Parameters.Length == 0);
×
86

87
        // Count mappable properties
88
        var mappableProperties = propertySymbols
×
89
            .Where(p => IsMappableProperty(p, classIgnored))
×
90
            .ToList();
×
91

92
        // Report unsupported property types
93
        foreach (var prop in propertySymbols)
×
94
        {
95
            var propertyAttributes = prop.GetAttributes();
×
96

97
            if (classIgnored.Contains(prop.Name) || HasIgnorePropertyAttribute(propertyAttributes) || IsNotMapped(propertyAttributes))
×
98
                continue;
99

NEW
100
            var jsonColumn = GetJsonColumnAttribute(propertyAttributes);
×
NEW
101
            if (jsonColumn != null)
×
102
            {
NEW
103
                AnalyzeJsonColumnAttribute(context, prop, jsonColumn);
×
UNCOV
104
                continue;
×
105
            }
106

107
            if (!IsSupportedType(prop.Type))
×
108
            {
109
                context.ReportDiagnostic(Diagnostic.Create(
×
110
                    DiagnosticDescriptors.UnsupportedPropertyType,
×
111
                    prop.Locations.FirstOrDefault() ?? Location.None,
×
112
                    prop.Name,
×
113
                    targetSymbol.Name,
×
114
                    prop.Type.ToDisplayString()));
×
115
            }
116
        }
117

118
        // Report no mappable properties
119
        if (mappableProperties.Count == 0)
×
120
        {
121
            context.ReportDiagnostic(Diagnostic.Create(
×
122
                DiagnosticDescriptors.NoMappableProperties,
×
123
                targetSymbol.Locations.FirstOrDefault() ?? Location.None,
×
124
                targetSymbol.Name));
×
125
            return;
×
126
        }
127

128
        // Constructor mode analysis
129
        if (!hasParameterlessCtor)
×
130
        {
131
            var mappableCount = propertySymbols
×
132
                .Count(p => IsMappableProperty(p, classIgnored));
×
133

134
            var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
×
135

136
            if (constructor == null)
×
137
            {
138
                context.ReportDiagnostic(Diagnostic.Create(
×
139
                    DiagnosticDescriptors.NoMatchingConstructor,
×
140
                    targetSymbol.Locations.FirstOrDefault() ?? Location.None,
×
141
                    targetSymbol.Name,
×
142
                    mappableCount));
×
143
                return;
×
144
            }
145

146
            // Check for unmatched constructor parameters
147
            foreach (var parameter in constructor.Parameters)
×
148
            {
149
                var hasMatch = propertySymbols.Any(p =>
×
150
                    string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
×
151

152
                if (!hasMatch)
×
153
                {
154
                    context.ReportDiagnostic(Diagnostic.Create(
×
155
                        DiagnosticDescriptors.ConstructorParameterNotMatched,
×
156
                        parameter.Locations.FirstOrDefault() ?? constructor.Locations.FirstOrDefault() ?? Location.None,
×
157
                        parameter.Name,
×
158
                        targetSymbol.Name));
×
159
                }
160
            }
161
        }
162
    }
×
163

164
    private static void AnalyzeJsonColumnAttribute(SymbolAnalysisContext context, IPropertySymbol propertySymbol, AttributeData jsonColumn)
165
    {
NEW
166
        var location = jsonColumn.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation()
×
NEW
167
            ?? propertySymbol.Locations.FirstOrDefault()
×
NEW
168
            ?? Location.None;
×
169

NEW
170
        if (jsonColumn.ConstructorArguments.Length == 1
×
NEW
171
            && jsonColumn.ConstructorArguments[0].Value is INamedTypeSymbol optionsProviderType
×
NEW
172
            && !HasJsonSerializerOptionsProperty(optionsProviderType))
×
173
        {
NEW
174
            context.ReportDiagnostic(Diagnostic.Create(
×
NEW
175
                DiagnosticDescriptors.InvalidJsonColumnOptionsProvider,
×
NEW
176
                location,
×
NEW
177
                optionsProviderType.ToDisplayString()));
×
178
        }
179

NEW
180
        if (jsonColumn.ConstructorArguments.Length == 2
×
NEW
181
            && jsonColumn.ConstructorArguments[0].Value is INamedTypeSymbol serializerContextType
×
NEW
182
            && jsonColumn.ConstructorArguments[1].Value is string typeInfoPropertyName
×
NEW
183
            && (!DerivesFromJsonSerializerContext(serializerContextType) || !HasProperty(serializerContextType, typeInfoPropertyName)))
×
184
        {
NEW
185
            context.ReportDiagnostic(Diagnostic.Create(
×
NEW
186
                DiagnosticDescriptors.InvalidJsonColumnSerializerContext,
×
NEW
187
                location,
×
NEW
188
                serializerContextType.ToDisplayString(),
×
NEW
189
                typeInfoPropertyName));
×
190
        }
NEW
191
    }
×
192

193
    private static bool HasJsonSerializerOptionsProperty(INamedTypeSymbol typeSymbol)
194
    {
NEW
195
        return typeSymbol.GetMembers("Options")
×
NEW
196
            .OfType<IPropertySymbol>()
×
NEW
197
            .Any(p => p.IsStatic && IsJsonSerializerOptions(p.Type));
×
198
    }
199

200
    private static bool IsJsonSerializerOptions(ITypeSymbol typeSymbol)
201
    {
NEW
202
        return typeSymbol
×
NEW
203
            .WithNullableAnnotation(NullableAnnotation.NotAnnotated)
×
NEW
204
            .ToDisplayString() == "System.Text.Json.JsonSerializerOptions";
×
205
    }
206

207
    private static bool DerivesFromJsonSerializerContext(INamedTypeSymbol typeSymbol)
208
    {
NEW
209
        var current = typeSymbol;
×
NEW
210
        while (current != null)
×
211
        {
NEW
212
            if (current.ToDisplayString() == "System.Text.Json.Serialization.JsonSerializerContext")
×
NEW
213
                return true;
×
214

NEW
215
            current = current.BaseType;
×
216
        }
217

NEW
218
        return false;
×
219
    }
220

221
    private static bool HasProperty(INamedTypeSymbol typeSymbol, string propertyName)
222
    {
NEW
223
        return typeSymbol.GetMembers(propertyName).OfType<IPropertySymbol>().Any();
×
224
    }
225

226
    #region Attribute helpers (mirrors generator logic)
227

228
    private static bool IsGenerateReaderAttribute(AttributeData attr)
229
    {
230
        return attr.AttributeClass is
×
231
        {
×
232
            Name: "GenerateReaderAttribute",
×
233
            ContainingNamespace:
×
234
            {
×
235
                Name: "Attributes",
×
236
                ContainingNamespace.Name: "FluentCommand"
×
237
            }
×
238
        };
×
239
    }
240

241
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
242
    {
243
        return attributes.FirstOrDefault(a =>
×
244
            a.AttributeClass is
×
245
            {
×
246
                ContainingNamespace:
×
247
                {
×
248
                    Name: "Schema",
×
249
                    ContainingNamespace:
×
250
                    {
×
251
                        Name: "DataAnnotations",
×
252
                        ContainingNamespace:
×
253
                        {
×
254
                            Name: "ComponentModel",
×
255
                            ContainingNamespace.Name: "System"
×
256
                        }
×
257
                    }
×
258
                }
×
259
            }
×
260
            && a.AttributeClass.Name == name
×
261
        );
×
262
    }
263

264
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
265
    {
266
        return attributes.Any(a => a.AttributeClass is
×
267
        {
×
268
            Name: "IgnorePropertyAttribute",
×
269
            ContainingNamespace:
×
270
            {
×
271
                Name: "Attributes",
×
272
                ContainingNamespace.Name: "FluentCommand"
×
273
            }
×
274
        });
×
275
    }
276

277
    private static bool HasJsonColumnAttribute(ImmutableArray<AttributeData> attributes)
278
    {
NEW
279
        return GetJsonColumnAttribute(attributes) != null;
×
280
    }
281

282
    private static AttributeData? GetJsonColumnAttribute(ImmutableArray<AttributeData> attributes)
283
    {
NEW
284
        return attributes.FirstOrDefault(a => a.AttributeClass is
×
285
        {
×
286
            Name: "JsonColumnAttribute",
×
287
            ContainingNamespace:
×
288
            {
×
289
                Name: "Attributes",
×
290
                ContainingNamespace.Name: "FluentCommand"
×
291
            }
×
292
        });
×
293
    }
294

295
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
296
    {
297
        return FindSchemaAttribute(attributes, "NotMappedAttribute") != null;
×
298
    }
299

300
    private static bool IsMappableProperty(IPropertySymbol propertySymbol, HashSet<string> classIgnored)
301
    {
302
        var attributes = propertySymbol.GetAttributes();
×
303
        if (classIgnored.Contains(propertySymbol.Name) || HasIgnorePropertyAttribute(attributes) || IsNotMapped(attributes))
×
304
            return false;
×
305

306
        return HasJsonColumnAttribute(attributes) || IsSupportedType(propertySymbol.Type);
×
307
    }
308

309
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
310
    {
311
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
312

313
        foreach (var attr in attributes)
×
314
        {
315
            if (attr.AttributeClass is not
×
316
                {
×
317
                    Name: "IgnorePropertyAttribute",
×
318
                    ContainingNamespace:
×
319
                    {
×
320
                        Name: "Attributes",
×
321
                        ContainingNamespace.Name: "FluentCommand"
×
322
                    }
×
323
                })
×
324
            {
325
                continue;
326
            }
327

328
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
329
            {
330
                ignored.Add(ctorName);
×
331
                continue;
×
332
            }
333

334
            foreach (var namedArg in attr.NamedArguments)
×
335
            {
336
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
337
                    ignored.Add(namedValue);
×
338
            }
339
        }
340

341
        return ignored;
×
342
    }
343

344
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
345
    {
346
        var properties = new Dictionary<string, IPropertySymbol>();
×
347
        var currentSymbol = targetSymbol;
×
348

349
        while (currentSymbol != null)
×
350
        {
351
            var propertySymbols = currentSymbol
×
352
                .GetMembers()
×
353
                .Where(m => m.Kind == SymbolKind.Property)
×
354
                .OfType<IPropertySymbol>()
×
355
                .Where(p => !p.IsIndexer && !p.IsAbstract && p.DeclaredAccessibility == Accessibility.Public)
×
356
                .Where(p => !properties.ContainsKey(p.Name));
×
357

358
            foreach (var propertySymbol in propertySymbols)
×
359
                properties.Add(propertySymbol.Name, propertySymbol);
×
360

361
            currentSymbol = currentSymbol.BaseType;
×
362
        }
363

364
        return properties.Values.ToList();
×
365
    }
366

367
    private static bool IsSupportedType(ITypeSymbol type)
368
    {
369
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
370
            return IsSupportedType(namedType.TypeArguments[0]);
×
371

372
        if (type.TypeKind == TypeKind.Enum)
×
373
            return true;
×
374

375
        switch (type.SpecialType)
×
376
        {
377
            case SpecialType.System_Boolean:
378
            case SpecialType.System_Byte:
379
            case SpecialType.System_Char:
380
            case SpecialType.System_Decimal:
381
            case SpecialType.System_Double:
382
            case SpecialType.System_Single:
383
            case SpecialType.System_Int16:
384
            case SpecialType.System_Int32:
385
            case SpecialType.System_Int64:
386
            case SpecialType.System_String:
387
                return true;
×
388
        }
389

390
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
391
            return true;
×
392

393
        var fullName = type.ToDisplayString();
×
394
        return fullName is
×
395
            "System.DateTime" or
×
396
            "System.DateTimeOffset" or
×
397
            "System.Guid" or
×
398
            "System.TimeSpan" or
×
399
            "System.DateOnly" or
×
400
            "System.TimeOnly" or
×
401
            "FluentCommand.ConcurrencyToken";
×
402
    }
403

404
    #endregion
405
}
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