Skip to content

Commit 8bc564b

Browse files
committed
Add DataReaderFactory analyzer and refactor generator
1 parent 5c71893 commit 8bc564b

13 files changed

Lines changed: 964 additions & 188 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|-------------------------------
8+
FLC001 | Usage | Error | DataReaderFactoryAnalyzer
9+
FLC002 | Usage | Warning | DataReaderFactoryAnalyzer
10+
FLC003 | Usage | Warning | DataReaderFactoryAnalyzer
11+
FLC004 | Usage | Info | DataReaderFactoryAnalyzer
12+
FLC005 | Usage | Error | DataReaderFactoryAnalyzer
13+
FLC006 | Usage | Warning | DataReaderFactoryAnalyzer
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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
87+
.Where(p => !classIgnored.Contains(p.Name)
88+
&& !HasIgnorePropertyAttribute(p.GetAttributes())
89+
&& IsSupportedType(p.Type))
90+
.ToList();
91+
92+
// Report unsupported property types
93+
foreach (var prop in propertySymbols)
94+
{
95+
if (classIgnored.Contains(prop.Name) || HasIgnorePropertyAttribute(prop.GetAttributes()))
96+
continue;
97+
98+
if (!IsSupportedType(prop.Type))
99+
{
100+
context.ReportDiagnostic(Diagnostic.Create(
101+
DiagnosticDescriptors.UnsupportedPropertyType,
102+
prop.Locations.FirstOrDefault() ?? Location.None,
103+
prop.Name,
104+
targetSymbol.Name,
105+
prop.Type.ToDisplayString()));
106+
}
107+
}
108+
109+
// Report no mappable properties
110+
if (mappableProperties.Count == 0)
111+
{
112+
context.ReportDiagnostic(Diagnostic.Create(
113+
DiagnosticDescriptors.NoMappableProperties,
114+
targetSymbol.Locations.FirstOrDefault() ?? Location.None,
115+
targetSymbol.Name));
116+
return;
117+
}
118+
119+
// Constructor mode analysis
120+
if (!hasParameterlessCtor)
121+
{
122+
var mappableCount = propertySymbols
123+
.Count(p => !classIgnored.Contains(p.Name) && !HasIgnorePropertyAttribute(p.GetAttributes()));
124+
125+
var constructor = targetSymbol.Constructors.FirstOrDefault(c => c.Parameters.Length == mappableCount);
126+
127+
if (constructor == null)
128+
{
129+
context.ReportDiagnostic(Diagnostic.Create(
130+
DiagnosticDescriptors.NoMatchingConstructor,
131+
targetSymbol.Locations.FirstOrDefault() ?? Location.None,
132+
targetSymbol.Name,
133+
mappableCount));
134+
return;
135+
}
136+
137+
// Check for unmatched constructor parameters
138+
foreach (var parameter in constructor.Parameters)
139+
{
140+
var hasMatch = propertySymbols.Any(p =>
141+
string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
142+
143+
if (!hasMatch)
144+
{
145+
context.ReportDiagnostic(Diagnostic.Create(
146+
DiagnosticDescriptors.ConstructorParameterNotMatched,
147+
parameter.Locations.FirstOrDefault() ?? constructor.Locations.FirstOrDefault() ?? Location.None,
148+
parameter.Name,
149+
targetSymbol.Name));
150+
}
151+
}
152+
}
153+
}
154+
155+
#region Attribute helpers (mirrors generator logic)
156+
157+
private static bool IsGenerateReaderAttribute(AttributeData attr)
158+
{
159+
return attr.AttributeClass is
160+
{
161+
Name: "GenerateReaderAttribute",
162+
ContainingNamespace:
163+
{
164+
Name: "Attributes",
165+
ContainingNamespace.Name: "FluentCommand"
166+
}
167+
};
168+
}
169+
170+
private static AttributeData? FindSchemaAttribute(ImmutableArray<AttributeData> attributes, string name)
171+
{
172+
return attributes.FirstOrDefault(a =>
173+
a.AttributeClass is
174+
{
175+
ContainingNamespace:
176+
{
177+
Name: "Schema",
178+
ContainingNamespace:
179+
{
180+
Name: "DataAnnotations",
181+
ContainingNamespace:
182+
{
183+
Name: "ComponentModel",
184+
ContainingNamespace.Name: "System"
185+
}
186+
}
187+
}
188+
}
189+
&& a.AttributeClass.Name == name
190+
);
191+
}
192+
193+
private static bool HasIgnorePropertyAttribute(ImmutableArray<AttributeData> attributes)
194+
{
195+
return attributes.Any(a => a.AttributeClass is
196+
{
197+
Name: "IgnorePropertyAttribute",
198+
ContainingNamespace:
199+
{
200+
Name: "Attributes",
201+
ContainingNamespace.Name: "FluentCommand"
202+
}
203+
});
204+
}
205+
206+
private static HashSet<string> GetClassIgnoredProperties(ImmutableArray<AttributeData> attributes)
207+
{
208+
var ignored = new HashSet<string>(StringComparer.Ordinal);
209+
210+
foreach (var attr in attributes)
211+
{
212+
if (attr.AttributeClass is not
213+
{
214+
Name: "IgnorePropertyAttribute",
215+
ContainingNamespace:
216+
{
217+
Name: "Attributes",
218+
ContainingNamespace.Name: "FluentCommand"
219+
}
220+
})
221+
{
222+
continue;
223+
}
224+
225+
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string ctorName)
226+
{
227+
ignored.Add(ctorName);
228+
continue;
229+
}
230+
231+
foreach (var namedArg in attr.NamedArguments)
232+
{
233+
if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string namedValue)
234+
ignored.Add(namedValue);
235+
}
236+
}
237+
238+
return ignored;
239+
}
240+
241+
private static List<IPropertySymbol> GetProperties(INamedTypeSymbol targetSymbol)
242+
{
243+
var properties = new Dictionary<string, IPropertySymbol>();
244+
var currentSymbol = targetSymbol;
245+
246+
while (currentSymbol != null)
247+
{
248+
var propertySymbols = currentSymbol
249+
.GetMembers()
250+
.Where(m => m.Kind == SymbolKind.Property)
251+
.OfType<IPropertySymbol>()
252+
.Where(p => !p.IsIndexer && !p.IsAbstract && p.DeclaredAccessibility == Accessibility.Public)
253+
.Where(p => !properties.ContainsKey(p.Name));
254+
255+
foreach (var propertySymbol in propertySymbols)
256+
properties.Add(propertySymbol.Name, propertySymbol);
257+
258+
currentSymbol = currentSymbol.BaseType;
259+
}
260+
261+
return properties.Values.ToList();
262+
}
263+
264+
private static bool IsSupportedType(ITypeSymbol type)
265+
{
266+
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType)
267+
return IsSupportedType(namedType.TypeArguments[0]);
268+
269+
if (type.TypeKind == TypeKind.Enum)
270+
return true;
271+
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:
284+
return true;
285+
}
286+
287+
if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
288+
return true;
289+
290+
var fullName = type.ToDisplayString();
291+
return fullName is
292+
"System.DateTime" or
293+
"System.DateTimeOffset" or
294+
"System.Guid" or
295+
"System.TimeSpan" or
296+
"System.DateOnly" or
297+
"System.TimeOnly" or
298+
"FluentCommand.ConcurrencyToken";
299+
}
300+
301+
#endregion
302+
}

0 commit comments

Comments
 (0)