feat: Justification Property in Ignore Attributes and Diagnostic#2176
feat: Justification Property in Ignore Attributes and Diagnostic#2176OlliMartin wants to merge 7 commits intoriok:mainfrom
Conversation
| [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] | ||
| public partial B Map(A src); | ||
| [MapperIgnoreSource(nameof(A.BaseValueA)] |
There was a problem hiding this comment.
I stumbled across this while going through the failing snapshot tests:
How/why does this work in the first place, I can't wrap my head around it - it is not valid C# syntax, right?
There is a missing ) here, it should be [MapperIgnoreSource(nameof(A.BaseValueA))]
There was a problem hiding this comment.
I think the way to go would be disabling the new diagnostic for all tests except where we explicitly want to test it. I think you should be able to control editorsettings configs in the tests using TestHelperOptions.AnalyzerConfigOptions
There was a problem hiding this comment.
Oops, my comment does not really answer your question but a question you raised in the PR description.
It sometimes still works, even if the syntax tree is not parsed 100% correct as the parsed parts may still be enough fot mapperly to work🤷
There was a problem hiding this comment.
Pull request overview
This draft PR introduces a Justification property to MapperIgnoreSourceAttribute and adds a new hidden analyzer diagnostic (RMG096) to encourage documenting ignored mappings, along with initial tests and documentation.
Changes:
- Add
JustificationtoMapperIgnoreSourceAttribute(public API update) and update affected snapshots/tests. - Add RMG096 diagnostic descriptor and emit it for
[MapperIgnoreSource]entries missing a justification. - Add initial test coverage and documentation for RMG096; adjust test helper defaults to ignore the new hidden diagnostic by default.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Riok.Mapperly.Tests/TestHelperOptions.cs | Adds default ignored diagnostic set including RMG096. |
| test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs | Updates a MapperIgnoreSource usage to include Justification. |
| test/Riok.Mapperly.Tests/Mapping/IncludeMappingConfigurationTest.cs | Updates a MapperIgnoreSource usage to include Justification. |
| test/Riok.Mapperly.Tests/Mapping/DerivedTypeTest.cs | Updates a MapperIgnoreSource usage to include Justification. |
| test/Riok.Mapperly.Tests/DocumentationDiagnostics/MapperIgnoreJustificationTests.cs | Adds new tests asserting RMG096 behavior for MapperIgnoreSource. |
| test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs | Updates public API snapshot for the new property. |
| src/Riok.Mapperly/Helpers/EnumerableExtensions.cs | Adds WhereNotNullBy helper used during configuration reading. |
| src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs | Introduces diagnostic descriptor RMG096. |
| src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs | Emits RMG096 when MapperIgnoreSource has no justification. |
| src/Riok.Mapperly/AnalyzerReleases.Shipped.md | Tracks RMG096 in analyzer release tracking file. |
| src/Riok.Mapperly.Abstractions/MapperIgnoreSourceAttribute.cs | Adds Justification property and XML docs. |
| docs/docs/configuration/analyzer-diagnostics/RMG096.mdx | Adds documentation page for RMG096. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| foreach (var missingIgnoreSourceJustificationAttribute in ignoreSourceMemberAttributes.Where(attr => attr.Justification is null)) | ||
| { | ||
| _diagnostics.ReportDiagnostic( | ||
| DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification, | ||
| configRef.Method, | ||
| missingIgnoreSourceJustificationAttribute.Source | ||
| ); |
There was a problem hiding this comment.
The new RMG096 diagnostic only checks attr.Justification is null. This allows Justification = "" or whitespace to pass even though it provides no documentation. Consider treating null/empty/whitespace as missing (eg string.IsNullOrWhiteSpace).
| _diagnostics.ReportDiagnostic( | ||
| DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification, | ||
| configRef.Method, | ||
| missingIgnoreSourceJustificationAttribute.Source | ||
| ); |
There was a problem hiding this comment.
RMG096 is reported on configRef.Method, so the diagnostic will point at the method declaration rather than the specific [MapperIgnoreSource(...)] attribute that is missing a justification. For better UX, consider carrying the attribute syntax location through configuration reading (eg via a small config type inheriting HasSyntaxReference) and reporting the diagnostic at that attribute location.
| RMG093 | Mapper | Error | Target type is not assignable to the included target type | ||
| RMG094 | Mapper | Error | Circular existing target mapping without setter detected | ||
| RMG095 | Mapper | Warning | Invalid MSBuild configuration option | ||
| RMG096 | Mapper | Hidden | A MapperIgnore* attribute does not specify a Justification No newline at end of file |
There was a problem hiding this comment.
This adds a brand-new analyzer rule (RMG096) to AnalyzerReleases.Shipped.md. New rules are typically tracked in AnalyzerReleases.Unshipped.md until they are actually released; otherwise release tracking will claim the rule was already shipped.
| public static readonly IReadOnlySet<DiagnosticDescriptor> DefaultIgnoredDiagnostics = new HashSet<DiagnosticDescriptor> | ||
| { | ||
| // ignore NoMemberMappings as a lot of tests use this for simplicity | ||
| DiagnosticDescriptors.NoMemberMappings, | ||
| // ignore missing Justification properties unless specifically tested for, | ||
| // in which case the diagnostic is removed before using the options. | ||
| DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification, | ||
| }; |
There was a problem hiding this comment.
DefaultIgnoredDiagnostics is backed by a mutable HashSet but exposed as a static shared IReadOnlySet. Because the underlying set can still be mutated (via casts or by keeping a reference), this can create test cross-talk and order-dependent failures. Prefer an immutable collection (eg ImmutableHashSet) or create a new set per TestHelperOptions instance.
| private TestHelperOptions TestHelperOptions => | ||
| TestHelperOptions.Default with | ||
| { | ||
| IgnoredDiagnostics = TestHelperOptions | ||
| .DefaultIgnoredDiagnostics.Except([DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification]) | ||
| .ToImmutableHashSet(), | ||
| AllowedDiagnosticSeverities = new HashSet<DiagnosticSeverity>() { DiagnosticSeverity.Hidden }, | ||
| }; |
There was a problem hiding this comment.
The property name TestHelperOptions shadows the TestHelperOptions type name, which makes the initializer hard to read and easy to misinterpret. Renaming this to something like Options/HelperOptions would improve clarity and avoid confusion in future edits.
| If Mapperly detects a mapping ignore without a Justification, `RMG096` is emitted. | ||
|
|
||
| The following attributes may raise this diagnostic: | ||
|
|
||
| - `MapperIgnoreSource` | ||
| - `MapperIgnoreTarget` | ||
| - `MapperIgnoreSourceValue` | ||
| - `MapperIgnoreTargetValue` |
There was a problem hiding this comment.
The documentation claims RMG096 may be raised by MapperIgnoreTarget / MapperIgnoreSourceValue / MapperIgnoreTargetValue, but the current implementation only reports it for MapperIgnoreSource (and only that attribute has Justification in this PR). Either narrow the doc to the currently supported attribute(s) or implement the diagnostic for the listed attributes before publishing this page.
|
|
||
| class SourceClass { | ||
| string IgnoredProperty { get; set; } | ||
| } |
There was a problem hiding this comment.
In the example, IgnoredProperty has no access modifier, so it's private by default and wouldn't be a mappable member in most scenarios. Marking it public (or using a public property in the sample) would make the example align with the intended use of ignore attributes.
| [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] | ||
| public partial B Map(A src); | ||
| [MapperIgnoreSource(nameof(A.BaseValueA)] | ||
| [MapperIgnoreSource(nameof(A.BaseValueA), Justification = "BaseValueA is ignored for testing purposes")] | ||
| [MapperIgnoreTarget(nameof(B.BaseValueB)] |
There was a problem hiding this comment.
The C# code embedded in the raw string appears to have malformed attribute syntax (missing a closing ) before ]) for [MapProperty(...] and [MapperIgnoreTarget(...]. Even if the generator can run with parse errors, this makes the test source harder to reason about and can hide real failures; please fix the attribute syntax in the test snippet.
latonz
left a comment
There was a problem hiding this comment.
Thank you for this contribution 🥳 I added my feedback.
| @@ -0,0 +1,30 @@ | |||
| --- | |||
| sidebar_label: RMG096 | |||
| description: 'Mapperly analyzer diagnostic RMG096 — Ignored mapping is missing Justification' | |||
There was a problem hiding this comment.
Nitpick: lowercase justification
There was a problem hiding this comment.
Same at several locations in this document.
| - `MapperIgnoreSource` | ||
| - `MapperIgnoreTarget` | ||
| - `MapperIgnoreSourceValue` | ||
| - `MapperIgnoreTargetValue` |
There was a problem hiding this comment.
We should add MapperIgnoreAttribute.
| public static IEnumerable<TItem> WhereNotNullBy<TItem, TSelector>(this IEnumerable<TItem> enumerable, Func<TItem, TSelector?> selector) | ||
| where TSelector : notnull | ||
| { | ||
| #nullable disable | ||
| return enumerable.Where(x => selector(x) != null); | ||
| #nullable restore | ||
| } | ||
|
|
There was a problem hiding this comment.
What is the benefit of this new extension method? The WhereNotNull doesn't accept a parameter and ensures the source enumerable is notnull-annotated after calling the method. This does not work the same with a selector 🤔
| return MapperConfiguration.Members; | ||
| } | ||
|
|
||
| foreach (var missingIgnoreSourceJustificationAttribute in ignoreSourceMemberAttributes.Where(attr => attr.Justification is null)) |
There was a problem hiding this comment.
When adding support for the other attributes I'd move this into its own method and call it just next to resolving the attributes.
|
|
||
| public static readonly DiagnosticDescriptor MapperIgnoreAttributeMissingJustification = new( | ||
| "RMG096", | ||
| "Ignored mapping is missing Justification", |
There was a problem hiding this comment.
nitpick: I'd lowercase the justification.
| configRef.Method, | ||
| missingIgnoreSourceJustificationAttribute.Source | ||
| ); | ||
| } |
There was a problem hiding this comment.
Add a help link for the added docs, see other diagnostics on how to do it.
| [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] | ||
| public partial B Map(A src); | ||
| [MapperIgnoreSource(nameof(A.BaseValueA)] |
There was a problem hiding this comment.
I think the way to go would be disabling the new diagnostic for all tests except where we explicitly want to test it. I think you should be able to control editorsettings configs in the tests using TestHelperOptions.AnalyzerConfigOptions
Justification Property in Ignore Attributes and Diagnostic
Note: This Pull Request is a draft/request for comments. I would like to make sure my changes go into the right direction before developing something that goes against the architecture.
Description
This Pull Request aims to address the feature request of #2172.
With the addition of the
Justificationproperty toMapperIgnoreXattributes (more on that below) it is possible to implement a diagnostic to enforce developers to document their decision why a mapping should be ignored.After reviewing the code I identified the following attributes to be relevant for the diagnostic (please correct me if that's incorrect, not complete):
MapperIgnoreSourceAttributeMapperIgnoreTargetAttributeMapperIgnoreSourceValueAttributeMapperIgnoreTargetValueAttributeIn this first draft I only included handling for
MapperIgnoreSourceand I would add the handling for the other attributes including additional tests once my approach is confirmed.Fixes #2172
Open Points
Verify/snapshot tests because they would write the new diagnostic otherwise. I do not see a good option to globally ignore theHiddendiagnostic, as the generator output is just passed along to Verify. I'm not fooling myself, presumably I am missing something.TestHelperOptions.Default). Please let me know if that is ok for you.WhereNotNullBy. My motivation was to align it with the naming of other LINQ methods such asGroupByChecklist