Skip to content

Commit 7cb7211

Browse files
committed
Implement exclusion and breaking change classification (Task 4.3)
1 parent ef0d118 commit 7cb7211

File tree

8 files changed

+807
-5
lines changed

8 files changed

+807
-5
lines changed

src/DotNetApiDiff/ApiExtraction/ApiComparer.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class ApiComparer : IApiComparer
1515
private readonly IApiExtractor _apiExtractor;
1616
private readonly IDifferenceCalculator _differenceCalculator;
1717
private readonly INameMapper _nameMapper;
18+
private readonly IChangeClassifier _changeClassifier;
1819
private readonly ILogger<ApiComparer> _logger;
1920

2021
/// <summary>
@@ -23,16 +24,19 @@ public class ApiComparer : IApiComparer
2324
/// <param name="apiExtractor">API extractor for getting API members</param>
2425
/// <param name="differenceCalculator">Calculator for detailed change analysis</param>
2526
/// <param name="nameMapper">Mapper for namespace and type name transformations</param>
27+
/// <param name="changeClassifier">Classifier for breaking changes and exclusions</param>
2628
/// <param name="logger">Logger for diagnostic information</param>
2729
public ApiComparer(
2830
IApiExtractor apiExtractor,
2931
IDifferenceCalculator differenceCalculator,
3032
INameMapper nameMapper,
33+
IChangeClassifier changeClassifier,
3134
ILogger<ApiComparer> logger)
3235
{
3336
_apiExtractor = apiExtractor ?? throw new ArgumentNullException(nameof(apiExtractor));
3437
_differenceCalculator = differenceCalculator ?? throw new ArgumentNullException(nameof(differenceCalculator));
3538
_nameMapper = nameMapper ?? throw new ArgumentNullException(nameof(nameMapper));
39+
_changeClassifier = changeClassifier ?? throw new ArgumentNullException(nameof(changeClassifier));
3640
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3741
}
3842

@@ -80,10 +84,12 @@ public ComparisonResult CompareAssemblies(Assembly oldAssembly, Assembly newAsse
8084
// Compare types
8185
var typeDifferences = CompareTypes(oldTypes, newTypes).ToList();
8286

83-
// Add the differences to the result
87+
// Classify and add the differences to the result
8488
foreach (var diff in typeDifferences)
8589
{
86-
result.Differences.Add(diff);
90+
// Classify the difference using the change classifier
91+
var classifiedDiff = _changeClassifier.ClassifyChange(diff);
92+
result.Differences.Add(classifiedDiff);
8793
}
8894

8995
// Update summary statistics
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
2+
using System.Text.RegularExpressions;
3+
using DotNetApiDiff.Interfaces;
4+
using DotNetApiDiff.Models;
5+
using DotNetApiDiff.Models.Configuration;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace DotNetApiDiff.ApiExtraction;
9+
10+
/// <summary>
11+
/// Classifies API changes as breaking, non-breaking, or excluded based on configuration rules
12+
/// </summary>
13+
public class ChangeClassifier : IChangeClassifier
14+
{
15+
private readonly BreakingChangeRules _breakingChangeRules;
16+
private readonly ExclusionConfiguration _exclusionConfig;
17+
private readonly ILogger<ChangeClassifier> _logger;
18+
private readonly Dictionary<string, Regex> _typePatternCache = new();
19+
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>
27+
public ChangeClassifier(
28+
BreakingChangeRules breakingChangeRules,
29+
ExclusionConfiguration exclusionConfig,
30+
ILogger<ChangeClassifier> logger)
31+
{
32+
_breakingChangeRules = breakingChangeRules ?? throw new ArgumentNullException(nameof(breakingChangeRules));
33+
_exclusionConfig = exclusionConfig ?? throw new ArgumentNullException(nameof(exclusionConfig));
34+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
35+
36+
// Initialize regex pattern caches
37+
InitializePatternCaches();
38+
}
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)
46+
{
47+
if (difference == null)
48+
{
49+
throw new ArgumentNullException(nameof(difference));
50+
}
51+
52+
// Check if the element should be excluded
53+
if (ShouldExcludeElement(difference))
54+
{
55+
difference.ChangeType = ChangeType.Excluded;
56+
difference.IsBreakingChange = false;
57+
difference.Severity = SeverityLevel.Info;
58+
difference.Description = $"Excluded {difference.ElementType}: {difference.ElementName}";
59+
60+
_logger.LogDebug("Classified {ElementType} '{ElementName}' as excluded",
61+
difference.ElementType, difference.ElementName);
62+
63+
return difference;
64+
}
65+
66+
// Classify based on change type and breaking change rules
67+
switch (difference.ChangeType)
68+
{
69+
case ChangeType.Added:
70+
ClassifyAddedChange(difference);
71+
break;
72+
73+
case ChangeType.Removed:
74+
ClassifyRemovedChange(difference);
75+
break;
76+
77+
case ChangeType.Modified:
78+
ClassifyModifiedChange(difference);
79+
break;
80+
81+
case ChangeType.Moved:
82+
ClassifyMovedChange(difference);
83+
break;
84+
}
85+
86+
_logger.LogDebug("Classified {ElementType} '{ElementName}' as {ChangeType}, Breaking: {IsBreaking}",
87+
difference.ElementType, difference.ElementName, difference.ChangeType, difference.IsBreakingChange);
88+
89+
return difference;
90+
}
91+
92+
/// <summary>
93+
/// Determines if a type should be excluded from comparison
94+
/// </summary>
95+
/// <param name="typeName">The fully qualified type name</param>
96+
/// <returns>True if the type should be excluded, false otherwise</returns>
97+
public bool IsTypeExcluded(string typeName)
98+
{
99+
if (string.IsNullOrWhiteSpace(typeName))
100+
{
101+
return false;
102+
}
103+
104+
// Check exact matches first
105+
if (_exclusionConfig.ExcludedTypes.Contains(typeName))
106+
{
107+
_logger.LogDebug("Type '{TypeName}' excluded by exact match", typeName);
108+
return true;
109+
}
110+
111+
// Check pattern matches
112+
foreach (var pattern in _typePatternCache.Keys)
113+
{
114+
if (_typePatternCache[pattern].IsMatch(typeName))
115+
{
116+
_logger.LogDebug("Type '{TypeName}' excluded by pattern '{Pattern}'", typeName, pattern);
117+
return true;
118+
}
119+
}
120+
121+
return false;
122+
}
123+
124+
/// <summary>
125+
/// Determines if a member should be excluded from comparison
126+
/// </summary>
127+
/// <param name="memberName">The fully qualified member name</param>
128+
/// <returns>True if the member should be excluded, false otherwise</returns>
129+
public bool IsMemberExcluded(string memberName)
130+
{
131+
if (string.IsNullOrWhiteSpace(memberName))
132+
{
133+
return false;
134+
}
135+
136+
// Check exact matches first
137+
if (_exclusionConfig.ExcludedMembers.Contains(memberName))
138+
{
139+
_logger.LogDebug("Member '{MemberName}' excluded by exact match", memberName);
140+
return true;
141+
}
142+
143+
// Check pattern matches
144+
foreach (var pattern in _memberPatternCache.Keys)
145+
{
146+
if (_memberPatternCache[pattern].IsMatch(memberName))
147+
{
148+
_logger.LogDebug("Member '{MemberName}' excluded by pattern '{Pattern}'", memberName, pattern);
149+
return true;
150+
}
151+
}
152+
153+
// Check if the declaring type is excluded
154+
int lastDotIndex = memberName.LastIndexOf('.');
155+
if (lastDotIndex > 0)
156+
{
157+
string declaringTypeName = memberName.Substring(0, lastDotIndex);
158+
if (IsTypeExcluded(declaringTypeName))
159+
{
160+
_logger.LogDebug("Member '{MemberName}' excluded because its declaring type is excluded", memberName);
161+
return true;
162+
}
163+
}
164+
165+
return false;
166+
}
167+
168+
/// <summary>
169+
/// Initializes the regex pattern caches for type and member exclusion patterns
170+
/// </summary>
171+
private void InitializePatternCaches()
172+
{
173+
// Convert type patterns to regex
174+
foreach (var pattern in _exclusionConfig.ExcludedTypePatterns)
175+
{
176+
try
177+
{
178+
var regex = new Regex(WildcardToRegex(pattern), RegexOptions.Compiled);
179+
_typePatternCache[pattern] = regex;
180+
}
181+
catch (Exception ex)
182+
{
183+
_logger.LogWarning(ex, "Invalid type exclusion pattern: {Pattern}", pattern);
184+
}
185+
}
186+
187+
// Convert member patterns to regex
188+
foreach (var pattern in _exclusionConfig.ExcludedMemberPatterns)
189+
{
190+
try
191+
{
192+
var regex = new Regex(WildcardToRegex(pattern), RegexOptions.Compiled);
193+
_memberPatternCache[pattern] = regex;
194+
}
195+
catch (Exception ex)
196+
{
197+
_logger.LogWarning(ex, "Invalid member exclusion pattern: {Pattern}", pattern);
198+
}
199+
}
200+
201+
_logger.LogDebug(
202+
"Initialized exclusion pattern caches with {TypePatternCount} type patterns and {MemberPatternCount} member patterns",
203+
_typePatternCache.Count,
204+
_memberPatternCache.Count);
205+
}
206+
207+
/// <summary>
208+
/// Converts a wildcard pattern to a regular expression
209+
/// </summary>
210+
/// <param name="pattern">The wildcard pattern</param>
211+
/// <returns>A regular expression pattern</returns>
212+
private static string WildcardToRegex(string pattern)
213+
{
214+
return "^" + Regex.Escape(pattern)
215+
.Replace("\\*", ".*")
216+
.Replace("\\?", ".") + "$";
217+
}
218+
219+
/// <summary>
220+
/// Determines if an API difference should be excluded based on configuration
221+
/// </summary>
222+
/// <param name="difference">The API difference to check</param>
223+
/// <returns>True if the difference should be excluded, false otherwise</returns>
224+
private bool ShouldExcludeElement(ApiDifference difference)
225+
{
226+
// Check if the element is excluded by name
227+
switch (difference.ElementType)
228+
{
229+
case ApiElementType.Type:
230+
return IsTypeExcluded(difference.ElementName);
231+
232+
case ApiElementType.Method:
233+
case ApiElementType.Property:
234+
case ApiElementType.Field:
235+
case ApiElementType.Event:
236+
case ApiElementType.Constructor:
237+
return IsMemberExcluded(difference.ElementName);
238+
239+
default:
240+
return false;
241+
}
242+
}
243+
244+
/// <summary>
245+
/// Classifies an added change based on breaking change rules
246+
/// </summary>
247+
/// <param name="difference">The difference to classify</param>
248+
private void ClassifyAddedChange(ApiDifference difference)
249+
{
250+
// By default, added changes are not breaking
251+
difference.IsBreakingChange = false;
252+
difference.Severity = SeverityLevel.Info;
253+
254+
// Check specific rules for added changes
255+
if (difference.ElementType == ApiElementType.Type && _breakingChangeRules.TreatAddedTypeAsBreaking)
256+
{
257+
difference.IsBreakingChange = true;
258+
difference.Severity = SeverityLevel.Warning;
259+
}
260+
else if (difference.ElementType != ApiElementType.Type && _breakingChangeRules.TreatAddedMemberAsBreaking)
261+
{
262+
difference.IsBreakingChange = true;
263+
difference.Severity = SeverityLevel.Warning;
264+
}
265+
}
266+
267+
/// <summary>
268+
/// Classifies a removed change based on breaking change rules
269+
/// </summary>
270+
/// <param name="difference">The difference to classify</param>
271+
private void ClassifyRemovedChange(ApiDifference difference)
272+
{
273+
// By default, removed changes are breaking
274+
difference.IsBreakingChange = true;
275+
difference.Severity = SeverityLevel.Error;
276+
277+
// Check specific rules for removed changes
278+
if (difference.ElementType == ApiElementType.Type && !_breakingChangeRules.TreatTypeRemovalAsBreaking)
279+
{
280+
difference.IsBreakingChange = false;
281+
difference.Severity = SeverityLevel.Warning;
282+
}
283+
else if (difference.ElementType != ApiElementType.Type && !_breakingChangeRules.TreatMemberRemovalAsBreaking)
284+
{
285+
difference.IsBreakingChange = false;
286+
difference.Severity = SeverityLevel.Warning;
287+
}
288+
}
289+
290+
/// <summary>
291+
/// Classifies a modified change based on breaking change rules
292+
/// </summary>
293+
/// <param name="difference">The difference to classify</param>
294+
private void ClassifyModifiedChange(ApiDifference difference)
295+
{
296+
// For modified changes, we need to analyze what changed
297+
// The DifferenceCalculator already set IsBreakingChange based on its analysis
298+
// Here we can refine that classification based on additional rules
299+
300+
// If signature changed and we treat signature changes as breaking
301+
if (difference.OldSignature != difference.NewSignature && _breakingChangeRules.TreatSignatureChangeAsBreaking)
302+
{
303+
difference.IsBreakingChange = true;
304+
difference.Severity = SeverityLevel.Error;
305+
}
306+
// If not already classified as breaking, keep the original classification
307+
else if (!difference.IsBreakingChange)
308+
{
309+
difference.Severity = SeverityLevel.Info;
310+
}
311+
}
312+
313+
/// <summary>
314+
/// Classifies a moved change based on breaking change rules
315+
/// </summary>
316+
/// <param name="difference">The difference to classify</param>
317+
private void ClassifyMovedChange(ApiDifference difference)
318+
{
319+
// Moved changes are typically breaking unless configured otherwise
320+
difference.IsBreakingChange = true;
321+
difference.Severity = SeverityLevel.Warning;
322+
}
323+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
2+
using DotNetApiDiff.Models;
3+
4+
namespace DotNetApiDiff.Interfaces;
5+
6+
/// <summary>
7+
/// Interface for classifying API changes as breaking, non-breaking, or excluded
8+
/// </summary>
9+
public interface IChangeClassifier
10+
{
11+
/// <summary>
12+
/// Classifies an API difference as breaking, non-breaking, or excluded
13+
/// </summary>
14+
/// <param name="difference">The API difference to classify</param>
15+
/// <returns>The classified API difference with updated properties</returns>
16+
ApiDifference ClassifyChange(ApiDifference difference);
17+
18+
/// <summary>
19+
/// Determines if a type should be excluded from comparison
20+
/// </summary>
21+
/// <param name="typeName">The fully qualified type name</param>
22+
/// <returns>True if the type should be excluded, false otherwise</returns>
23+
bool IsTypeExcluded(string typeName);
24+
25+
/// <summary>
26+
/// Determines if a member should be excluded from comparison
27+
/// </summary>
28+
/// <param name="memberName">The fully qualified member name</param>
29+
/// <returns>True if the member should be excluded, false otherwise</returns>
30+
bool IsMemberExcluded(string memberName);
31+
}

0 commit comments

Comments
 (0)