Skip to content

Commit 029743c

Browse files
committed
Added possibility to declare properties as unmapped.
1 parent 3376d0a commit 029743c

File tree

7 files changed

+113
-7
lines changed

7 files changed

+113
-7
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@ Declare a method as a mapping method by decorating it with `MappingMethodAttribu
2222

2323
The analyzer will report errors for any property with a public setter that is not assigned within the mapping method.
2424

25-
In the example below the analyzer will report that the method `MapToModel` does not map the property `PersonModel.LastName`.
25+
You can exclude properties by adding one or more instances of `UnmappedPropertyAttribute` to the mapping method and passing the property name to it.
26+
27+
In the example below the analyzer will report that the method `MapToModel` does not map the property `PersonModel.LastName`. The property `PersonModel.Tag` is not reported because it is declared as unmapped property.
2628

2729
```csharp
2830
using ManualMappingGuard;
2931

3032
public class Person
3133
{
34+
public int Id { get; set; }
3235
public string FirstName { get; set; }
3336
public string LastName { get; set; }
37+
public string FullName => $"{FirstName} {LastName}";
3438
}
3539

3640
public class PersonModel
@@ -42,11 +46,18 @@ public class PersonModel
4246
public static class Mapper
4347
{
4448
[MappingMethod]
45-
public static PersonModel MapToModel(Person person)
49+
[UnmappedProperty(nameof(Person.Id))]
50+
public static Person Map(PersonModel model)
4651
{
47-
return new PersonModel { FirstName = person.FirstName };
52+
return new Person { FirstName = model.FirstName };
4853
}
4954
}
55+
56+
/*
57+
MSBuild Output:
58+
59+
Program.cs(19, 4): [MMG1001] Property LastName is not mapped.
60+
*/
5061
```
5162

5263
#### Limitations

src/Analyzers/MappingAnalyzer.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ private void OnMethodDeclaration(SyntaxNodeAnalysisContext context)
5454
mappedProperties.Add(targetProperty);
5555
}
5656

57-
var unmappedProperties = mappingTargetProperties.Except(mappedProperties, new RootPropertyEqualityComparer());
58-
foreach (var unmappedProperty in unmappedProperties.OrderBy(p => p.Name))
57+
var excludedPropertyNames = method.GetAttributes()
58+
.Where(a => a.AttributeClass.Name == "UnmappedPropertyAttribute")
59+
.Select(a => (string) a.ConstructorArguments[0].Value)
60+
.ToList();
61+
62+
var unmappedPropertyNames = mappingTargetProperties
63+
.Except(mappedProperties, new RootPropertyEqualityComparer())
64+
.Select(p => p.Name)
65+
.Except(excludedPropertyNames)
66+
.OrderBy(n => n);
67+
68+
foreach (var unmappedPropertyName in unmappedPropertyNames)
5969
{
60-
var diagnostic = Diagnostic.Create(Diagnostics.UnmappedProperty, location, unmappedProperty.Name);
70+
var diagnostic = Diagnostic.Create(Diagnostics.UnmappedProperty, location, unmappedPropertyName);
6171
context.ReportDiagnostic(diagnostic);
6272
}
6373
}

src/Common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
<PropertyGroup>
33
<LangVersion>8</LangVersion>
44
<Nullable>enable</Nullable>
5-
<WarningsAsErrors>CS8600;CS8602;CS8603;CS8604;CS8618</WarningsAsErrors>
5+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
66
</PropertyGroup>
77
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace ManualMappingGuard
4+
{
5+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
6+
public class UnmappedPropertyAttribute : Attribute
7+
{
8+
public string PropertyName { get; }
9+
10+
public UnmappedPropertyAttribute(string propertyName)
11+
{
12+
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
13+
}
14+
}
15+
}

src/Tests/Analyzers/MappingAnalyzerTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,61 @@ public Person Map()
137137
AssertNoUnmappedProperties(diagnostics);
138138
}
139139

140+
[Test]
141+
public async Task UnmappedProperty_Excluded_DoesNotReportMissingProperty()
142+
{
143+
var diagnostics = await Analyze(@"
144+
public class Person
145+
{
146+
public int Id { get; set; }
147+
public string FirstName { get; set; }
148+
public string LastName { get; set; }
149+
}
150+
151+
[MappingMethod]
152+
[UnmappedProperty(nameof(Person.Id))]
153+
public Person Map() => new Person { FirstName = ""Test"" };
154+
");
155+
156+
AssertUnmappedProperties(diagnostics, "LastName");
157+
}
158+
159+
[Test]
160+
public async Task UnmappedProperty_MultipleExcluded_DoesNotReportMissingProperties()
161+
{
162+
var diagnostics = await Analyze(@"
163+
public class Person
164+
{
165+
public const string IdPropertyName = ""Id"";
166+
167+
public int Id { get; set; }
168+
public string FirstName { get; set; }
169+
public string LastName { get; set; }
170+
}
171+
172+
[MappingMethod]
173+
[UnmappedProperty(Person.IdPropertyName)]
174+
[UnmappedProperty(""LastName"")]
175+
public Person Map() => new Person { FirstName = ""Test"" };
176+
");
177+
178+
AssertNoUnmappedProperties(diagnostics);
179+
}
180+
181+
[Test]
182+
public async Task NonExistingExcludedProperty_DoesNotFail()
183+
{
184+
var diagnostics = await Analyze(@"
185+
public class Person { }
186+
187+
[MappingMethod]
188+
[UnmappedProperty(""NotExisting"")]
189+
public Person Map() => new Person();
190+
");
191+
192+
Assert.That(diagnostics, Is.Empty);
193+
}
194+
140195
private void AssertMissingMappingTargetType(ImmutableArray<Diagnostic> diagnostics)
141196
{
142197
var expectedMessages = new[]

src/Tests/Analyzers/TestInfrastructure/CompilationUtility.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ private static IEnumerable<Type> GetReferencedTypes()
2020
yield return typeof(object);
2121
yield return typeof(MappingMethodAttribute);
2222
yield return typeof(MappingTargetAttribute);
23+
yield return typeof(UnmappedPropertyAttribute);
2324
}
2425

2526
private static IReadOnlyCollection<MetadataReference> CreateMetadataReferences()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using NUnit.Framework;
2+
3+
namespace ManualMappingGuard.Core
4+
{
5+
[TestFixture]
6+
public class UnmappedPropertyAttributeTests
7+
{
8+
[Test]
9+
public void NullPropertyName_ThrowsException()
10+
{
11+
Assert.That(() => new UnmappedPropertyAttribute(null!), Throws.ArgumentNullException);
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)