• 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/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
{
NEW
11
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
×
NEW
12
        ImmutableArray.Create(
×
NEW
13
            DiagnosticDescriptors.NoMatchingConstructor,
×
NEW
14
            DiagnosticDescriptors.ConstructorParameterNotMatched,
×
NEW
15
            DiagnosticDescriptors.NoMappableProperties,
×
NEW
16
            DiagnosticDescriptors.UnsupportedPropertyType,
×
NEW
17
            DiagnosticDescriptors.InvalidGenerateReaderArgument,
×
NEW
18
            DiagnosticDescriptors.TableAttributeOnInvalidType
×
NEW
19
        );
×
20

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

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

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

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

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

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

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

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

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

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

85
        // Count mappable properties
NEW
86
        var mappableProperties = propertySymbols
×
NEW
87
            .Where(p => !classIgnored.Contains(p.Name)
×
NEW
88
                        && !HasIgnorePropertyAttribute(p.GetAttributes())
×
NEW
89
                        && IsSupportedType(p.Type))
×
NEW
90
            .ToList();
×
91

92
        // Report unsupported property types
NEW
93
        foreach (var prop in propertySymbols)
×
94
        {
NEW
95
            if (classIgnored.Contains(prop.Name) || HasIgnorePropertyAttribute(prop.GetAttributes()))
×
96
                continue;
97

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

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

119
        // Constructor mode analysis
NEW
120
        if (!hasParameterlessCtor)
×
121
        {
NEW
122
            var mappableCount = propertySymbols
×
NEW
123
                .Count(p => !classIgnored.Contains(p.Name) && !HasIgnorePropertyAttribute(p.GetAttributes()));
×
124

NEW
125
            var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
×
126

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

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

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

155
    #region Attribute helpers (mirrors generator logic)
156

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

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

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

206
    private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
207
    {
NEW
208
        var ignored = new HashSet<string>(StringComparer.Ordinal);
×
209

NEW
210
        foreach (var attr in attributes)
×
211
        {
NEW
212
            if (attr.AttributeClass is not
×
NEW
213
                {
×
NEW
214
                    Name: "IgnorePropertyAttribute",
×
NEW
215
                    ContainingNamespace:
×
NEW
216
                    {
×
NEW
217
                        Name: "Attributes",
×
NEW
218
                        ContainingNamespace.Name: "FluentCommand"
×
NEW
219
                    }
×
NEW
220
                })
×
221
            {
222
                continue;
223
            }
224

NEW
225
            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
×
226
            {
NEW
227
                ignored.Add(ctorName);
×
NEW
228
                continue;
×
229
            }
230

NEW
231
            foreach (var namedArg in attr.NamedArguments)
×
232
            {
NEW
233
                if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
×
NEW
234
                    ignored.Add(namedValue);
×
235
            }
236
        }
237

NEW
238
        return ignored;
×
239
    }
240

241
    private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
242
    {
NEW
243
        var properties = new Dictionary<string, IPropertySymbol>();
×
NEW
244
        var currentSymbol = targetSymbol;
×
245

NEW
246
        while (currentSymbol != null)
×
247
        {
NEW
248
            var propertySymbols = currentSymbol
×
NEW
249
                .GetMembers()
×
NEW
250
                .Where(m => m.Kind == SymbolKind.Property)
×
NEW
251
                .OfType<IPropertySymbol>()
×
NEW
252
                .Where(p => !p.IsIndexer && !p.IsAbstract && p.DeclaredAccessibility == Accessibility.Public)
×
NEW
253
                .Where(p => !properties.ContainsKey(p.Name));
×
254

NEW
255
            foreach (var propertySymbol in propertySymbols)
×
NEW
256
                properties.Add(propertySymbol.Name, propertySymbol);
×
257

NEW
258
            currentSymbol = currentSymbol.BaseType;
×
259
        }
260

NEW
261
        return properties.Values.ToList();
×
262
    }
263

264
    private static bool IsSupportedType(ITypeSymbol type)
265
    {
NEW
266
        if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
×
NEW
267
            return IsSupportedType(namedType.TypeArguments[0]);
×
268

NEW
269
        if (type.TypeKind == TypeKind.Enum)
×
NEW
270
            return true;
×
271

NEW
272
        switch (type.SpecialType)
×
273
        {
274
            case SpecialType.System_Boolean:
275
            case SpecialType.System_Byte:
276
            case SpecialType.System_Char:
277
            case SpecialType.System_Decimal:
278
            case SpecialType.System_Double:
279
            case SpecialType.System_Single:
280
            case SpecialType.System_Int16:
281
            case SpecialType.System_Int32:
282
            case SpecialType.System_Int64:
283
            case SpecialType.System_String:
NEW
284
                return true;
×
285
        }
286

NEW
287
        if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
×
NEW
288
            return true;
×
289

NEW
290
        var fullName = type.ToDisplayString();
×
NEW
291
        return fullName is
×
NEW
292
            "System.DateTime" or
×
NEW
293
            "System.DateTimeOffset" or
×
NEW
294
            "System.Guid" or
×
NEW
295
            "System.TimeSpan" or
×
NEW
296
            "System.DateOnly" or
×
NEW
297
            "System.TimeOnly" or
×
NEW
298
            "FluentCommand.ConcurrencyToken";
×
299
    }
300

301
    #endregion
302
}
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