Skip to content

feat: Justification Property in Ignore Attributes and Diagnostic#2176

Draft
OlliMartin wants to merge 7 commits intoriok:mainfrom
OlliMartin:feature/2172-JustificationPropertyAndDiagnostic
Draft

feat: Justification Property in Ignore Attributes and Diagnostic#2176
OlliMartin wants to merge 7 commits intoriok:mainfrom
OlliMartin:feature/2172-JustificationPropertyAndDiagnostic

Conversation

@OlliMartin
Copy link

@OlliMartin OlliMartin commented Mar 6, 2026

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 Justification property to MapperIgnoreX attributes (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):

  • MapperIgnoreSourceAttribute
  • MapperIgnoreTargetAttribute
  • MapperIgnoreSourceValueAttribute
  • MapperIgnoreTargetValueAttribute

In this first draft I only included handling for MapperIgnoreSource and I would add the handling for the other attributes including additional tests once my approach is confirmed.

Fixes #2172

Open Points

  • I already adjusted the code (added a Justification) of three Verify/snapshot tests because they would write the new diagnostic otherwise. I do not see a good option to globally ignore the Hidden diagnostic, as the generator output is just passed along to Verify. I'm not fooling myself, presumably I am missing something.
  • Instead of adjusting all existing tests, I added the new diagnostic to the default ignore set (TestHelperOptions.Default). Please let me know if that is ok for you.
  • I'm not happy with the naming of WhereNotNullBy. My motivation was to align it with the naming of other LINQ methods such as GroupBy

Checklist

  • I did not use AI tools to generate this PR, or I have manually verified that the code is correct, optimal, and follows the project guidelines and architecture
  • I understand that low-quality, AI-generated PRs will be closed immediately without further explanation
  • The existing code style is followed
  • The commit message follows our guidelines
  • Performed a self-review of my code
  • Hard-to-understand areas of my code are commented
  • The documentation is updated (as applicable)
  • Unit tests are added/updated
  • Integration tests are added/updated (as applicable, especially if feature/bug depends on roslyn or framework version in use)

[MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)]
public partial B Map(A src);
[MapperIgnoreSource(nameof(A.BaseValueA)]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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🤷

@OlliMartin OlliMartin changed the title RFC | Feature/2172 Justification Property and Diagnostic RFC | Feature/2172 Justification Property in Ignore Attributes and Diagnostic Mar 7, 2026
@latonz latonz requested review from Copilot and latonz March 10, 2026 12:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Justification to MapperIgnoreSourceAttribute (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.

Comment on lines +262 to +268
foreach (var missingIgnoreSourceJustificationAttribute in ignoreSourceMemberAttributes.Where(attr => attr.Justification is null))
{
_diagnostics.ReportDiagnostic(
DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification,
configRef.Method,
missingIgnoreSourceJustificationAttribute.Source
);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +268
_diagnostics.ReportDiagnostic(
DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification,
configRef.Method,
missingIgnoreSourceJustificationAttribute.Source
);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +29
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,
};
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +16
private TestHelperOptions TestHelperOptions =>
TestHelperOptions.Default with
{
IgnoredDiagnostics = TestHelperOptions
.DefaultIgnoredDiagnostics.Except([DiagnosticDescriptors.MapperIgnoreAttributeMissingJustification])
.ToImmutableHashSet(),
AllowedDiagnosticSeverities = new HashSet<DiagnosticSeverity>() { DiagnosticSeverity.Hidden },
};
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +15
If Mapperly detects a mapping ignore without a Justification, `RMG096` is emitted.

The following attributes may raise this diagnostic:

- `MapperIgnoreSource`
- `MapperIgnoreTarget`
- `MapperIgnoreSourceValue`
- `MapperIgnoreTargetValue`
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +27

class SourceClass {
string IgnoredProperty { get; set; }
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 228 to 232
[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)]
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@latonz latonz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: lowercase justification

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same at several locations in this document.

- `MapperIgnoreSource`
- `MapperIgnoreTarget`
- `MapperIgnoreSourceValue`
- `MapperIgnoreTargetValue`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add MapperIgnoreAttribute.

Comment on lines +21 to +28
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
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I'd lowercase the justification.

configRef.Method,
missingIgnoreSourceJustificationAttribute.Source
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@latonz latonz added the enhancement New feature or request label Mar 10, 2026
@latonz latonz changed the title RFC | Feature/2172 Justification Property in Ignore Attributes and Diagnostic feat: Justification Property in Ignore Attributes and Diagnostic Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Justification Property in Ignore Attributes and Diagnostic

3 participants