< Summary

Information
Class: DotNetApiDiff.ApiExtraction.ApiComparer
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs
Line coverage
62%
Covered lines: 222
Uncovered lines: 132
Coverable lines: 354
Total lines: 597
Line coverage: 62.7%
Branch coverage
62%
Covered branches: 90
Total branches: 144
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1212100%
CompareAssemblies(...)100%6684.31%
CompareTypes(...)68.91%3327463.88%
TryFindTypeBySimpleName(...)0%156120%
CompareMembers(...)95%242078.46%
ApplyTypeMappingsToSignature(...)40%371035%
ReplaceTypeNameInSignature(...)100%210%
AreSignaturesEquivalent(...)100%11100%
FindEquivalentMember(...)40%191055.55%

File(s)

/home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ApiExtraction/ApiComparer.cs

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using System.Reflection;
 3using DotNetApiDiff.Interfaces;
 4using DotNetApiDiff.Models;
 5using DotNetApiDiff.Models.Configuration;
 6using Microsoft.Extensions.Logging;
 7
 8namespace DotNetApiDiff.ApiExtraction;
 9
 10/// <summary>
 11/// Compares APIs between two .NET assemblies to identify differences
 12/// </summary>
 13public class ApiComparer : IApiComparer
 14{
 15    private readonly IApiExtractor _apiExtractor;
 16    private readonly IDifferenceCalculator _differenceCalculator;
 17    private readonly INameMapper _nameMapper;
 18    private readonly IChangeClassifier _changeClassifier;
 19    private readonly ComparisonConfiguration _configuration;
 20    private readonly ILogger<ApiComparer> _logger;
 21
 22    /// <summary>
 23    /// Creates a new instance of the ApiComparer
 24    /// </summary>
 25    /// <param name="apiExtractor">API extractor for getting API members</param>
 26    /// <param name="differenceCalculator">Calculator for detailed change analysis</param>
 27    /// <param name="nameMapper">Mapper for namespace and type name transformations</param>
 28    /// <param name="changeClassifier">Classifier for breaking changes and exclusions</param>
 29    /// <param name="configuration">Configuration used for the comparison</param>
 30    /// <param name="logger">Logger for diagnostic information</param>
 2031    public ApiComparer(
 2032        IApiExtractor apiExtractor,
 2033        IDifferenceCalculator differenceCalculator,
 2034        INameMapper nameMapper,
 2035        IChangeClassifier changeClassifier,
 2036        ComparisonConfiguration configuration,
 2037        ILogger<ApiComparer> logger)
 2038    {
 2039        _apiExtractor = apiExtractor ?? throw new ArgumentNullException(nameof(apiExtractor));
 2040        _differenceCalculator = differenceCalculator ?? throw new ArgumentNullException(nameof(differenceCalculator));
 2041        _nameMapper = nameMapper ?? throw new ArgumentNullException(nameof(nameMapper));
 2042        _changeClassifier = changeClassifier ?? throw new ArgumentNullException(nameof(changeClassifier));
 2043        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
 2044        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 2045    }
 46
 47    /// <summary>
 48    /// Compares the public APIs of two assemblies and returns the differences
 49    /// </summary>
 50    /// <param name="oldAssembly">The original assembly</param>
 51    /// <param name="newAssembly">The new assembly to compare against</param>
 52    /// <returns>Comparison result containing all detected differences</returns>
 53    public ComparisonResult CompareAssemblies(Assembly oldAssembly, Assembly newAssembly)
 454    {
 455        if (oldAssembly == null)
 156        {
 157            throw new ArgumentNullException(nameof(oldAssembly));
 58        }
 59
 360        if (newAssembly == null)
 161        {
 162            throw new ArgumentNullException(nameof(newAssembly));
 63        }
 64
 265        _logger.LogInformation(
 266            "Comparing assemblies: {OldAssembly} and {NewAssembly}",
 267            oldAssembly.GetName().Name,
 268            newAssembly.GetName().Name);
 69
 270        var result = new ComparisonResult
 271        {
 272            OldAssemblyPath = oldAssembly.Location,
 273            NewAssemblyPath = newAssembly.Location,
 274            ComparisonTimestamp = DateTime.UtcNow,
 275            Configuration = _configuration
 276        };
 77
 78        try
 279        {
 80            // Extract API members from both assemblies
 281            var oldTypes = _apiExtractor.GetPublicTypes(oldAssembly).ToList();
 282            var newTypes = _apiExtractor.GetPublicTypes(newAssembly).ToList();
 83
 284            _logger.LogDebug(
 285                "Found {OldTypeCount} types in old assembly and {NewTypeCount} types in new assembly",
 286                oldTypes.Count,
 287                newTypes.Count);
 88
 89            // Compare types
 290            var typeDifferences = CompareTypes(oldTypes, newTypes).ToList();
 91
 92            // Classify and add the differences to the result
 1093            foreach (var diff in typeDifferences)
 294            {
 95                // Classify the difference using the change classifier
 296                var classifiedDiff = _changeClassifier.ClassifyChange(diff);
 297                result.Differences.Add(classifiedDiff);
 298            }
 99
 100            // Update summary statistics
 4101            result.Summary.AddedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Added);
 4102            result.Summary.RemovedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Removed);
 4103            result.Summary.ModifiedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Modified);
 4104            result.Summary.BreakingChangesCount = result.Differences.Count(d => d.IsBreakingChange);
 105
 2106            _logger.LogInformation(
 2107                "Comparison complete. Found {TotalDifferences} differences ({AddedCount} added, {RemovedCount} removed, 
 2108                result.TotalDifferences,
 2109                result.Summary.AddedCount,
 2110                result.Summary.RemovedCount,
 2111                result.Summary.ModifiedCount);
 112
 2113            return result;
 114        }
 0115        catch (Exception ex)
 0116        {
 0117            _logger.LogError(
 0118                ex,
 0119                "Error comparing assemblies {OldAssembly} and {NewAssembly}",
 0120                oldAssembly.GetName().Name,
 0121                newAssembly.GetName().Name);
 0122            throw;
 123        }
 2124    }
 125
 126    /// <summary>
 127    /// Compares types between two assemblies
 128    /// </summary>
 129    /// <param name="oldTypes">Types from the original assembly</param>
 130    /// <param name="newTypes">Types from the new assembly</param>
 131    /// <returns>List of type-level differences</returns>
 132    public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnumerable<Type> newTypes)
 8133    {
 8134        if (oldTypes == null)
 1135        {
 1136            throw new ArgumentNullException(nameof(oldTypes));
 137        }
 138
 7139        if (newTypes == null)
 1140        {
 1141            throw new ArgumentNullException(nameof(newTypes));
 142        }
 143
 6144        var differences = new List<ApiDifference>();
 6145        var oldTypesList = oldTypes.ToList();
 6146        var newTypesList = newTypes.ToList();
 147
 148        // Create dictionaries for faster lookup
 13149        var oldTypesByFullName = oldTypesList.ToDictionary(t => t.FullName ?? t.Name);
 15150        var newTypesByFullName = newTypesList.ToDictionary(t => t.FullName ?? t.Name);
 151
 152        // Create a lookup for mapped types
 6153        var mappedTypeLookup = new Dictionary<string, List<Type>>();
 154
 155        // Build the mapped type lookup
 32156        foreach (var oldType in oldTypesList)
 7157        {
 7158            var oldTypeName = oldType.FullName ?? oldType.Name;
 7159            var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();
 160
 23161            foreach (var mappedName in mappedNames)
 1162            {
 1163                if (mappedName != oldTypeName)
 0164                {
 0165                    _logger.LogDebug("Mapped type {OldTypeName} to {MappedTypeName}", oldTypeName, mappedName);
 166
 0167                    if (!mappedTypeLookup.ContainsKey(mappedName))
 0168                    {
 0169                        mappedTypeLookup[mappedName] = new List<Type>();
 0170                    }
 171
 0172                    mappedTypeLookup[mappedName].Add(oldType);
 0173                }
 1174            }
 7175        }
 176
 177        // Find added types
 36178        foreach (var newType in newTypesList)
 9179        {
 9180            var newTypeName = newType.FullName ?? newType.Name;
 9181            bool foundMatch = false;
 182
 183            // Check direct match
 9184            if (oldTypesByFullName.ContainsKey(newTypeName))
 5185            {
 5186                foundMatch = true;
 5187            }
 188            else
 4189            {
 190                // Check if any old type maps to this new type
 20191                foreach (var oldType in oldTypesList)
 4192                {
 4193                    var oldTypeName = oldType.FullName ?? oldType.Name;
 4194                    var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();
 195
 14196                    foreach (var mappedName in mappedNames)
 1197                    {
 1198                        if (string.Equals(mappedName, newTypeName, StringComparison.Ordinal))
 0199                        {
 0200                            foundMatch = true;
 0201                            _logger.LogDebug(
 0202                                "Found mapped type: {OldTypeName} -> {NewTypeName}",
 0203                                oldTypeName,
 0204                                newTypeName);
 0205                            break;
 206                        }
 1207                    }
 208
 4209                    if (foundMatch)
 0210                    {
 0211                        break;
 212                    }
 4213                }
 214
 215                // Check for auto-mapping if enabled
 4216                if (!foundMatch && _nameMapper.ShouldAutoMapType(newTypeName))
 0217                {
 0218                    if (TryFindTypeBySimpleName(newTypeName, oldTypesList, out var matchedOldTypeName))
 0219                    {
 0220                        foundMatch = true;
 0221                        _logger.LogDebug(
 0222                            "Auto-mapped type {NewTypeName} to {OldTypeName} by simple name",
 0223                            newTypeName,
 0224                            matchedOldTypeName);
 0225                    }
 0226                }
 4227            }
 228
 9229            if (!foundMatch)
 4230            {
 4231                differences.Add(_differenceCalculator.CalculateAddedType(newType));
 4232            }
 9233        }
 234
 235        // Find removed types
 32236        foreach (var oldType in oldTypesList)
 7237        {
 7238            var oldTypeName = oldType.FullName ?? oldType.Name;
 7239            bool foundMatch = false;
 240
 241            // Check direct match
 7242            if (newTypesByFullName.ContainsKey(oldTypeName))
 5243            {
 5244                foundMatch = true;
 5245            }
 246            else
 2247            {
 248                // Check mapped names
 2249                var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();
 250
 8251                foreach (var mappedName in mappedNames)
 1252                {
 1253                    if (newTypesByFullName.ContainsKey(mappedName))
 0254                    {
 0255                        foundMatch = true;
 0256                        break;
 257                    }
 1258                }
 259
 260                // Check for auto-mapping if enabled
 2261                if (!foundMatch && _nameMapper.ShouldAutoMapType(oldTypeName))
 0262                {
 0263                    if (TryFindTypeBySimpleName(oldTypeName, newTypesList, out var matchedNewTypeName))
 0264                    {
 0265                        foundMatch = true;
 0266                        _logger.LogDebug(
 0267                            "Auto-mapped type {OldTypeName} to {NewTypeName} by simple name",
 0268                            oldTypeName,
 0269                            matchedNewTypeName);
 0270                    }
 0271                }
 2272            }
 273
 7274            if (!foundMatch)
 2275            {
 2276                differences.Add(_differenceCalculator.CalculateRemovedType(oldType));
 2277            }
 7278        }
 279
 280        // Find modified types - direct matches
 32281        foreach (var oldType in oldTypesList)
 7282        {
 7283            var oldTypeName = oldType.FullName ?? oldType.Name;
 284
 285            // Check direct match first
 7286            if (newTypesByFullName.TryGetValue(oldTypeName, out var newType))
 5287            {
 288                // Compare the types
 5289                var memberDifferences = CompareMembers(oldType, newType).ToList();
 5290                differences.AddRange(memberDifferences);
 291
 292                // Check for type-level changes (e.g., accessibility, base class, interfaces)
 5293                var typeDifference = _differenceCalculator.CalculateTypeChanges(oldType, newType);
 5294                if (typeDifference != null)
 1295                {
 1296                    differences.Add(typeDifference);
 1297                }
 5298            }
 299            else
 2300            {
 301                // Check mapped names
 2302                var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();
 303
 8304                foreach (var mappedName in mappedNames)
 1305                {
 1306                    if (newTypesByFullName.TryGetValue(mappedName, out var mappedNewType))
 0307                    {
 0308                        _logger.LogDebug("Comparing mapped types: {OldTypeName} -> {MappedTypeName}", oldTypeName, mappe
 309
 310                        // Compare the types
 0311                        var memberDifferences = CompareMembers(oldType, mappedNewType).ToList();
 0312                        differences.AddRange(memberDifferences);
 313
 314                        // Check for type-level changes with signature equivalence
 315                        // For mapped types, check if the old type name maps to the new type name
 0316                        var mappedOldTypeName = _nameMapper.MapTypeName(oldType.Name);
 0317                        var areTypeNamesEquivalent = string.Equals(mappedOldTypeName, mappedNewType.Name, StringComparis
 318
 0319                        var typeDifference = _differenceCalculator.CalculateTypeChanges(oldType, mappedNewType, areTypeN
 0320                        if (typeDifference != null)
 0321                        {
 0322                            differences.Add(typeDifference);
 0323                        }
 324
 0325                        break;
 326                    }
 1327                }
 2328            }
 7329        }
 330
 6331        return differences;
 6332    }
 333
 334    /// <summary>
 335    /// Tries to find a matching type by simple name (without namespace)
 336    /// </summary>
 337    /// <param name="typeName">The type name to find a match for</param>
 338    /// <param name="candidateTypes">List of candidate types to search</param>
 339    /// <param name="matchedTypeName">The matched type name, if found</param>
 340    /// <returns>True if a match was found, false otherwise</returns>
 341    private bool TryFindTypeBySimpleName(string typeName, IEnumerable<Type> candidateTypes, out string? matchedTypeName)
 0342    {
 0343        matchedTypeName = null;
 344
 345        // Extract simple type name for auto-mapping
 0346        int lastDotIndex = typeName.LastIndexOf('.');
 0347        if (lastDotIndex <= 0)
 0348        {
 0349            return false;
 350        }
 351
 0352        string simpleTypeName = typeName.Substring(lastDotIndex + 1);
 353
 354        // Look for any type with the same simple name
 0355        foreach (var candidateType in candidateTypes)
 0356        {
 0357            var candidateTypeName = candidateType.FullName ?? candidateType.Name;
 0358            int candidateLastDotIndex = candidateTypeName.LastIndexOf('.');
 359
 0360            if (candidateLastDotIndex > 0)
 0361            {
 0362                string candidateSimpleTypeName = candidateTypeName.Substring(candidateLastDotIndex + 1);
 363
 0364                if (string.Equals(
 0365                    simpleTypeName,
 0366                    candidateSimpleTypeName,
 0367                    _nameMapper.Configuration.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal
 0368                {
 0369                    matchedTypeName = candidateTypeName;
 0370                    return true;
 371                }
 0372            }
 0373        }
 374
 0375        return false;
 0376    }
 377
 378    /// <summary>
 379    /// Compares members (methods, properties, fields) of two types
 380    /// </summary>
 381    /// <param name="oldType">Original type</param>
 382    /// <param name="newType">New type to compare against</param>
 383    /// <returns>List of member-level differences</returns>
 384    public IEnumerable<ApiDifference> CompareMembers(Type oldType, Type newType)
 10385    {
 10386        if (oldType == null)
 1387        {
 1388            throw new ArgumentNullException(nameof(oldType));
 389        }
 390
 9391        if (newType == null)
 1392        {
 1393            throw new ArgumentNullException(nameof(newType));
 394        }
 395
 8396        var differences = new List<ApiDifference>();
 397
 398        try
 8399        {
 400            // Extract members from both types
 8401            var oldMembers = _apiExtractor.ExtractTypeMembers(oldType).ToList();
 8402            var newMembers = _apiExtractor.ExtractTypeMembers(newType).ToList();
 403
 8404            _logger.LogDebug(
 8405                "Found {OldMemberCount} members in old type and {NewMemberCount} members in new type",
 8406                oldMembers.Count,
 8407                newMembers.Count);            // Find added members (exist in new but not in old)
 28408            foreach (var newMember in newMembers)
 2409            {
 2410                var equivalentOldMember = FindEquivalentMember(newMember, oldMembers);
 2411                if (equivalentOldMember == null)
 1412                {
 1413                    _logger.LogDebug("Found added member: {MemberName}", newMember.FullName);
 1414                    var addedDifference = _differenceCalculator.CalculateAddedMember(newMember);
 1415                    differences.Add(addedDifference);
 1416                }
 2417            }
 418
 419            // Find removed members (exist in old but not in new)
 28420            foreach (var oldMember in oldMembers)
 2421            {
 2422                var equivalentNewMember = FindEquivalentMember(oldMember, newMembers);
 2423                if (equivalentNewMember == null)
 1424                {
 1425                    _logger.LogDebug("Found removed member: {MemberName}", oldMember.FullName);
 1426                    var removedDifference = _differenceCalculator.CalculateRemovedMember(oldMember);
 1427                    differences.Add(removedDifference);
 1428                }
 2429            }
 430
 431            // Find modified members (exist in both but with differences)
 28432            foreach (var oldMember in oldMembers)
 2433            {
 2434                var equivalentNewMember = FindEquivalentMember(oldMember, newMembers);
 2435                if (equivalentNewMember != null)
 1436                {
 437                    // Check if the members are truly different or just equivalent via type mappings
 1438                    if (AreSignaturesEquivalent(oldMember.Signature, equivalentNewMember.Signature))
 0439                    {
 440                        // Members are equivalent via type mappings - no difference to report
 0441                        _logger.LogDebug(
 0442                            "Members are equivalent via type mappings: {OldSignature} <-> {NewSignature}",
 0443                            oldMember.Signature,
 0444                            equivalentNewMember.Signature);
 0445                        continue;
 446                    }
 447
 448                    // Members match but have other differences beyond type mappings
 1449                    var memberDifference = _differenceCalculator.CalculateMemberChanges(oldMember, equivalentNewMember);
 1450                    if (memberDifference != null)
 1451                    {
 1452                        _logger.LogDebug("Found modified member: {MemberName}", oldMember.FullName);
 1453                        differences.Add(memberDifference);
 1454                    }
 1455                }
 2456            }
 457
 8458            return differences;
 459        }
 0460        catch (Exception ex)
 0461        {
 0462            _logger.LogError(
 0463                ex,
 0464                "Error comparing members of types {OldType} and {NewType}",
 0465                oldType.FullName,
 0466                newType.FullName);
 0467            return Enumerable.Empty<ApiDifference>();
 468        }
 8469    }
 470
 471    /// <summary>
 472    /// Applies type mappings to a signature to enable equivalence checking
 473    /// </summary>
 474    /// <param name="signature">The original signature</param>
 475    /// <returns>The signature with type mappings applied</returns>
 476    private string ApplyTypeMappingsToSignature(string signature)
 1477    {
 1478        if (string.IsNullOrEmpty(signature))
 0479        {
 0480            return signature;
 481        }
 482
 1483        var mappedSignature = signature;
 484
 485        // Check if we have type mappings configured
 1486        if (_nameMapper.Configuration?.TypeMappings == null)
 0487        {
 0488            return mappedSignature;
 489        }
 490
 491        // Apply all type mappings to the signature
 3492        foreach (var mapping in _nameMapper.Configuration.TypeMappings)
 0493        {
 494            // Replace the type name in the signature
 495            // We need to be careful to only replace whole type names, not partial matches
 0496            mappedSignature = ReplaceTypeNameInSignature(mappedSignature, mapping.Key, mapping.Value);
 497
 498            // Also try with just the type name (without namespace) since signatures might not include full namespaces
 0499            var oldTypeNameOnly = mapping.Key.Split('.').Last();
 0500            var newTypeNameOnly = mapping.Value.Split('.').Last();
 501
 502            // Only if we had a namespace
 0503            if (oldTypeNameOnly != mapping.Key)
 0504            {
 0505                mappedSignature = ReplaceTypeNameInSignature(mappedSignature, oldTypeNameOnly, newTypeNameOnly);
 0506            }
 0507        }
 508
 1509        return mappedSignature;
 1510    }
 511
 512    /// <summary>
 513    /// Replaces a type name in a signature, ensuring we only replace complete type names
 514    /// </summary>
 515    /// <param name="signature">The signature to modify</param>
 516    /// <param name="oldTypeName">The type name to replace</param>
 517    /// <param name="newTypeName">The replacement type name</param>
 518    /// <returns>The modified signature</returns>
 519    private string ReplaceTypeNameInSignature(string signature, string oldTypeName, string newTypeName)
 0520    {
 521        // We need to replace type names carefully to avoid partial matches
 522        // For example, when replacing "RedisValue" with "ValkeyValue", we don't want to
 523        // replace "RedisValueWithExpiry" incorrectly
 0524        var result = signature;
 525
 526        // Pattern 1: Type name followed by non-word character (space, <, >, ,, etc.)
 527        // This handles most cases including generic parameters and return types
 0528        result = System.Text.RegularExpressions.Regex.Replace(
 0529            result,
 0530            $@"\b{System.Text.RegularExpressions.Regex.Escape(oldTypeName)}\b",
 0531            newTypeName);
 532
 533        // Pattern 2: Special handling for constructor names
 534        // Constructor signatures typically look like: "public RedisValue(parameters)"
 535        // We need to replace the constructor name (which matches the type name) as well
 536        // This pattern matches: word boundary + type name + opening parenthesis
 0537        result = System.Text.RegularExpressions.Regex.Replace(
 0538            result,
 0539            $@"\b{System.Text.RegularExpressions.Regex.Escape(oldTypeName)}(?=\s*\()",
 0540            newTypeName);
 541
 0542        return result;
 0543    }
 544
 545    /// <summary>
 546    /// Checks if two signatures are equivalent considering type mappings
 547    /// </summary>
 548    /// <param name="sourceSignature">Signature from the source assembly</param>
 549    /// <param name="targetSignature">Signature from the target assembly</param>
 550    /// <returns>True if the signatures are equivalent after applying type mappings</returns>
 551    private bool AreSignaturesEquivalent(string sourceSignature, string targetSignature)
 1552    {
 553        // Apply type mappings to the source signature to see if it matches the target
 1554        var mappedSourceSignature = ApplyTypeMappingsToSignature(sourceSignature);
 555
 1556        return string.Equals(mappedSourceSignature, targetSignature, StringComparison.Ordinal);
 1557    }
 558
 559    /// <summary>
 560    /// Finds an equivalent member in the target collection based on signature equivalence with type mappings
 561    /// </summary>
 562    /// <param name="sourceMember">The member from the source assembly (could be old or new)</param>
 563    /// <param name="targetMembers">The collection of members from the target assembly (could be new or old)</param>
 564    /// <returns>The equivalent member if found, null otherwise</returns>
 565    private ApiMember? FindEquivalentMember(ApiMember sourceMember, IEnumerable<ApiMember> targetMembers)
 6566    {
 567        // First, try to find a member with the same name - this handles "modified" members
 568        // where the signature might have changed but it's still the same conceptual member
 6569        var sameNameMember = targetMembers.FirstOrDefault(m =>
 9570            m.Name == sourceMember.Name &&
 9571            m.FullName == sourceMember.FullName);
 572
 6573        if (sameNameMember != null)
 3574        {
 3575            return sameNameMember;
 576        }
 577
 578        // If no exact name match, check for signature equivalence due to type mappings
 579        // This handles cases where type mappings make signatures equivalent even with different names
 9580        foreach (var targetMember in targetMembers)
 0581        {
 582            // Check if source maps to target (source signature with mappings applied == target signature)
 0583            if (AreSignaturesEquivalent(sourceMember.Signature, targetMember.Signature))
 0584            {
 0585                return targetMember;
 586            }
 587
 588            // Also check the reverse: if target maps to source (target signature with mappings applied == source signat
 0589            if (AreSignaturesEquivalent(targetMember.Signature, sourceMember.Signature))
 0590            {
 0591                return targetMember;
 592            }
 0593        }
 594
 3595        return null;
 6596    }
 597}