Skip to content

Commit b881ada

Browse files
authored
Generate map partial keyword diagnostics And Constructor missing Diagnostics (#39)
* Add `AOM105` diagnostic to enforce `partial` keyword requirement for classes in mapper generation * Add unit tests for `AOM105` diagnostic validation and adjust `MapperGenerator` syntax checking * Add `AOM207` diagnostic for missing public constructor or factory method validation; update generator logic, tests, and analyzer release notes * Fix nullable syntax reference usages and correct variable typo in `MapperGenerator` and related files * Add unit test for `AOM207` diagnostic validation in `NoConstructorTests` when no empty constructor avaliable
1 parent 5687c6a commit b881ada

10 files changed

Lines changed: 268 additions & 52 deletions

File tree

src/AotObjectMapper.Mapper/AOMDiagnostics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public static class DiagnosticCategories
178178
title: "No accessible constructor or factory method",
179179
messageFormat: "No accessible constructor or factory method found for {0}",
180180
category: DiagnosticCategories.Configuration,
181-
defaultSeverity: DiagnosticSeverity.Error,
181+
defaultSeverity: DiagnosticSeverity.Error,
182182
isEnabledByDefault: true);
183183

184184
public const string IgnoredMemberDoesNotExistId = "AOM208";

src/AotObjectMapper.Mapper/AnalyzerReleases.Shipped.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### New Rules
1212

13-
Rule ID | Category | Severity | Notes
14-
-----------|----------|----------|---------------------------------------------------
15-
AOM105 | Usage | Error | Class must contain the partial keyword
13+
Rule ID | Category | Severity | Notes
14+
-----------|---------------|-------------|---------------------------------------------------
15+
AOM105 | Usage | Error | Class must contain the partial keyword
16+
AOM207 | Configuration | Error | No accessible constructor or factory method

src/AotObjectMapper.Mapper/AnalyzerReleases.Unshipped.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
AOM204 | Design | Warning | Potential recursive mapping detected
1515
AOM205 | Usage | Error | Mapper should only have one default FormatProvider
1616
AOM206 | Usage | Error | Mapper should only have one FormatProvider for type pair
17-
AOM207 | Configuration | Error | No accessible constructor or factory method
1817
AOM208 | Configuration | Warning | Ignored member does not exist
1918
AOM209 | Configuration | Error | Duplicate configuration for member
2019
AOM300 | TypeSafety | Warning | No map found for destination type

src/AotObjectMapper.Mapper/Extensions/ITypeSymbolExtensions.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ public static class ITypeSymbolExtensions
99
{
1010
extension(ITypeSymbol type)
1111
{
12-
1312
public IEnumerable<IPropertySymbol> GetAllReadableProperties()
1413
{
1514
var seen = new HashSet<IPropertySymbol>(SymbolEqualityComparer.Default);
@@ -79,25 +78,42 @@ SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Dec
7978
return false;
8079
}
8180

82-
public string BlankTypeConstructor(MethodGenerationInfo? info, out (string type, string argName)[] arguments)
81+
public bool TryGetBlankTypeConstructor(MethodGenerationInfo? info, out string ctorCode, out (string type, string argName)[] arguments)
8382
{
83+
if (type is not INamedTypeSymbol namedType)
84+
{
85+
arguments = [];
86+
ctorCode = string.Empty;
87+
return false;
88+
}
89+
8490
if (info?.FactoryMethod is not null)
8591
{
8692
if (info.FactoryMethod?.Parameters.Length is 1)
8793
{
8894
arguments = [new("global::AotObjectMapper.Abstractions.Models.MapperContext" ,"ctx")];
89-
return $"{info.FactoryMethod.Name}(ctx)";
95+
ctorCode = $"{info.FactoryMethod.Name}(ctx)";
96+
return true;
9097
}
9198
else
9299
{
93100
arguments = [];
94-
return $"{info.FactoryMethod.Name}()";
101+
ctorCode = $"{info.FactoryMethod!.Name}()";
102+
return true;
95103
}
96104

97105
}
98106

99107
arguments = [];
100-
return $"new global::{type.ToDisplayString()}() {{ {string.Join(" ", type.GetMembers().OfType<IPropertySymbol>().Where(p => p.SetMethod is not null && p.IsRequired).Select(x => $"{x.Name} = {(x.Type.IsReferenceType ? "null!" : "default")},"))} }}";
108+
109+
if (namedType.Constructors.Any(x => x.DeclaredAccessibility is Accessibility.Public && x.Parameters.Length is 0))
110+
{
111+
ctorCode = $"new global::{type.ToDisplayString()}() {{ {string.Join(" ", type.GetMembers().OfType<IPropertySymbol>().Where(p => p.SetMethod is not null && p.IsRequired).Select(x => $"{x.Name} = {(x.Type.IsReferenceType ? "null!" : "default")},"))} }}";
112+
return true;
113+
}
114+
115+
ctorCode = string.Empty;
116+
return false;
101117
}
102118
}
103119
}

src/AotObjectMapper.Mapper/Mapper/MapperGenerator.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ private static string GenerateMapperClass(Compilation compilation, ISymbol mappe
116116

117117
foreach (var mapAttr in mapAttributes)
118118
{
119-
var info = new MethodGenerationInfo((INamedTypeSymbol)mapper, mapAttr);
119+
var info = new MethodGenerationInfo(compilation, (INamedTypeSymbol)mapper, mapAttr);
120120

121121
methodGenInfos.Add(info);
122122
}
@@ -135,7 +135,7 @@ private static string GenerateMapperClass(Compilation compilation, ISymbol mappe
135135
isb.IndentLevel++;
136136
}
137137

138-
List<string> interfaces = [];
138+
List<string> interfaces = [];
139139

140140
foreach (var methodGenInfo in methodGenInfos)
141141
{
@@ -150,7 +150,7 @@ private static string GenerateMapperClass(Compilation compilation, ISymbol mappe
150150
{
151151
try
152152
{
153-
GenerateMapperMethod(compilation, isb, methodGenInfo);
153+
GenerateMapperMethod(compilation, context, isb, methodGenInfo);
154154
isb.AppendLine();
155155
}
156156
catch (Exception ex)
@@ -166,7 +166,7 @@ private static string GenerateMapperClass(Compilation compilation, ISymbol mappe
166166
{
167167
try
168168
{
169-
GeneratePopulationMethod(compilation, isb, methodGenInfo);
169+
GeneratePopulationMethod(compilation, context, isb, methodGenInfo);
170170
isb.AppendLine();
171171
}
172172
catch (Exception ex)
@@ -187,12 +187,22 @@ private static string GenerateMapperClass(Compilation compilation, ISymbol mappe
187187
return isb.ToString();
188188
}
189189

190-
private static void GenerateMapperMethod(Compilation compilation, IndentedStringBuilder isb, MethodGenerationInfo info)
190+
private static void GenerateMapperMethod(Compilation compilation, SourceProductionContext context, IndentedStringBuilder isb, MethodGenerationInfo info)
191191
{
192-
var propertyAssignments = info.GeneratePropertyAssignments(compilation).ToArray();
192+
var propertyAssignments = info.GeneratePropertyAssignments(compilation, context).ToArray();
193193

194194
GenerateMethodDocs(info, isb);
195195

196+
if (!info.DestinationType.TryGetBlankTypeConstructor(info, out var ctor, out var ctorArgs) && !info.DestinationType.IsAbstract && info.DestinationType.TypeKind is not TypeKind.Interface)
197+
{
198+
AttributeSyntax declaration = (AttributeSyntax)info.MapAttribute.ApplicationSyntaxReference!.GetSyntax();
199+
200+
var destTypeNode = declaration.DescendantNodes().OfType<IdentifierNameSyntax>().ElementAt(1);
201+
202+
context.ReportDiagnostic(Diagnostic.Create(AOMDiagnostics.AOM207_NoConstructor, destTypeNode.GetLocation(), info.DestinationType.Name));
203+
return;
204+
}
205+
196206
using (isb.IndentBlock($"public static global::{info.DestinationType.ToDisplayString()} Map(global::{info.SourceType.ToDisplayString()} src, global::AotObjectMapper.Abstractions.Models.MapperContextBase? ctx = null)"))
197207
{
198208
if (info.DestinationType.TypeKind is TypeKind.Interface || info.DestinationType.IsAbstract)
@@ -206,8 +216,6 @@ private static void GenerateMapperMethod(Compilation compilation, IndentedString
206216

207217
isb.AppendLine("ctx ??= new global::AotObjectMapper.Abstractions.Models.MapperContext();");
208218

209-
var ctor = info.DestinationType.BlankTypeConstructor(info, out var ctorArgs);
210-
211219
isb.AppendLine($"return ctx.GetOrMapObject<global::{info.SourceType.ToDisplayString()}, global::{info.DestinationType.ToDisplayString()}>(src, ctx, static ({(ctorArgs.Any() ? string.Join(", ", ctorArgs.Select(x => $"{x.type} {x.argName}")) : "")}) => {ctor}, Utils.Populate);");
212220
}
213221
else
@@ -218,7 +226,7 @@ private static void GenerateMapperMethod(Compilation compilation, IndentedString
218226
isb.AppendLine();
219227
isb.AppendLine("// Pre-Map Actions");
220228
isb.AppendLine("ctx.IncrementDepth();");
221-
isb.AppendLine($"var dest = {info.DestinationType.BlankTypeConstructor(info, out _)};");
229+
isb.AppendLine($"var dest = {ctor};");
222230

223231
foreach (var mapMethodInfo in info.PreMapMethods.OrderBy(x => x.Attribute.ConstructorArguments[0].Value))
224232
{
@@ -284,9 +292,9 @@ private static void GenerateMethodDocs(MethodGenerationInfo info, IndentedString
284292
}
285293
}
286294

287-
private static void GeneratePopulationMethod(Compilation compilation, IndentedStringBuilder isb , MethodGenerationInfo info)
295+
private static void GeneratePopulationMethod(Compilation compilation, SourceProductionContext context, IndentedStringBuilder isb , MethodGenerationInfo info)
288296
{
289-
var propertyAssignments = info.GeneratePropertyAssignments(compilation).ToArray();
297+
var propertyAssignments = info.GeneratePropertyAssignments(compilation, context).ToArray();
290298

291299
isb.AppendLine("/// Populates an existing object, Designed for internal use.");
292300
isb.AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");

src/AotObjectMapper.Mapper/MapperInfo/MethodGenerationInfo.cs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public sealed class MethodGenerationInfo
7676
public bool ThrowExceptionOnUnmappedEnum { get; }
7777

7878

79-
public MethodGenerationInfo(ITypeSymbol mapperType, AttributeData mapAttr)
79+
public MethodGenerationInfo(Compilation compilation, ITypeSymbol mapperType, AttributeData mapAttr)
8080
{
8181
MapperType = (INamedTypeSymbol)mapperType;
8282
SourceType = (INamedTypeSymbol)mapAttr.AttributeClass!.TypeArguments[0];
@@ -125,7 +125,7 @@ public MethodGenerationInfo(ITypeSymbol mapperType, AttributeData mapAttr)
125125
ForMemberMethods = UserDefinedMapperMethods.GetSymbolsWithSingleGenericAttribute(nameof(ForMemberAttribute<,>), SourceType, DestinationType).ToArray();
126126
}
127127

128-
public IEnumerable<(IPropertySymbol propertySymbol, string assignemnt)> GeneratePropertyAssignments(Compilation compilation)
128+
public IEnumerable<(IPropertySymbol propertySymbol, string assignemnt)> GeneratePropertyAssignments(Compilation compilation, SourceProductionContext context)
129129
{
130130
List<(IPropertySymbol propertySymbol, string assignemnt)> assignments = [];
131131

@@ -137,7 +137,7 @@ public MethodGenerationInfo(ITypeSymbol mapperType, AttributeData mapAttr)
137137
if (IgnoredMembers.Any(x => x.Equals(destProp.Name)))
138138
continue;
139139

140-
if(TryBuildAssignmentExpression(compilation, srcProp.Type ,destProp.Type, $"src.{srcProp.Name}", srcProp.NullableAnnotation is not NullableAnnotation.None, out var expression))
140+
if(TryBuildAssignmentExpression(compilation, context, srcProp.Type ,destProp.Type, $"src.{srcProp.Name}", srcProp.NullableAnnotation is not NullableAnnotation.None, out var expression))
141141
assignments.Add(new (destProp, expression));
142142
}
143143

@@ -166,7 +166,7 @@ public MethodGenerationInfo(ITypeSymbol mapperType, AttributeData mapAttr)
166166
return assignments;
167167
}
168168

169-
private bool TryBuildAssignmentExpression(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol destinationType, string sourceExpression, bool sourceIsNullable, out string assignmentExpression)
169+
private bool TryBuildAssignmentExpression(Compilation compilation, SourceProductionContext context, ITypeSymbol sourceType, ITypeSymbol destinationType, string sourceExpression, bool sourceIsNullable, out string assignmentExpression)
170170
{
171171
// exact type match
172172
if (sourceType.Equals(destinationType, SymbolEqualityComparer.Default))
@@ -185,8 +185,15 @@ private bool TryBuildAssignmentExpression(Compilation compilation, ITypeSymbol s
185185
{
186186
if (PreserveReferences)
187187
{
188-
var ctor = otherMapper.AttributeClass!.TypeArguments[2].BlankTypeConstructor(this, out var ctorArgs);
189-
assignmentExpression = $"ctx.GetOrMapObject<{otherMapper.AttributeClass!.TypeArguments[1].ToDisplayString()}, {otherMapper.AttributeClass!.TypeArguments[2].ToDisplayString()}>({sourceExpression}, ctx, static ({(ctorArgs.Any() ? string.Join(", ", ctorArgs.Select(x => $"{x.type} {x.argName}")) : "")}) => {ctor}, {otherMapper.AttributeClass!.TypeArguments[0].Name}.Utils.Populate)";
188+
if(otherMapper.AttributeClass!.TypeArguments[2].TryGetBlankTypeConstructor(this, out var ctor, out var ctorArgs))
189+
assignmentExpression = $"ctx.GetOrMapObject<{otherMapper.AttributeClass!.TypeArguments[1].ToDisplayString()}, {otherMapper.AttributeClass!.TypeArguments[2].ToDisplayString()}>({sourceExpression}, ctx, static ({(ctorArgs.Any() ? string.Join(", ", ctorArgs.Select(x => $"{x.type} {x.argName}")) : "")}) => {ctor}, {otherMapper.AttributeClass!.TypeArguments[0].Name}.Utils.Populate)";
190+
191+
else
192+
{
193+
context.ReportDiagnostic(Diagnostic.Create(AOMDiagnostics.AOM207_NoConstructor, MapAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), otherMapper.AttributeClass!.TypeArguments[1].Name));
194+
assignmentExpression = string.Empty;
195+
return false;
196+
}
190197
}
191198
else
192199
{
@@ -205,8 +212,15 @@ private bool TryBuildAssignmentExpression(Compilation compilation, ITypeSymbol s
205212

206213
if (PreserveReferences)
207214
{
208-
var ctor = method.destination.BlankTypeConstructor(this, out var ctorArgs);
209-
assignmentExpression = $"ctx.GetOrMapObject<global::{method.source.ToDisplayString()}, global::{method.destination.ToDisplayString()}>({sourceExpression}, ctx, static ({(ctorArgs.Any() ? string.Join(", ", ctorArgs.Select(x => $"{x.type} {x.argName}")) : "")}) => {ctor}, global::{MapperType.ToDisplayString()}.Utils.Populate)";
215+
if(method.destination.TryGetBlankTypeConstructor(this, out var ctor, out var ctorArgs))
216+
assignmentExpression = $"ctx.GetOrMapObject<global::{method.source.ToDisplayString()}, global::{method.destination.ToDisplayString()}>({sourceExpression}, ctx, static ({(ctorArgs.Any() ? string.Join(", ", ctorArgs.Select(x => $"{x.type} {x.argName}")) : "")}) => {ctor}, global::{MapperType.ToDisplayString()}.Utils.Populate)";
217+
218+
else
219+
{
220+
context.ReportDiagnostic(Diagnostic.Create(AOMDiagnostics.AOM207_NoConstructor, MapAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), destinationType.Name));
221+
assignmentExpression = string.Empty;
222+
return false;
223+
}
210224
}
211225
else
212226
{
@@ -230,7 +244,7 @@ private bool TryBuildAssignmentExpression(Compilation compilation, ITypeSymbol s
230244
// IEnumerable
231245
if(TryGetEnumerableInitializationInfo(sourceType, out var sourceElementType))
232246
if(TryGetEnumerableInitializationInfo(destinationType, out var destinationElementType))
233-
if (TryBuildAssignmentExpression(compilation, sourceElementType, destinationElementType, "x", destinationElementType.NullableAnnotation is not NullableAnnotation.None, out var elementExpression))
247+
if (TryBuildAssignmentExpression(compilation, context, sourceElementType, destinationElementType, "x", destinationElementType.NullableAnnotation is not NullableAnnotation.None, out var elementExpression))
234248
if(TryGetEnumerationInitialization(destinationType, sourceElementType, destinationElementType, sourceExpression, elementExpression, out string selectExpression))
235249
{
236250
assignmentExpression = $"{selectExpression}";
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using AotObjectMapper.Abstractions.Attributes;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Testing;
7+
using Microsoft.CodeAnalysis.Testing;
8+
9+
namespace AotObjectMapper.Mapper.Tests;
10+
11+
public abstract class AOMVerifierBase
12+
{
13+
public async static Task VerifyGeneratorDiagnosticsAsync(
14+
string code,
15+
IEnumerable<DiagnosticResult>? expectedDiagnostics = null,
16+
CancellationToken cancellationToken = default)
17+
{
18+
var test = new CSharpSourceGeneratorTest<MapperGenerator, DefaultVerifier>()
19+
{
20+
TestState =
21+
{
22+
Sources = { code, },
23+
},
24+
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
25+
};
26+
27+
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GenerateMapperAttribute).Assembly.Location));
28+
29+
if(expectedDiagnostics is not null)
30+
test.ExpectedDiagnostics.AddRange(expectedDiagnostics);
31+
32+
await test.RunAsync(cancellationToken);
33+
}
34+
}

0 commit comments

Comments
 (0)