< Summary

Information
Class: DotNetApiDiff.ApiExtraction.DifferenceCalculator
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ApiExtraction/DifferenceCalculator.cs
Line coverage
70%
Covered lines: 173
Uncovered lines: 72
Coverable lines: 245
Total lines: 412
Line coverage: 70.6%
Branch coverage
58%
Covered branches: 44
Total branches: 75
Branch coverage: 58.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%44100%
CalculateAddedType(...)66.66%6680.95%
CalculateRemovedType(...)66.66%6680.95%
CalculateTypeChanges(...)55%522056.86%
CalculateAddedMember(...)100%2280%
CalculateRemovedMember(...)100%2280%
CalculateMemberChanges(...)87.5%211673.33%
GetTypeKindString(...)50%16850%
GetMemberTypeString(...)100%11100%
GetApiElementType(...)9.09%411137.5%
IsReducedAccessibility(...)100%11100%

File(s)

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

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using DotNetApiDiff.Interfaces;
 3using DotNetApiDiff.Models;
 4using Microsoft.Extensions.Logging;
 5
 6namespace DotNetApiDiff.ApiExtraction;
 7
 8/// <summary>
 9/// Calculates detailed API differences between members and types
 10/// </summary>
 11public class DifferenceCalculator : IDifferenceCalculator
 12{
 13    private readonly ITypeAnalyzer _typeAnalyzer;
 14    private readonly ILogger<DifferenceCalculator> _logger;
 15
 16    /// <summary>
 17    /// Creates a new instance of the DifferenceCalculator
 18    /// </summary>
 19    /// <param name="typeAnalyzer">Type analyzer for analyzing types</param>
 20    /// <param name="logger">Logger for diagnostic information</param>
 2321    public DifferenceCalculator(ITypeAnalyzer typeAnalyzer, ILogger<DifferenceCalculator> logger)
 2322    {
 2323        _typeAnalyzer = typeAnalyzer ?? throw new ArgumentNullException(nameof(typeAnalyzer));
 2324        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 2325    }
 26
 27    /// <summary>
 28    /// Calculates an ApiDifference for an added type
 29    /// </summary>
 30    /// <param name="newType">The new type that was added</param>
 31    /// <returns>ApiDifference representing the addition</returns>
 32    public ApiDifference CalculateAddedType(Type newType)
 233    {
 234        if (newType == null)
 135        {
 136            throw new ArgumentNullException(nameof(newType));
 37        }
 38
 39        try
 140        {
 141            var typeMember = _typeAnalyzer.AnalyzeType(newType);
 42
 143            return new ApiDifference
 144            {
 145                ChangeType = ChangeType.Added,
 146                ElementType = ApiElementType.Type,
 147                ElementName = newType.FullName ?? newType.Name,
 148                Description = $"Added {GetTypeKindString(newType)} '{newType.FullName ?? newType.Name}'",
 149                IsBreakingChange = false, // Adding types is not breaking
 150                Severity = SeverityLevel.Info,
 151                NewSignature = typeMember.Signature
 152            };
 53        }
 054        catch (Exception ex)
 055        {
 056            _logger.LogError(ex, "Error calculating difference for added type {TypeName}", newType.Name);
 057            throw;
 58        }
 159    }
 60
 61    /// <summary>
 62    /// Calculates an ApiDifference for a removed type
 63    /// </summary>
 64    /// <param name="oldType">The old type that was removed</param>
 65    /// <returns>ApiDifference representing the removal</returns>
 66    public ApiDifference CalculateRemovedType(Type oldType)
 267    {
 268        if (oldType == null)
 169        {
 170            throw new ArgumentNullException(nameof(oldType));
 71        }
 72
 73        try
 174        {
 175            var typeMember = _typeAnalyzer.AnalyzeType(oldType);
 76
 177            return new ApiDifference
 178            {
 179                ChangeType = ChangeType.Removed,
 180                ElementType = ApiElementType.Type,
 181                ElementName = oldType.FullName ?? oldType.Name,
 182                Description = $"Removed {GetTypeKindString(oldType)} '{oldType.FullName ?? oldType.Name}'",
 183                IsBreakingChange = true, // Removing types is breaking
 184                Severity = SeverityLevel.Error,
 185                OldSignature = typeMember.Signature
 186            };
 87        }
 088        catch (Exception ex)
 089        {
 090            _logger.LogError(ex, "Error calculating difference for removed type {TypeName}", oldType.Name);
 091            throw;
 92        }
 193    }
 94
 95    /// <summary>
 96    /// Calculates an ApiDifference for changes between two types
 97    /// </summary>
 98    /// <param name="oldType">The original type</param>
 99    /// <param name="newType">The new type</param>
 100    /// <param name="signaturesEquivalent">Whether the signatures are equivalent after applying type mappings</param>
 101    /// <returns>ApiDifference representing the changes, or null if no changes</returns>
 102    public ApiDifference? CalculateTypeChanges(Type oldType, Type newType, bool signaturesEquivalent = false)
 4103    {
 4104        if (oldType == null)
 1105        {
 1106            throw new ArgumentNullException(nameof(oldType));
 107        }
 108
 3109        if (newType == null)
 1110        {
 1111            throw new ArgumentNullException(nameof(newType));
 112        }
 113
 114        try
 2115        {
 2116            var oldTypeMember = _typeAnalyzer.AnalyzeType(oldType);
 2117            var newTypeMember = _typeAnalyzer.AnalyzeType(newType);
 118
 119            // For testing purposes, always return a difference if accessibility changes
 2120            if (oldTypeMember.Accessibility != newTypeMember.Accessibility)
 1121            {
 1122                bool accessibilityBreaking = IsReducedAccessibility(oldTypeMember.Accessibility, newTypeMember.Accessibi
 1123                SeverityLevel accessibilitySeverity = accessibilityBreaking ? SeverityLevel.Error : SeverityLevel.Info;
 124
 1125                return new ApiDifference
 1126                {
 1127                    ChangeType = ChangeType.Modified,
 1128                    ElementType = ApiElementType.Type,
 1129                    ElementName = oldType.FullName ?? oldType.Name,
 1130                    Description = $"Modified {GetTypeKindString(oldType)} '{oldType.FullName ?? oldType.Name}'",
 1131                    IsBreakingChange = accessibilityBreaking,
 1132                    Severity = accessibilitySeverity,
 1133                    OldSignature = oldTypeMember.Signature,
 1134                    NewSignature = newTypeMember.Signature
 1135                };
 136            }
 137
 138            // If signatures are different but equivalent after type mappings, no change
 1139            if (signaturesEquivalent)
 0140            {
 0141                return null;
 142            }
 143
 144            // If signatures are different, we have changes
 1145            if (oldTypeMember.Signature != newTypeMember.Signature)
 0146            {
 0147                return new ApiDifference
 0148                {
 0149                    ChangeType = ChangeType.Modified,
 0150                    ElementType = ApiElementType.Type,
 0151                    ElementName = oldType.FullName ?? oldType.Name,
 0152                    Description = $"Modified {GetTypeKindString(oldType)} '{oldType.FullName ?? oldType.Name}'",
 0153                    IsBreakingChange = true, // Assume signature changes are breaking
 0154                    Severity = SeverityLevel.Warning,
 0155                    OldSignature = oldTypeMember.Signature,
 0156                    NewSignature = newTypeMember.Signature
 0157                };
 158            }
 159
 160            // If signatures are identical and no other changes detected, return null
 1161            return null;
 162        }
 0163        catch (Exception ex)
 0164        {
 0165            _logger.LogError(
 0166                ex,
 0167                "Error calculating differences between types {OldType} and {NewType}",
 0168                oldType.Name,
 0169                newType.Name);
 0170            return null;
 171        }
 2172    }
 173
 174    /// <summary>
 175    /// Calculates an ApiDifference for an added member
 176    /// </summary>
 177    /// <param name="newMember">The new member that was added</param>
 178    /// <returns>ApiDifference representing the addition</returns>
 179    public ApiDifference CalculateAddedMember(ApiMember newMember)
 2180    {
 2181        if (newMember == null)
 1182        {
 1183            throw new ArgumentNullException(nameof(newMember));
 184        }
 185
 186        try
 1187        {
 1188            return new ApiDifference
 1189            {
 1190                ChangeType = ChangeType.Added,
 1191                ElementType = GetApiElementType(newMember.Type),
 1192                ElementName = newMember.FullName,
 1193                Description = $"Added {GetMemberTypeString(newMember.Type)} '{newMember.FullName}'",
 1194                IsBreakingChange = false, // Adding members is not breaking
 1195                Severity = SeverityLevel.Info,
 1196                NewSignature = newMember.Signature
 1197            };
 198        }
 0199        catch (Exception ex)
 0200        {
 0201            _logger.LogError(ex, "Error calculating difference for added member {MemberName}", newMember.Name);
 0202            throw;
 203        }
 1204    }
 205
 206    /// <summary>
 207    /// Calculates an ApiDifference for a removed member
 208    /// </summary>
 209    /// <param name="oldMember">The old member that was removed</param>
 210    /// <returns>ApiDifference representing the removal</returns>
 211    public ApiDifference CalculateRemovedMember(ApiMember oldMember)
 2212    {
 2213        if (oldMember == null)
 1214        {
 1215            throw new ArgumentNullException(nameof(oldMember));
 216        }
 217
 218        try
 1219        {
 1220            return new ApiDifference
 1221            {
 1222                ChangeType = ChangeType.Removed,
 1223                ElementType = GetApiElementType(oldMember.Type),
 1224                ElementName = oldMember.FullName,
 1225                Description = $"Removed {GetMemberTypeString(oldMember.Type)} '{oldMember.FullName}'",
 1226                IsBreakingChange = true, // Removing members is breaking
 1227                Severity = SeverityLevel.Error,
 1228                OldSignature = oldMember.Signature
 1229            };
 230        }
 0231        catch (Exception ex)
 0232        {
 0233            _logger.LogError(ex, "Error calculating difference for removed member {MemberName}", oldMember.Name);
 0234            throw;
 235        }
 1236    }
 237
 238    /// <summary>
 239    /// Calculates an ApiDifference for changes between two members
 240    /// </summary>
 241    /// <param name="oldMember">The original member</param>
 242    /// <param name="newMember">The new member</param>
 243    /// <returns>ApiDifference representing the changes, or null if no changes</returns>
 244    public ApiDifference? CalculateMemberChanges(ApiMember oldMember, ApiMember newMember)
 5245    {
 5246        if (oldMember == null)
 1247        {
 1248            throw new ArgumentNullException(nameof(oldMember));
 249        }
 250
 4251        if (newMember == null)
 1252        {
 1253            throw new ArgumentNullException(nameof(newMember));
 254        }
 255
 256        try
 3257        {
 258            // If signatures are identical, no changes
 3259            if (oldMember.Signature == newMember.Signature)
 1260            {
 1261                return null;
 262            }
 263
 2264            List<string> changes = new List<string>();
 2265            bool isBreaking = false;
 2266            SeverityLevel severity = SeverityLevel.Info;
 267
 268            // Check for accessibility changes
 2269            if (oldMember.Accessibility != newMember.Accessibility)
 1270            {
 1271                var accessibilityChange = $"Accessibility changed from '{oldMember.Accessibility}' to '{newMember.Access
 1272                changes.Add(accessibilityChange);
 273
 274                // Reducing accessibility is breaking
 1275                if (IsReducedAccessibility(oldMember.Accessibility, newMember.Accessibility))
 1276                {
 1277                    isBreaking = true;
 1278                    severity = SeverityLevel.Error;
 1279                }
 1280            }
 281
 282            // Check for attribute changes
 2283            var removedAttributes = oldMember.Attributes.Except(newMember.Attributes).ToList();
 2284            var addedAttributes = newMember.Attributes.Except(oldMember.Attributes).ToList();
 285
 8286            foreach (var removedAttr in removedAttributes)
 1287            {
 1288                changes.Add($"Removed attribute '{removedAttr}'");
 1289            }
 290
 6291            foreach (var addedAttr in addedAttributes)
 0292            {
 0293                changes.Add($"Added attribute '{addedAttr}'");
 0294            }
 295
 296            // If no changes were detected but signatures differ, add a generic change
 2297            if (!changes.Any())
 0298            {
 0299                changes.Add("Member signature changed");
 300
 301                // Signature changes are potentially breaking
 0302                isBreaking = true;
 0303                severity = SeverityLevel.Warning;
 0304            }
 305
 2306            return new ApiDifference
 2307            {
 2308                ChangeType = ChangeType.Modified,
 2309                ElementType = GetApiElementType(oldMember.Type),
 2310                ElementName = oldMember.FullName,
 2311                Description = $"Modified {GetMemberTypeString(oldMember.Type)} '{oldMember.FullName}'",
 2312                IsBreakingChange = isBreaking,
 2313                Severity = severity,
 2314                OldSignature = oldMember.Signature,
 2315                NewSignature = newMember.Signature
 2316            };
 317        }
 0318        catch (Exception ex)
 0319        {
 0320            _logger.LogError(
 0321                ex,
 0322                "Error calculating differences between members {OldMember} and {NewMember}",
 0323                oldMember.Name,
 0324                newMember.Name);
 0325            return null;
 326        }
 3327    }
 328
 329    /// <summary>
 330    /// Gets a string representation of a type kind
 331    /// </summary>
 332    /// <param name="type">Type to get kind string for</param>
 333    /// <returns>Type kind string</returns>
 334    private string GetTypeKindString(Type type)
 3335    {
 3336        if (type.IsInterface)
 0337        {
 0338            return "interface";
 339        }
 3340        else if (type.IsEnum)
 0341        {
 0342            return "enum";
 343        }
 3344        else if (type.IsValueType)
 0345        {
 0346            return "struct";
 347        }
 3348        else if (type.IsSubclassOf(typeof(MulticastDelegate)))
 0349        {
 0350            return "delegate";
 351        }
 352        else
 3353        {
 3354            return "class";
 355        }
 3356    }
 357
 358    /// <summary>
 359    /// Gets a string representation of a member type
 360    /// </summary>
 361    /// <param name="memberType">Member type to get string for</param>
 362    /// <returns>Member type string</returns>
 363    private string GetMemberTypeString(MemberType memberType)
 4364    {
 4365        return memberType.ToString().ToLowerInvariant();
 4366    }
 367
 368    /// <summary>
 369    /// Maps a MemberType to an ApiElementType
 370    /// </summary>
 371    /// <param name="memberType">Member type to map</param>
 372    /// <returns>Corresponding API element type</returns>
 373    private ApiElementType GetApiElementType(MemberType memberType)
 4374    {
 4375        return memberType switch
 4376        {
 0377            MemberType.Class => ApiElementType.Type,
 0378            MemberType.Interface => ApiElementType.Type,
 0379            MemberType.Struct => ApiElementType.Type,
 0380            MemberType.Enum => ApiElementType.Type,
 0381            MemberType.Delegate => ApiElementType.Type,
 4382            MemberType.Method => ApiElementType.Method,
 0383            MemberType.Property => ApiElementType.Property,
 0384            MemberType.Field => ApiElementType.Field,
 0385            MemberType.Event => ApiElementType.Event,
 0386            MemberType.Constructor => ApiElementType.Constructor,
 0387            _ => ApiElementType.Type
 4388        };
 4389    }
 390
 391    /// <summary>
 392    /// Checks if accessibility has been reduced
 393    /// </summary>
 394    /// <param name="oldAccessibility">Original accessibility</param>
 395    /// <param name="newAccessibility">New accessibility</param>
 396    /// <returns>True if accessibility has been reduced, false otherwise</returns>
 397    private bool IsReducedAccessibility(AccessibilityLevel oldAccessibility, AccessibilityLevel newAccessibility)
 2398    {
 399        // Higher values are more accessible
 2400        var accessibilityRank = new Dictionary<AccessibilityLevel, int>
 2401        {
 2402            { AccessibilityLevel.Public, 5 },
 2403            { AccessibilityLevel.ProtectedInternal, 4 },
 2404            { AccessibilityLevel.Internal, 3 },
 2405            { AccessibilityLevel.Protected, 2 },
 2406            { AccessibilityLevel.ProtectedPrivate, 1 },
 2407            { AccessibilityLevel.Private, 0 }
 2408        };
 409
 2410        return accessibilityRank[newAccessibility] < accessibilityRank[oldAccessibility];
 2411    }
 412}