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

loresoft / FluentCommand / 26594986582

28 May 2026 06:28PM UTC coverage: 55.553%. First build
26594986582

push

github

pwelter34
Move JSON support, add docs and examples

1358 of 3215 branches covered (42.24%)

Branch coverage included in aggregate %.

103 of 234 new or added lines in 9 files covered. (44.02%)

4389 of 7130 relevant lines covered (61.56%)

312.88 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,
×
18
            DiagnosticDescriptors.TableAttributeOnInvalidType
×
19
        );
×
20

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

26
        context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
×
27
    }
×
28

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

34
        var attributes = typeSymbol.GetAttributes();
×
35

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

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

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

73
            AnalyzeEntityType(context, targetSymbol);
×
74
        }
75
    }
×
76

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

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

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

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

NEW
95
            if (classIgnored.Contains(prop.Name) || HasIgnorePropertyAttribute(propertyAttributes) || IsNotMapped(propertyAttributes))
×
96
                continue;
97

NEW
98
            if (HasJsonColumnAttribute(propertyAttributes))
×
99
                continue;
100

101
            if (!IsSupportedType(prop.Type))
×
102
            {
103
                context.ReportDiagnostic(Diagnostic.Create(
×
104
                    DiagnosticDescriptors.UnsupportedPropertyType,
×
105
                    prop.Locations.FirstOrDefault() ?? Location.None,
×
106
                    prop.Name,
×
107
                    targetSymbol.Name,
×
108
                    prop.Type.ToDisplayString()));
×
109
            }
110
        }
111

112
        // Report no mappable properties
113
        if (mappableProperties.Count == 0)
×
114
        {
115
            context.ReportDiagnostic(Diagnostic.Create(
×
116
                DiagnosticDescriptors.NoMappableProperties,
×
117
                targetSymbol.Locations.FirstOrDefault() ?? Location.None,
×
118
                targetSymbol.Name));
×
119
            return;
×
120
        }
121

122
        // Constructor mode analysis
123
        if (!hasParameterlessCtor)
×
124
        {
125
            var mappableCount = propertySymbols
×
NEW
126
                .Count(p => IsMappableProperty(p, classIgnored));
×
127

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

130
            if (constructor == null)
×
131
            {
132
                context.ReportDiagnostic(Diagnostic.Create(
×
133
                    DiagnosticDescriptors.NoMatchingConstructor,
×
134
                    targetSymbol.Locations.FirstOrDefault() ?? Location.None,
×
135
                    targetSymbol.Name,
×
136
                    mappableCount));
×
137
                return;
×
138
            }
139

140
            // Check for unmatched constructor parameters
141
            foreach (var parameter in constructor.Parameters)
×
142
            {
143
                var hasMatch = propertySymbols.Any(p =>
×
144
                    string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
×
145

146
                if (!hasMatch)
×
147
                {
148
                    context.ReportDiagnostic(Diagnostic.Create(
×
149
                        DiagnosticDescriptors.ConstructorParameterNotMatched,
×
150
                        parameter.Locations.FirstOrDefault() ?? constructor.Locations.FirstOrDefault() ?? Location.None,
×
151
                        parameter.Name,
×
152
                        targetSymbol.Name));
×
153
                }
154
            }
155
        }
156
    }
×
157

158
    #region Attribute helpers (mirrors generator logic)
159

160
    private static bool IsGenerateReaderAttribute(AttributeData attr)
161
    {
162
        return attr.AttributeClass is
×
163
        {
×
164
            Name: "GenerateReaderAttribute",
×
165
            ContainingNamespace:
×
166
            {
×
167
                Name: "Attributes",
×
168
                ContainingNamespace.Name: "FluentCommand"
×
169
            }
×
170
        };
×
171
    }
172

173
    private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
174
    {
175
        return attributes.FirstOrDefault(a =>
×
176
            a.AttributeClass is
×
177
            {
×
178
                ContainingNamespace:
×
179
                {
×
180
                    Name: "Schema",
×
181
                    ContainingNamespace:
×
182
                    {
×
183
                        Name: "DataAnnotations",
×
184
                        ContainingNamespace:
×
185
                        {
×
186
                            Name: "ComponentModel",
×
187
                            ContainingNamespace.Name: "System"
×
188
                        }
×
189
                    }
×
190
                }
×
191
            }
×
192
            && a.AttributeClass.Name == name
×
193
        );
×
194
    }
195

196
    private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
197
    {
198
        return attributes.Any(a => a.AttributeClass is
×
199
        {
×
200
            Name: "IgnorePropertyAttribute",
×
201
            ContainingNamespace:
×
202
            {
×
203
                Name: "Attributes",
×
204
                ContainingNamespace.Name: "FluentCommand"
×
205
            }
×
206
        });
×
207
    }
208

209
    private static bool HasJsonColumnAttribute(ImmutableArray<AttributeData> attributes)
210
    {
NEW
211
        return attributes.Any(a => a.AttributeClass is
×
NEW
212
        {
×
NEW
213
            Name: "JsonColumnAttribute",
×
NEW
214
            ContainingNamespace:
×
NEW
215
            {
×
NEW
216
                Name: "Attributes",
×
NEW
217
                ContainingNamespace.Name: "FluentCommand"
×
NEW
218
            }
×
NEW
219
        });
×
220
    }
221

222
    private static bool IsNotMapped(ImmutableArray<AttributeData> attributes)
223
    {
NEW
224
        return FindSchemaAttribute(attributes, "NotMappedAttribute") != null;
×
225
    }
226

227
    private static bool IsMappableProperty(IPropertySymbol propertySymbol, HashSet<string> classIgnored)
228
    {
NEW
229
        var attributes = propertySymbol.GetAttributes();
×
NEW
230
        if (classIgnored.Contains(propertySymbol.Name) || HasIgnorePropertyAttribute(attributes) || IsNotMapped(attributes))
×
NEW
231
            return false;
×
232

NEW
233
        return HasJsonColumnAttribute(attributes) || IsSupportedType(propertySymbol.Type);
×
234
    }
235

236
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
237
    {
238
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
239

240
        foreach (var attr in attributes)
×
241
        {
242
            if (attr.AttributeClass is not
×
243
                {
×
244
                    Name: "IgnorePropertyAttribute",
×
245
                    ContainingNamespace:
×
246
                    {
×
247
                        Name: "Attributes",
×
248
                        ContainingNamespace.Name: "FluentCommand"
×
249
                    }
×
250
                })
×
251
            {
252
                continue;
253
            }
254

255
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
256
            {
257
                ignored.Add(ctorName);
×
258
                continue;
×
259
            }
260

261
            foreach (var namedArg in attr.NamedArguments)
×
262
            {
263
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
264
                    ignored.Add(namedValue);
×
265
            }
266
        }
267

268
        return ignored;
×
269
    }
270

271
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
272
    {
273
        var properties = new Dictionary<string, IPropertySymbol>();
×
274
        var currentSymbol = targetSymbol;
×
275

276
        while (currentSymbol != null)
×
277
        {
278
            var propertySymbols = currentSymbol
×
279
                .GetMembers()
×
280
                .Where(m => m.Kind == SymbolKind.Property)
×
281
                .OfType<IPropertySymbol>()
×
282
                .Where(p => !p.IsIndexer && !p.IsAbstract && p.DeclaredAccessibility == Accessibility.Public)
×
283
                .Where(p => !properties.ContainsKey(p.Name));
×
284

285
            foreach (var propertySymbol in propertySymbols)
×
286
                properties.Add(propertySymbol.Name, propertySymbol);
×
287

288
            currentSymbol = currentSymbol.BaseType;
×
289
        }
290

291
        return properties.Values.ToList();
×
292
    }
293

294
    private static bool IsSupportedType(ITypeSymbol type)
295
    {
296
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
297
            return IsSupportedType(namedType.TypeArguments[0]);
×
298

299
        if (type.TypeKind == TypeKind.Enum)
×
300
            return true;
×
301

302
        switch (type.SpecialType)
×
303
        {
304
            case SpecialType.System_Boolean:
305
            case SpecialType.System_Byte:
306
            case SpecialType.System_Char:
307
            case SpecialType.System_Decimal:
308
            case SpecialType.System_Double:
309
            case SpecialType.System_Single:
310
            case SpecialType.System_Int16:
311
            case SpecialType.System_Int32:
312
            case SpecialType.System_Int64:
313
            case SpecialType.System_String:
314
                return true;
×
315
        }
316

317
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
318
            return true;
×
319

320
        var fullName = type.ToDisplayString();
×
321
        return fullName is
×
322
            "System.DateTime" or
×
323
            "System.DateTimeOffset" or
×
324
            "System.Guid" or
×
325
            "System.TimeSpan" or
×
326
            "System.DateOnly" or
×
327
            "System.TimeOnly" or
×
328
            "FluentCommand.ConcurrencyToken";
×
329
    }
330

331
    #endregion
332
}
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