Add DiagnosticAnalyzer for Arbiter.Mapping.Generators to validate mapper classes#382
Add DiagnosticAnalyzer for Arbiter.Mapping.Generators to validate mapper classes#382
Conversation
…per classes Introduces a Roslyn DiagnosticAnalyzer (separate from the source generator pipeline) that validates [GenerateMapper] classes at compile time, preserving generator cache: - ARB0001: Mapper class must be partial - ARB0002: Mapper class must inherit MapperProfile<TSource, TDestination> - ARB0003: ConfigureMapping contains unsupported statements (not mapping calls) - ARB0004: Destination property in Property() does not exist on destination type - ARB0005: Duplicate destination property mapping - ARB0006: Unrecognized mapping call pattern (not From/Value/Ignore) - ARB0007: Source property in From() does not exist on source type https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e
…roperty existence The Property() and From() lambdas are strongly-typed expressions, so the C# compiler itself will error if a referenced property doesn't exist on the type. These diagnostics were redundant. https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e
Warns when the generator auto-matches properties by name but the source type cannot be implicitly converted to the destination type. Without this, the generated assignment (destination.X = source.X) fails to compile with a confusing error in generated code. Uses Compilation.ClassifyConversion to check implicit convertibility. Skips properties with explicit custom mappings (From/Value/Ignore) and handles Nullable<T> unwrapping for same-underlying-type cases. https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e
Tests cover all diagnostic rules using CSharpCompilation with inline source: - ARB0001: class must be partial (positive and negative) - ARB0002: class must inherit MapperProfile (positive and negative) - ARB0003: unsupported statements in ConfigureMapping (variable declaration, if, foreach, return, non-mapping method calls, mixed valid/invalid) - ARB0004: auto-matched property type mismatch (incompatible types, implicit numeric conversion, nullable same-type, custom-mapped skip, multiple) - ARB0005: duplicate destination property mapping - ARB0006: unrecognized method on property chain - Valid mapper and no-attribute scenarios produce zero diagnostics https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e
There was a problem hiding this comment.
Pull request overview
Adds a standalone Roslyn DiagnosticAnalyzer to validate [GenerateMapper] mapper profiles at compile time (separate from the incremental generator pipeline), along with unit tests to exercise the analyzer diagnostics.
Changes:
- Introduce
MapperDiagnosticAnalyzerto report ARB0001–ARB0006 diagnostics for mapper shape andConfigureMappingcontents. - Add
MapperDiagnosticsdescriptor definitions backing those analyzer rules. - Add analyzer-focused tests that compile in-memory sources and run the analyzer, plus a test dependency on
Microsoft.CodeAnalysis.CSharp.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
test/Arbiter.Mapping.Tests/MapperDiagnosticAnalyzerTests.cs |
Adds in-memory compilation harness and tests validating analyzer diagnostics. |
test/Arbiter.Mapping.Tests/Arbiter.Mapping.Tests.csproj |
Adds Roslyn C# package reference needed to compile/run the analyzer in tests. |
src/Arbiter.Mapping.Generators/MapperDiagnostics.cs |
Defines diagnostic descriptors ARB0001–ARB0006. |
src/Arbiter.Mapping.Generators/MapperDiagnosticAnalyzer.cs |
Implements analyzer logic to validate [GenerateMapper] mapper classes and ConfigureMapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Handle nullable-to-nullable and nullable-to-non-nullable of the same underlying type | ||
| var sourceUnderlying = GetUnderlyingType(sourceTypeSymbol); | ||
| var destUnderlying = GetUnderlyingType(destTypeSymbol); | ||
|
|
||
| if (SymbolEqualityComparer.Default.Equals(sourceUnderlying, destUnderlying)) | ||
| continue; | ||
|
|
There was a problem hiding this comment.
The ARB0004 type-compatibility check currently skips cases where the source is nullable and the destination is non-nullable but has the same underlying type (e.g., int? -> int). That conversion is explicit in C# and the generator emits direct assignments for auto-mapped properties (see MapperWriter.WriteSourceExpression path-length==1 branch), so the generated code will not compile. Consider removing this underlying-type equality short-circuit for nullable->non-nullable (or only skipping when the destination is nullable / an implicit conversion exists).
| // Handle nullable-to-nullable and nullable-to-non-nullable of the same underlying type | |
| var sourceUnderlying = GetUnderlyingType(sourceTypeSymbol); | |
| var destUnderlying = GetUnderlyingType(destTypeSymbol); | |
| if (SymbolEqualityComparer.Default.Equals(sourceUnderlying, destUnderlying)) | |
| continue; |
| [Test] | ||
| public async Task ARB0004_NullableToNonNullableSameType_NoDiagnostic() | ||
| { | ||
| var source = """ | ||
| using Arbiter.Mapping; | ||
|
|
||
| namespace TestApp; | ||
|
|
||
| [GenerateMapper] | ||
| public partial class MyMapper : MapperProfile<Source, Dest> { } | ||
|
|
||
| public class Source { public int? Value { get; set; } } | ||
| public class Dest { public int Value { get; set; } } | ||
| """; | ||
|
|
||
| var diagnostics = await RunAnalyzerAsync(source); | ||
|
|
||
| diagnostics.Should().NotContain(d => d.Id == "ARB0004"); | ||
| } |
There was a problem hiding this comment.
This test asserts that ARB0004 should NOT be reported for nullable->non-nullable auto-matched properties (int? -> int). The generator currently emits direct property access for single-segment paths, so that assignment will not compile without a coalesce/cast. Once ARB0004 is fixed to flag explicit-only conversions, this expectation should be updated (or the generator updated to emit a safe conversion).
| /// <summary> | ||
| /// ARB0004: An auto-matched property has incompatible types between source and destination. | ||
| /// The generated assignment will not compile. | ||
| /// </summary> | ||
| public static readonly DiagnosticDescriptor PropertyTypeMismatch = new( | ||
| id: "ARB0004", | ||
| title: "Mapped property type mismatch", | ||
| messageFormat: "Property '{0}' cannot be auto-mapped: source type '{1}' is not implicitly convertible to destination type '{2}'", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true, | ||
| description: "An auto-matched property (matched by name) has incompatible types between source and destination. " + | ||
| "The generated code will produce a compile error. Use mapping.Property(d => d.Prop).From(...) to provide " + | ||
| "an explicit conversion, or .Ignore() to skip the property."); | ||
|
|
There was a problem hiding this comment.
The PR description lists diagnostics ARB0004 (destination property does not exist) and ARB0007 (source property in From() does not exist), but this PR’s implementation defines ARB0004 as a type-mismatch warning for auto-matched properties and does not implement any ARB0007 descriptor or source/destination existence checks. Please either update the PR description to match the shipped diagnostics, or implement the missing diagnostics so the analyzer behavior aligns with the stated contract.
- Add AnalyzerReleases/Shipped.md and Unshipped.md to satisfy RS2008 (release tracking required by EnforceExtendedAnalyzerRules) - Register release tracking files as AdditionalFiles in csproj - Add MA0051 pragma suppression to MapperDiagnosticAnalyzer.cs - Change test helper return type to List<Diagnostic> for assertion compat https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e
Introduces a Roslyn DiagnosticAnalyzer (separate from the source generator pipeline)
that validates [GenerateMapper] classes at compile time, preserving generator cache:
https://claude.ai/code/session_01SCR7bdSkrCSmNazN911Y2e