< Summary

Information
Class: DotNetApiDiff.ApiExtraction.ChangeClassifier
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ApiExtraction/ChangeClassifier.cs
Line coverage
82%
Covered lines: 140
Uncovered lines: 29
Coverable lines: 169
Total lines: 330
Line coverage: 82.8%
Branch coverage
75%
Covered branches: 49
Total branches: 65
Branch coverage: 75.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%66100%
ClassifyChange(...)88.88%9993.75%
IsTypeExcluded(...)75%8882.35%
IsMemberExcluded(...)83.33%121288.46%
WildcardToRegex(...)100%11100%
InitializePatternCaches()100%4471.42%
ShouldExcludeElement(...)75%4483.33%
ClassifyAddedChange(...)62.5%9871.42%
ClassifyRemovedChange(...)62.5%9871.42%
ClassifyModifiedChange(...)83.33%66100%
ClassifyMovedChange(...)100%210%

File(s)

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

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using System.Text.RegularExpressions;
 3using DotNetApiDiff.Interfaces;
 4using DotNetApiDiff.Models;
 5using DotNetApiDiff.Models.Configuration;
 6using Microsoft.Extensions.Logging;
 7
 8namespace DotNetApiDiff.ApiExtraction;
 9
 10/// <summary>
 11/// Classifies API changes as breaking, non-breaking, or excluded based on configuration rules
 12/// </summary>
 13public class ChangeClassifier : IChangeClassifier
 14{
 15    private readonly BreakingChangeRules _breakingChangeRules;
 16    private readonly ExclusionConfiguration _exclusionConfig;
 17    private readonly ILogger<ChangeClassifier> _logger;
 2318    private readonly Dictionary<string, Regex> _typePatternCache = new();
 2319    private readonly Dictionary<string, Regex> _memberPatternCache = new();
 20
 21    /// <summary>
 22    /// Creates a new instance of the ChangeClassifier
 23    /// </summary>
 24    /// <param name="breakingChangeRules">Rules for determining breaking changes</param>
 25    /// <param name="exclusionConfig">Configuration for exclusions</param>
 26    /// <param name="logger">Logger for diagnostic information</param>
 2327    public ChangeClassifier(
 2328        BreakingChangeRules breakingChangeRules,
 2329        ExclusionConfiguration exclusionConfig,
 2330        ILogger<ChangeClassifier> logger)
 2331    {
 2332        _breakingChangeRules = breakingChangeRules ?? throw new ArgumentNullException(nameof(breakingChangeRules));
 2333        _exclusionConfig = exclusionConfig ?? throw new ArgumentNullException(nameof(exclusionConfig));
 2334        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 35
 36        // Initialize regex pattern caches
 2337        InitializePatternCaches();
 2338    }
 39
 40    /// <summary>
 41    /// Classifies an API difference as breaking, non-breaking, or excluded
 42    /// </summary>
 43    /// <param name="difference">The API difference to classify</param>
 44    /// <returns>The classified API difference with updated properties</returns>
 45    public ApiDifference ClassifyChange(ApiDifference difference)
 946    {
 947        if (difference == null)
 148        {
 149            throw new ArgumentNullException(nameof(difference));
 50        }
 51
 52        // Check if the element should be excluded
 853        if (ShouldExcludeElement(difference))
 254        {
 255            difference.ChangeType = ChangeType.Excluded;
 256            difference.IsBreakingChange = false;
 257            difference.Severity = SeverityLevel.Info;
 258            difference.Description = $"Excluded {difference.ElementType}: {difference.ElementName}";
 59
 260            _logger.LogDebug(
 261                "Classified {ElementType} '{ElementName}' as excluded",
 262                difference.ElementType,
 263                difference.ElementName);
 64
 265            return difference;
 66        }
 67
 68        // Classify based on change type and breaking change rules
 669        switch (difference.ChangeType)
 70        {
 71            case ChangeType.Added:
 272                ClassifyAddedChange(difference);
 273                break;
 74
 75            case ChangeType.Removed:
 276                ClassifyRemovedChange(difference);
 277                break;
 78
 79            case ChangeType.Modified:
 280                ClassifyModifiedChange(difference);
 281                break;
 82
 83            case ChangeType.Moved:
 084                ClassifyMovedChange(difference);
 085                break;
 86        }
 87
 688        _logger.LogDebug(
 689            "Classified {ElementType} '{ElementName}' as {ChangeType}, Breaking: {IsBreaking}",
 690            difference.ElementType,
 691            difference.ElementName,
 692            difference.ChangeType,
 693            difference.IsBreakingChange);
 94
 695        return difference;
 896    }
 97
 98    /// <summary>
 99    /// Determines if a type should be excluded from comparison
 100    /// </summary>
 101    /// <param name="typeName">The fully qualified type name</param>
 102    /// <returns>True if the type should be excluded, false otherwise</returns>
 103    public bool IsTypeExcluded(string typeName)
 13104    {
 13105        if (string.IsNullOrWhiteSpace(typeName))
 0106        {
 0107            return false;
 108        }
 109
 110        // Check exact matches first
 13111        if (_exclusionConfig.ExcludedTypes.Contains(typeName))
 3112        {
 3113            _logger.LogDebug("Type '{TypeName}' excluded by exact match", typeName);
 3114            return true;
 115        }
 116
 117        // Check pattern matches
 32118        foreach (var pattern in _typePatternCache.Keys)
 2119        {
 2120            if (_typePatternCache[pattern].IsMatch(typeName))
 2121            {
 2122                _logger.LogDebug("Type '{TypeName}' excluded by pattern '{Pattern}'", typeName, pattern);
 2123                return true;
 124            }
 0125        }
 126
 8127        return false;
 13128    }
 129
 130    /// <summary>
 131    /// Determines if a member should be excluded from comparison
 132    /// </summary>
 133    /// <param name="memberName">The fully qualified member name</param>
 134    /// <returns>True if the member should be excluded, false otherwise</returns>
 135    public bool IsMemberExcluded(string memberName)
 6136    {
 6137        if (string.IsNullOrWhiteSpace(memberName))
 0138        {
 0139            return false;
 140        }
 141
 142        // Check exact matches first
 6143        if (_exclusionConfig.ExcludedMembers.Contains(memberName))
 2144        {
 2145            _logger.LogDebug("Member '{MemberName}' excluded by exact match", memberName);
 2146            return true;
 147        }
 148
 149        // Check pattern matches
 13150        foreach (var pattern in _memberPatternCache.Keys)
 1151        {
 1152            if (_memberPatternCache[pattern].IsMatch(memberName))
 1153            {
 1154                _logger.LogDebug("Member '{MemberName}' excluded by pattern '{Pattern}'", memberName, pattern);
 1155                return true;
 156            }
 0157        }
 158
 159        // Check if the declaring type is excluded
 3160        int lastDotIndex = memberName.LastIndexOf('.');
 3161        if (lastDotIndex > 0)
 3162        {
 3163            string declaringTypeName = memberName.Substring(0, lastDotIndex);
 3164            if (IsTypeExcluded(declaringTypeName))
 2165            {
 2166                _logger.LogDebug("Member '{MemberName}' excluded because its declaring type is excluded", memberName);
 2167                return true;
 168            }
 1169        }
 170
 1171        return false;
 6172    }
 173
 174    /// <summary>
 175    /// Converts a wildcard pattern to a regular expression
 176    /// </summary>
 177    /// <param name="pattern">The wildcard pattern</param>
 178    /// <returns>A regular expression pattern</returns>
 179    private static string WildcardToRegex(string pattern)
 3180    {
 3181        return "^" + Regex.Escape(pattern)
 3182                          .Replace("\\*", ".*")
 3183                          .Replace("\\?", ".") + "$";
 3184    }
 185
 186    /// <summary>
 187    /// Initializes the regex pattern caches for type and member exclusion patterns
 188    /// </summary>
 189    private void InitializePatternCaches()
 23190    {
 191        // Convert type patterns to regex
 73192        foreach (var pattern in _exclusionConfig.ExcludedTypePatterns)
 2193        {
 194            try
 2195            {
 2196                var regex = new Regex(WildcardToRegex(pattern), RegexOptions.Compiled);
 2197                _typePatternCache[pattern] = regex;
 2198            }
 0199            catch (Exception ex)
 0200            {
 0201                _logger.LogWarning(ex, "Invalid type exclusion pattern: {Pattern}", pattern);
 0202            }
 2203        }
 204
 205        // Convert member patterns to regex
 71206        foreach (var pattern in _exclusionConfig.ExcludedMemberPatterns)
 1207        {
 208            try
 1209            {
 1210                var regex = new Regex(WildcardToRegex(pattern), RegexOptions.Compiled);
 1211                _memberPatternCache[pattern] = regex;
 1212            }
 0213            catch (Exception ex)
 0214            {
 0215                _logger.LogWarning(ex, "Invalid member exclusion pattern: {Pattern}", pattern);
 0216            }
 1217        }
 218
 23219        _logger.LogDebug(
 23220            "Initialized exclusion pattern caches with {TypePatternCount} type patterns and {MemberPatternCount} member 
 23221            _typePatternCache.Count,
 23222            _memberPatternCache.Count);
 23223    }
 224
 225    /// <summary>
 226    /// Determines if an API difference should be excluded based on configuration
 227    /// </summary>
 228    /// <param name="difference">The API difference to check</param>
 229    /// <returns>True if the difference should be excluded, false otherwise</returns>
 230    private bool ShouldExcludeElement(ApiDifference difference)
 8231    {
 232        // Check if the element is excluded by name
 8233        switch (difference.ElementType)
 234        {
 235            case ApiElementType.Type:
 7236                return IsTypeExcluded(difference.ElementName);
 237
 238            case ApiElementType.Method:
 239            case ApiElementType.Property:
 240            case ApiElementType.Field:
 241            case ApiElementType.Event:
 242            case ApiElementType.Constructor:
 1243                return IsMemberExcluded(difference.ElementName);
 244
 245            default:
 0246                return false;
 247        }
 8248    }
 249
 250    /// <summary>
 251    /// Classifies an added change based on breaking change rules
 252    /// </summary>
 253    /// <param name="difference">The difference to classify</param>
 254    private void ClassifyAddedChange(ApiDifference difference)
 2255    {
 256        // By default, added changes are not breaking
 2257        difference.IsBreakingChange = false;
 2258        difference.Severity = SeverityLevel.Info;
 259
 260        // Check specific rules for added changes
 2261        if (difference.ElementType == ApiElementType.Type && _breakingChangeRules.TreatAddedTypeAsBreaking)
 1262        {
 1263            difference.IsBreakingChange = true;
 1264            difference.Severity = SeverityLevel.Warning;
 1265        }
 1266        else if (difference.ElementType != ApiElementType.Type && _breakingChangeRules.TreatAddedMemberAsBreaking)
 0267        {
 0268            difference.IsBreakingChange = true;
 0269            difference.Severity = SeverityLevel.Warning;
 0270        }
 2271    }
 272
 273    /// <summary>
 274    /// Classifies a removed change based on breaking change rules
 275    /// </summary>
 276    /// <param name="difference">The difference to classify</param>
 277    private void ClassifyRemovedChange(ApiDifference difference)
 2278    {
 279        // By default, removed changes are breaking
 2280        difference.IsBreakingChange = true;
 2281        difference.Severity = SeverityLevel.Error;
 282
 283        // Check specific rules for removed changes
 2284        if (difference.ElementType == ApiElementType.Type && !_breakingChangeRules.TreatTypeRemovalAsBreaking)
 1285        {
 1286            difference.IsBreakingChange = false;
 1287            difference.Severity = SeverityLevel.Warning;
 1288        }
 1289        else if (difference.ElementType != ApiElementType.Type && !_breakingChangeRules.TreatMemberRemovalAsBreaking)
 0290        {
 0291            difference.IsBreakingChange = false;
 0292            difference.Severity = SeverityLevel.Warning;
 0293        }
 2294    }
 295
 296    /// <summary>
 297    /// Classifies a modified change based on breaking change rules
 298    /// </summary>
 299    /// <param name="difference">The difference to classify</param>
 300    private void ClassifyModifiedChange(ApiDifference difference)
 2301    {
 302        // For modified changes, we need to analyze what changed
 303        // The DifferenceCalculator already set IsBreakingChange based on its analysis
 304        // Here we can refine that classification based on additional rules
 305
 306        // If signature changed and we treat signature changes as breaking
 2307        if (difference.OldSignature != difference.NewSignature && _breakingChangeRules.TreatSignatureChangeAsBreaking)
 1308        {
 1309            difference.IsBreakingChange = true;
 1310            difference.Severity = SeverityLevel.Error;
 1311        }
 312
 313        // If not already classified as breaking, keep the original classification
 1314        else if (!difference.IsBreakingChange)
 1315        {
 1316            difference.Severity = SeverityLevel.Info;
 1317        }
 2318    }
 319
 320    /// <summary>
 321    /// Classifies a moved change based on breaking change rules
 322    /// </summary>
 323    /// <param name="difference">The difference to classify</param>
 324    private void ClassifyMovedChange(ApiDifference difference)
 0325    {
 326        // Moved changes are typically breaking unless configured otherwise
 0327        difference.IsBreakingChange = true;
 0328        difference.Severity = SeverityLevel.Warning;
 0329    }
 330}