@@ -187,16 +187,26 @@ public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnum
187187 }
188188 else
189189 {
190- // Check if this type matches any mapped old types
191- var mappedOldNames = _nameMapper . MapFullTypeName ( newTypeName ) . ToList ( ) ;
192-
193- foreach ( var mappedName in mappedOldNames )
190+ // Check if any old type maps to this new type
191+ foreach ( var oldType in oldTypesList )
194192 {
195- if ( oldTypesByFullName . ContainsKey ( mappedName ) )
193+ var oldTypeName = oldType . FullName ?? oldType . Name ;
194+ var mappedNames = _nameMapper . MapFullTypeName ( oldTypeName ) . ToList ( ) ;
195+
196+ foreach ( var mappedName in mappedNames )
196197 {
197- foundMatch = true ;
198- break ;
198+ if ( string . Equals ( mappedName , newTypeName , StringComparison . Ordinal ) )
199+ {
200+ foundMatch = true ;
201+ _logger . LogDebug (
202+ "Found mapped type: {OldTypeName} -> {NewTypeName}" ,
203+ oldTypeName ,
204+ newTypeName ) ;
205+ break ;
206+ }
199207 }
208+
209+ if ( foundMatch ) break ;
200210 }
201211
202212 // Check for auto-mapping if enabled
@@ -298,8 +308,12 @@ public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnum
298308 var memberDifferences = CompareMembers ( oldType , mappedNewType ) . ToList ( ) ;
299309 differences . AddRange ( memberDifferences ) ;
300310
301- // Check for type-level changes
302- var typeDifference = _differenceCalculator . CalculateTypeChanges ( oldType , mappedNewType ) ;
311+ // Check for type-level changes with signature equivalence
312+ // For mapped types, check if the old type name maps to the new type name
313+ var mappedOldTypeName = _nameMapper . MapTypeName ( oldType . Name ) ;
314+ var areTypeNamesEquivalent = string . Equals ( mappedOldTypeName , mappedNewType . Name , StringComparison . Ordinal ) ;
315+
316+ var typeDifference = _differenceCalculator . CalculateTypeChanges ( oldType , mappedNewType , areTypeNamesEquivalent ) ;
303317 if ( typeDifference != null )
304318 {
305319 differences . Add ( typeDifference ) ;
@@ -387,40 +401,47 @@ public IEnumerable<ApiDifference> CompareMembers(Type oldType, Type newType)
387401 _logger . LogDebug (
388402 "Found {OldMemberCount} members in old type and {NewMemberCount} members in new type" ,
389403 oldMembers . Count ,
390- newMembers . Count ) ;
391-
392- // Create dictionaries for faster lookup
393- var oldMembersBySignature = oldMembers . ToDictionary ( m => m . Signature ) ;
394- var newMembersBySignature = newMembers . ToDictionary ( m => m . Signature ) ;
395-
396- // Find added members
404+ newMembers . Count ) ; // Find added members (exist in new but not in old)
397405 foreach ( var newMember in newMembers )
398406 {
399- if ( ! oldMembersBySignature . ContainsKey ( newMember . Signature ) )
407+ var equivalentOldMember = FindEquivalentMember ( newMember , oldMembers ) ;
408+ if ( equivalentOldMember == null )
400409 {
401410 _logger . LogDebug ( "Found added member: {MemberName}" , newMember . FullName ) ;
402411 var addedDifference = _differenceCalculator . CalculateAddedMember ( newMember ) ;
403412 differences . Add ( addedDifference ) ;
404413 }
405414 }
406415
407- // Find removed members
416+ // Find removed members (exist in old but not in new)
408417 foreach ( var oldMember in oldMembers )
409418 {
410- if ( ! newMembersBySignature . ContainsKey ( oldMember . Signature ) )
419+ var equivalentNewMember = FindEquivalentMember ( oldMember , newMembers ) ;
420+ if ( equivalentNewMember == null )
411421 {
412422 _logger . LogDebug ( "Found removed member: {MemberName}" , oldMember . FullName ) ;
413423 var removedDifference = _differenceCalculator . CalculateRemovedMember ( oldMember ) ;
414424 differences . Add ( removedDifference ) ;
415425 }
416426 }
417427
418- // Find modified members
428+ // Find modified members (exist in both but with differences)
419429 foreach ( var oldMember in oldMembers )
420430 {
421- if ( newMembersBySignature . TryGetValue ( oldMember . Signature , out var newMember ) )
431+ var equivalentNewMember = FindEquivalentMember ( oldMember , newMembers ) ;
432+ if ( equivalentNewMember != null )
422433 {
423- var memberDifference = _differenceCalculator . CalculateMemberChanges ( oldMember , newMember ) ;
434+ // Check if the members are truly different or just equivalent via type mappings
435+ if ( AreSignaturesEquivalent ( oldMember . Signature , equivalentNewMember . Signature ) )
436+ {
437+ // Members are equivalent via type mappings - no difference to report
438+ _logger . LogDebug ( "Members are equivalent via type mappings: {OldSignature} <-> {NewSignature}" ,
439+ oldMember . Signature , equivalentNewMember . Signature ) ;
440+ continue ;
441+ }
442+
443+ // Members match but have other differences beyond type mappings
444+ var memberDifference = _differenceCalculator . CalculateMemberChanges ( oldMember , equivalentNewMember ) ;
424445 if ( memberDifference != null )
425446 {
426447 _logger . LogDebug ( "Found modified member: {MemberName}" , oldMember . FullName ) ;
@@ -441,4 +462,129 @@ public IEnumerable<ApiDifference> CompareMembers(Type oldType, Type newType)
441462 return Enumerable . Empty < ApiDifference > ( ) ;
442463 }
443464 }
465+
466+ /// <summary>
467+ /// Applies type mappings to a signature to enable equivalence checking
468+ /// </summary>
469+ /// <param name="signature">The original signature</param>
470+ /// <returns>The signature with type mappings applied</returns>
471+ private string ApplyTypeMappingsToSignature ( string signature )
472+ {
473+ if ( string . IsNullOrEmpty ( signature ) )
474+ {
475+ return signature ;
476+ }
477+
478+ var mappedSignature = signature ;
479+
480+ // Check if we have type mappings configured
481+ if ( _nameMapper . Configuration ? . TypeMappings == null )
482+ {
483+ return mappedSignature ;
484+ }
485+
486+ // Apply all type mappings to the signature
487+ foreach ( var mapping in _nameMapper . Configuration . TypeMappings )
488+ {
489+ // Replace the type name in the signature
490+ // We need to be careful to only replace whole type names, not partial matches
491+ mappedSignature = ReplaceTypeNameInSignature ( mappedSignature , mapping . Key , mapping . Value ) ;
492+
493+ // Also try with just the type name (without namespace) since signatures might not include full namespaces
494+ var oldTypeNameOnly = mapping . Key . Split ( '.' ) . Last ( ) ;
495+ var newTypeNameOnly = mapping . Value . Split ( '.' ) . Last ( ) ;
496+
497+ if ( oldTypeNameOnly != mapping . Key ) // Only if we had a namespace
498+ {
499+ mappedSignature = ReplaceTypeNameInSignature ( mappedSignature , oldTypeNameOnly , newTypeNameOnly ) ;
500+ }
501+ }
502+
503+ return mappedSignature ;
504+ }
505+
506+ /// <summary>
507+ /// Replaces a type name in a signature, ensuring we only replace complete type names
508+ /// </summary>
509+ /// <param name="signature">The signature to modify</param>
510+ /// <param name="oldTypeName">The type name to replace</param>
511+ /// <param name="newTypeName">The replacement type name</param>
512+ /// <returns>The modified signature</returns>
513+ private string ReplaceTypeNameInSignature ( string signature , string oldTypeName , string newTypeName )
514+ {
515+ // We need to replace type names carefully to avoid partial matches
516+ // For example, when replacing "RedisValue" with "ValkeyValue", we don't want to
517+ // replace "RedisValueWithExpiry" incorrectly
518+
519+ var result = signature ;
520+
521+ // Pattern 1: Type name followed by non-word character (space, <, >, ,, etc.)
522+ // This handles most cases including generic parameters and return types
523+ result = System . Text . RegularExpressions . Regex . Replace (
524+ result ,
525+ $@ "\b{ System . Text . RegularExpressions . Regex . Escape ( oldTypeName ) } \b",
526+ newTypeName ) ;
527+
528+ // Pattern 2: Special handling for constructor names
529+ // Constructor signatures typically look like: "public RedisValue(parameters)"
530+ // We need to replace the constructor name (which matches the type name) as well
531+ // This pattern matches: word boundary + type name + opening parenthesis
532+ result = System . Text . RegularExpressions . Regex . Replace (
533+ result ,
534+ $@ "\b{ System . Text . RegularExpressions . Regex . Escape ( oldTypeName ) } (?=\s*\()",
535+ newTypeName ) ;
536+
537+ return result ;
538+ }
539+
540+ /// <summary>
541+ /// Checks if two signatures are equivalent considering type mappings
542+ /// </summary>
543+ /// <param name="sourceSignature">Signature from the source assembly</param>
544+ /// <param name="targetSignature">Signature from the target assembly</param>
545+ /// <returns>True if the signatures are equivalent after applying type mappings</returns>
546+ private bool AreSignaturesEquivalent ( string sourceSignature , string targetSignature )
547+ {
548+ // Apply type mappings to the source signature to see if it matches the target
549+ var mappedSourceSignature = ApplyTypeMappingsToSignature ( sourceSignature ) ;
550+
551+ return string . Equals ( mappedSourceSignature , targetSignature , StringComparison . Ordinal ) ;
552+ } /// <summary>
553+ /// Finds an equivalent member in the target collection based on signature equivalence with type mappings
554+ /// </summary>
555+ /// <param name="sourceMember">The member from the source assembly (could be old or new)</param>
556+ /// <param name="targetMembers">The collection of members from the target assembly (could be new or old)</param>
557+ /// <returns>The equivalent member if found, null otherwise</returns>
558+ private ApiMember ? FindEquivalentMember ( ApiMember sourceMember , IEnumerable < ApiMember > targetMembers )
559+ {
560+ // First, try to find a member with the same name - this handles "modified" members
561+ // where the signature might have changed but it's still the same conceptual member
562+ var sameNameMember = targetMembers . FirstOrDefault ( m =>
563+ m . Name == sourceMember . Name &&
564+ m . FullName == sourceMember . FullName ) ;
565+
566+ if ( sameNameMember != null )
567+ {
568+ return sameNameMember ;
569+ }
570+
571+ // If no exact name match, check for signature equivalence due to type mappings
572+ // This handles cases where type mappings make signatures equivalent even with different names
573+ foreach ( var targetMember in targetMembers )
574+ {
575+ // Check if source maps to target (source signature with mappings applied == target signature)
576+ if ( AreSignaturesEquivalent ( sourceMember . Signature , targetMember . Signature ) )
577+ {
578+ return targetMember ;
579+ }
580+
581+ // Also check the reverse: if target maps to source (target signature with mappings applied == source signature)
582+ if ( AreSignaturesEquivalent ( targetMember . Signature , sourceMember . Signature ) )
583+ {
584+ return targetMember ;
585+ }
586+ }
587+
588+ return null ;
589+ }
444590}
0 commit comments