@@ -222,6 +222,15 @@ public interface IAutoMapper<in TSource, out TResult>
222222 isEnabledByDefault : true ,
223223 helpLinkUri : "https://github.com/Swevo/AutoMap.Generator#am006" ) ;
224224
225+ private static readonly DiagnosticDescriptor AM007 = new DiagnosticDescriptor (
226+ "AM007" ,
227+ "Reverse mapping could not be generated" ,
228+ "Reverse = true specified for mapping from '{0}' to '{1}', but no reverse properties could be generated" ,
229+ "AutoMap" ,
230+ DiagnosticSeverity . Warning ,
231+ isEnabledByDefault : true ,
232+ helpLinkUri : "https://github.com/Swevo/AutoMap.Generator#am007" ) ;
233+
225234 // Strict-mode variants (same codes, Error severity — used when [Map(Strict = true)])
226235 private static readonly DiagnosticDescriptor AM001_Strict = new DiagnosticDescriptor (
227236 "AM001" , "No properties mapped" ,
@@ -327,7 +336,7 @@ private static ImmutableArray<MappingInfo> TransformAttributes(
327336
328337 // When Reverse = true, also generate the opposite direction
329338 if ( reverse )
330- builder . Add ( BuildMappingInfo ( destSymbol , sourceSymbol , null , ctx . SemanticModel . Compilation , strict ) ) ;
339+ builder . Add ( BuildReverseMappingInfo ( sourceSymbol , destSymbol , ctx . SemanticModel . Compilation , strict ) ) ;
331340 }
332341
333342 return builder . ToImmutable ( ) ;
@@ -365,7 +374,8 @@ private static MappingInfo BuildMappingInfo(
365374 INamedTypeSymbol destSymbol ,
366375 string ? methodName ,
367376 Compilation compilation ,
368- bool isStrict = false )
377+ bool isStrict = false ,
378+ bool reverse = false )
369379 {
370380 var sourceFqn = sourceSymbol . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
371381 var destFqn = destSymbol . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
@@ -589,6 +599,8 @@ private static MappingInfo BuildMappingInfo(
589599 }
590600
591601 // AM001 moved to GenerateSource (needs all mappings to know if deferred props resolve)
602+ var matchedMembers = mappings . Count + ctorParams . Count ( p => p . SourcePropertyName != null ) ;
603+
592604 return new MappingInfo (
593605 sourceFqn , destFqn , effectiveMethod ,
594606 mappings . ToImmutable ( ) ,
@@ -597,7 +609,176 @@ private static MappingInfo BuildMappingInfo(
597609 sourceSymbol . IsValueType ,
598610 ctorParams ,
599611 trimStrings ,
600- isStrict ) ;
612+ isStrict ,
613+ reverse ,
614+ matchedMembers ) ;
615+ }
616+
617+ private static MappingInfo BuildReverseMappingInfo (
618+ INamedTypeSymbol originalSourceSymbol ,
619+ INamedTypeSymbol originalDestSymbol ,
620+ Compilation compilation ,
621+ bool isStrict = false )
622+ {
623+ var reverseSourceSymbol = originalDestSymbol ;
624+ var reverseDestSymbol = originalSourceSymbol ;
625+
626+ var sourceFqn = reverseSourceSymbol . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
627+ var destFqn = reverseDestSymbol . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
628+ var methodName = "To" + originalSourceSymbol . Name ;
629+
630+ bool trimStrings = HasAttribute ( reverseSourceSymbol , "AutoMap.TrimStringsAttribute" )
631+ || HasAttribute ( reverseDestSymbol , "AutoMap.TrimStringsAttribute" ) ;
632+
633+ var sourceProps = BuildReadablePropLookup ( reverseSourceSymbol ) ;
634+ var reverseLookup = new Dictionary < string , ( string SourcePropertyName , bool SkipReverse ) > ( StringComparer . OrdinalIgnoreCase ) ;
635+
636+ foreach ( var forwardDestProp in GetAllProperties ( originalDestSymbol ) )
637+ {
638+ if ( forwardDestProp . IsStatic || forwardDestProp . IsIndexer ) continue ;
639+ if ( forwardDestProp . GetMethod ? . DeclaredAccessibility != Accessibility . Public ) continue ;
640+
641+ string lookupName = forwardDestProp . Name ;
642+ bool skipReverse = false ;
643+
644+ foreach ( var a in forwardDestProp . GetAttributes ( ) )
645+ {
646+ var fqn = a . AttributeClass ? . ToDisplayString ( ) ;
647+ if ( fqn == "AutoMap.MapIgnoreAttribute" || fqn == "AutoMap.MapWithAttribute" )
648+ skipReverse = true ;
649+ else if ( fqn == "AutoMap.MapPropertyAttribute" && a . ConstructorArguments . Length > 0 )
650+ lookupName = a . ConstructorArguments [ 0 ] . Value as string ?? lookupName ;
651+ }
652+
653+ if ( ! reverseLookup . ContainsKey ( lookupName ) )
654+ reverseLookup [ lookupName ] = ( forwardDestProp . Name , skipReverse ) ;
655+ }
656+
657+ var mappings = ImmutableArray . CreateBuilder < PropertyMapping > ( ) ;
658+ var unresolvedProperties = ImmutableArray . CreateBuilder < UnresolvedProperty > ( ) ;
659+ var diagnostics = ImmutableArray . CreateBuilder < DiagnosticInfo > ( ) ;
660+
661+ foreach ( var destProp in GetAllProperties ( reverseDestSymbol ) )
662+ {
663+ if ( destProp . IsStatic || destProp . IsIndexer ) continue ;
664+
665+ var setter = destProp . SetMethod ;
666+ if ( setter == null || setter . DeclaredAccessibility != Accessibility . Public ) continue ;
667+
668+ if ( ! reverseLookup . TryGetValue ( destProp . Name , out var reverseInfo ) || reverseInfo . SkipReverse ) continue ;
669+ if ( ! sourceProps . TryGetValue ( reverseInfo . SourcePropertyName , out var srcProp ) ) continue ;
670+
671+ var csharp = compilation as Microsoft . CodeAnalysis . CSharp . CSharpCompilation ;
672+ bool typeCompatible ;
673+ if ( csharp != null )
674+ {
675+ var conv = csharp . ClassifyConversion ( srcProp . Type , destProp . Type ) ;
676+ typeCompatible = conv . IsIdentity || conv . IsImplicit ;
677+ }
678+ else
679+ {
680+ var srcTypeFqn = srcProp . Type . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) . TrimEnd ( '?' ) ;
681+ var destTypeFqn = destProp . Type . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) . TrimEnd ( '?' ) ;
682+ typeCompatible = srcTypeFqn == destTypeFqn ;
683+ }
684+
685+ if ( typeCompatible )
686+ {
687+ var finalExpr = trimStrings && IsStringType ( destProp . Type )
688+ ? $ "src.{ srcProp . Name } ?.Trim()"
689+ : null ;
690+ mappings . Add ( new PropertyMapping ( destProp . Name , srcProp . Name , finalExpr ) ) ;
691+ }
692+ else if ( srcProp . Type . TypeKind == TypeKind . Enum
693+ && destProp . Type . TypeKind == TypeKind . Enum
694+ && srcProp . Type is INamedTypeSymbol srcEnum
695+ && destProp . Type is INamedTypeSymbol destEnum )
696+ {
697+ var switchExpr = BuildEnumSwitchExpression (
698+ $ "src.{ srcProp . Name } ", srcEnum , destEnum ,
699+ destEnum . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ,
700+ diagnostics ) ;
701+ mappings . Add ( new PropertyMapping ( destProp . Name , srcProp . Name , switchExpr ) ) ;
702+ }
703+ else
704+ {
705+ var unresolved = BuildUnresolvedProperty ( destProp . Name , srcProp . Name , srcProp . Type , destProp . Type ) ;
706+ if ( unresolved != null )
707+ unresolvedProperties . Add ( unresolved ) ;
708+ }
709+ }
710+
711+ bool hasParamlessCtor = false ;
712+ foreach ( var ctor in reverseDestSymbol . Constructors )
713+ {
714+ if ( ! ctor . IsStatic && ctor . DeclaredAccessibility == Accessibility . Public
715+ && ctor . Parameters . Length == 0 )
716+ { hasParamlessCtor = true ; break ; }
717+ }
718+
719+ bool hasMapConstructorAttr = HasAttribute ( reverseDestSymbol , "AutoMap.MapConstructorAttribute" ) ;
720+ var ctorParams = ImmutableArray < CtorParamMapping > . Empty ;
721+
722+ if ( ! hasParamlessCtor || hasMapConstructorAttr )
723+ {
724+ IMethodSymbol ? bestCtor = null ;
725+ foreach ( var ctor in reverseDestSymbol . Constructors )
726+ {
727+ if ( ctor . IsStatic || ctor . DeclaredAccessibility != Accessibility . Public ) continue ;
728+ if ( bestCtor == null || ctor . Parameters . Length > bestCtor . Parameters . Length )
729+ bestCtor = ctor ;
730+ }
731+
732+ if ( bestCtor != null )
733+ {
734+ var ctorBuilder = ImmutableArray . CreateBuilder < CtorParamMapping > ( bestCtor . Parameters . Length ) ;
735+ var ctorParamNames = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
736+
737+ foreach ( var param in bestCtor . Parameters )
738+ {
739+ IPropertySymbol ? matchedProp = null ;
740+ if ( reverseLookup . TryGetValue ( param . Name , out var reverseInfo )
741+ && ! reverseInfo . SkipReverse )
742+ {
743+ sourceProps . TryGetValue ( reverseInfo . SourcePropertyName , out matchedProp ) ;
744+ }
745+
746+ if ( matchedProp == null )
747+ {
748+ diagnostics . Add ( new DiagnosticInfo ( "AM005" ,
749+ ImmutableArray . Create ( param . Name , reverseDestSymbol . Name , reverseSourceSymbol . Name ) ) ) ;
750+ ctorBuilder . Add ( new CtorParamMapping ( param . Name , null ) ) ;
751+ }
752+ else
753+ {
754+ ctorBuilder . Add ( new CtorParamMapping ( param . Name , matchedProp . Name ) ) ;
755+ ctorParamNames . Add ( matchedProp . Name ) ;
756+ }
757+ }
758+
759+ ctorParams = ctorBuilder . ToImmutable ( ) ;
760+
761+ var filtered = ImmutableArray . CreateBuilder < PropertyMapping > ( ) ;
762+ foreach ( var pm in mappings )
763+ if ( ! ctorParamNames . Contains ( pm . SourcePropertyName ) )
764+ filtered . Add ( pm ) ;
765+ mappings = filtered ;
766+ }
767+ }
768+
769+ var matchedMembers = mappings . Count + ctorParams . Count ( p => p . SourcePropertyName != null ) ;
770+
771+ return new MappingInfo (
772+ sourceFqn , destFqn , methodName ,
773+ mappings . ToImmutable ( ) ,
774+ unresolvedProperties . ToImmutable ( ) ,
775+ diagnostics . ToImmutable ( ) ,
776+ reverseSourceSymbol . IsValueType ,
777+ ctorParams ,
778+ trimStrings ,
779+ isStrict ,
780+ reverse : true ,
781+ matchedMembers : matchedMembers ) ;
601782 }
602783
603784 // ── Helpers ───────────────────────────────────────────────────────────────
@@ -700,6 +881,7 @@ private static void GenerateSource(
700881 foreach ( var m in mappings )
701882 {
702883 if ( string . IsNullOrEmpty ( m . SourceFqn ) ) continue ;
884+ if ( m . Mappings . Length == 0 && m . CtorParams . Length == 0 && m . UnresolvedProperties . Length == 0 ) continue ;
703885 if ( ! knownMappings . TryGetValue ( m . SourceFqn , out var destMap ) )
704886 knownMappings [ m . SourceFqn ] = destMap = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
705887 destMap [ m . DestFqn ] = m . MethodName ;
@@ -752,20 +934,29 @@ private static void GenerateSource(
752934 resolvedExtras [ m ] = extras ;
753935 }
754936
937+ foreach ( var m in mappings )
938+ {
939+ if ( ! m . Reverse || string . IsNullOrEmpty ( m . SourceFqn ) ) continue ;
940+ var extras = resolvedExtras . TryGetValue ( m , out var ex ) ? ex : null ;
941+ if ( m . MatchedMembers == 0 && ( extras == null || extras . Count == 0 ) )
942+ spc . ReportDiagnostic ( Diagnostic . Create ( AM007 , null ,
943+ SimpleName ( m . SourceFqn ) , SimpleName ( m . DestFqn ) ) ) ;
944+ }
945+
755946 // Report stored diagnostics (AM002, AM003, AM005)
756947 foreach ( var m in mappings )
757948 {
758949 foreach ( var d in m . Diagnostics )
759950 {
760- var descriptor = d . Id switch { "AM002" => AM002 , "AM003" => AM003 , "AM005" => AM005 , "AM006" => AM006 , _ => AM001 } ;
951+ var descriptor = d . Id switch { "AM002" => AM002 , "AM003" => AM003 , "AM005" => AM005 , "AM006" => AM006 , "AM007" => AM007 , _ => AM001 } ;
761952 spc . ReportDiagnostic ( Diagnostic . Create ( descriptor , null , d . Args . ToArray < object > ( ) ) ) ;
762953 }
763954 }
764955
765956 // AM001 — no properties at all (direct or resolved)
766957 foreach ( var m in mappings )
767958 {
768- if ( string . IsNullOrEmpty ( m . SourceFqn ) || m . Diagnostics . Length > 0 ) continue ;
959+ if ( m . Reverse || string . IsNullOrEmpty ( m . SourceFqn ) || m . Diagnostics . Length > 0 ) continue ;
769960 var extras = resolvedExtras . TryGetValue ( m , out var ex ) ? ex : null ;
770961 if ( m . Mappings . Length == 0 && m . CtorParams . Length == 0 && ( extras == null || extras . Count == 0 ) )
771962 {
@@ -1019,6 +1210,8 @@ internal sealed class MappingInfo
10191210 public ImmutableArray < CtorParamMapping > CtorParams { get ; }
10201211 public bool TrimStrings { get ; }
10211212 public bool IsStrict { get ; }
1213+ public bool Reverse { get ; }
1214+ public int MatchedMembers { get ; }
10221215
10231216 public bool UseConstructor => CtorParams . Length > 0 ;
10241217
@@ -1029,14 +1222,18 @@ public MappingInfo(string sourceFqn, string destFqn, string methodName,
10291222 bool isSourceValueType ,
10301223 ImmutableArray < CtorParamMapping > ctorParams = default ,
10311224 bool trimStrings = false ,
1032- bool isStrict = false )
1225+ bool isStrict = false ,
1226+ bool reverse = false ,
1227+ int matchedMembers = 0 )
10331228 {
10341229 SourceFqn = sourceFqn ; DestFqn = destFqn ; MethodName = methodName ;
10351230 Mappings = mappings ; UnresolvedProperties = unresolvedProperties ;
10361231 Diagnostics = diagnostics ; IsSourceValueType = isSourceValueType ;
10371232 CtorParams = ctorParams . IsDefault ? ImmutableArray < CtorParamMapping > . Empty : ctorParams ;
10381233 TrimStrings = trimStrings ;
10391234 IsStrict = isStrict ;
1235+ Reverse = reverse ;
1236+ MatchedMembers = matchedMembers ;
10401237 }
10411238}
10421239
0 commit comments