| | 1 | | // Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT |
| | 2 | | using System.Reflection; |
| | 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 | | /// Compares APIs between two .NET assemblies to identify differences |
| | 12 | | /// </summary> |
| | 13 | | public 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> |
| 20 | 31 | | public ApiComparer( |
| 20 | 32 | | IApiExtractor apiExtractor, |
| 20 | 33 | | IDifferenceCalculator differenceCalculator, |
| 20 | 34 | | INameMapper nameMapper, |
| 20 | 35 | | IChangeClassifier changeClassifier, |
| 20 | 36 | | ComparisonConfiguration configuration, |
| 20 | 37 | | ILogger<ApiComparer> logger) |
| 20 | 38 | | { |
| 20 | 39 | | _apiExtractor = apiExtractor ?? throw new ArgumentNullException(nameof(apiExtractor)); |
| 20 | 40 | | _differenceCalculator = differenceCalculator ?? throw new ArgumentNullException(nameof(differenceCalculator)); |
| 20 | 41 | | _nameMapper = nameMapper ?? throw new ArgumentNullException(nameof(nameMapper)); |
| 20 | 42 | | _changeClassifier = changeClassifier ?? throw new ArgumentNullException(nameof(changeClassifier)); |
| 20 | 43 | | _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); |
| 20 | 44 | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| 20 | 45 | | } |
| | 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) |
| 4 | 54 | | { |
| 4 | 55 | | if (oldAssembly == null) |
| 1 | 56 | | { |
| 1 | 57 | | throw new ArgumentNullException(nameof(oldAssembly)); |
| | 58 | | } |
| | 59 | |
|
| 3 | 60 | | if (newAssembly == null) |
| 1 | 61 | | { |
| 1 | 62 | | throw new ArgumentNullException(nameof(newAssembly)); |
| | 63 | | } |
| | 64 | |
|
| 2 | 65 | | _logger.LogInformation( |
| 2 | 66 | | "Comparing assemblies: {OldAssembly} and {NewAssembly}", |
| 2 | 67 | | oldAssembly.GetName().Name, |
| 2 | 68 | | newAssembly.GetName().Name); |
| | 69 | |
|
| 2 | 70 | | var result = new ComparisonResult |
| 2 | 71 | | { |
| 2 | 72 | | OldAssemblyPath = oldAssembly.Location, |
| 2 | 73 | | NewAssemblyPath = newAssembly.Location, |
| 2 | 74 | | ComparisonTimestamp = DateTime.UtcNow, |
| 2 | 75 | | Configuration = _configuration |
| 2 | 76 | | }; |
| | 77 | |
|
| | 78 | | try |
| 2 | 79 | | { |
| | 80 | | // Extract API members from both assemblies |
| 2 | 81 | | var oldTypes = _apiExtractor.GetPublicTypes(oldAssembly).ToList(); |
| 2 | 82 | | var newTypes = _apiExtractor.GetPublicTypes(newAssembly).ToList(); |
| | 83 | |
|
| 2 | 84 | | _logger.LogDebug( |
| 2 | 85 | | "Found {OldTypeCount} types in old assembly and {NewTypeCount} types in new assembly", |
| 2 | 86 | | oldTypes.Count, |
| 2 | 87 | | newTypes.Count); |
| | 88 | |
|
| | 89 | | // Compare types |
| 2 | 90 | | var typeDifferences = CompareTypes(oldTypes, newTypes).ToList(); |
| | 91 | |
|
| | 92 | | // Classify and add the differences to the result |
| 10 | 93 | | foreach (var diff in typeDifferences) |
| 2 | 94 | | { |
| | 95 | | // Classify the difference using the change classifier |
| 2 | 96 | | var classifiedDiff = _changeClassifier.ClassifyChange(diff); |
| 2 | 97 | | result.Differences.Add(classifiedDiff); |
| 2 | 98 | | } |
| | 99 | |
|
| | 100 | | // Update summary statistics |
| 4 | 101 | | result.Summary.AddedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Added); |
| 4 | 102 | | result.Summary.RemovedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Removed); |
| 4 | 103 | | result.Summary.ModifiedCount = result.Differences.Count(d => d.ChangeType == ChangeType.Modified); |
| 4 | 104 | | result.Summary.BreakingChangesCount = result.Differences.Count(d => d.IsBreakingChange); |
| | 105 | |
|
| 2 | 106 | | _logger.LogInformation( |
| 2 | 107 | | "Comparison complete. Found {TotalDifferences} differences ({AddedCount} added, {RemovedCount} removed, |
| 2 | 108 | | result.TotalDifferences, |
| 2 | 109 | | result.Summary.AddedCount, |
| 2 | 110 | | result.Summary.RemovedCount, |
| 2 | 111 | | result.Summary.ModifiedCount); |
| | 112 | |
|
| 2 | 113 | | return result; |
| | 114 | | } |
| 0 | 115 | | catch (Exception ex) |
| 0 | 116 | | { |
| 0 | 117 | | _logger.LogError( |
| 0 | 118 | | ex, |
| 0 | 119 | | "Error comparing assemblies {OldAssembly} and {NewAssembly}", |
| 0 | 120 | | oldAssembly.GetName().Name, |
| 0 | 121 | | newAssembly.GetName().Name); |
| 0 | 122 | | throw; |
| | 123 | | } |
| 2 | 124 | | } |
| | 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) |
| 8 | 133 | | { |
| 8 | 134 | | if (oldTypes == null) |
| 1 | 135 | | { |
| 1 | 136 | | throw new ArgumentNullException(nameof(oldTypes)); |
| | 137 | | } |
| | 138 | |
|
| 7 | 139 | | if (newTypes == null) |
| 1 | 140 | | { |
| 1 | 141 | | throw new ArgumentNullException(nameof(newTypes)); |
| | 142 | | } |
| | 143 | |
|
| 6 | 144 | | var differences = new List<ApiDifference>(); |
| 6 | 145 | | var oldTypesList = oldTypes.ToList(); |
| 6 | 146 | | var newTypesList = newTypes.ToList(); |
| | 147 | |
|
| | 148 | | // Create dictionaries for faster lookup |
| 13 | 149 | | var oldTypesByFullName = oldTypesList.ToDictionary(t => t.FullName ?? t.Name); |
| 15 | 150 | | var newTypesByFullName = newTypesList.ToDictionary(t => t.FullName ?? t.Name); |
| | 151 | |
|
| | 152 | | // Create a lookup for mapped types |
| 6 | 153 | | var mappedTypeLookup = new Dictionary<string, List<Type>>(); |
| | 154 | |
|
| | 155 | | // Build the mapped type lookup |
| 32 | 156 | | foreach (var oldType in oldTypesList) |
| 7 | 157 | | { |
| 7 | 158 | | var oldTypeName = oldType.FullName ?? oldType.Name; |
| 7 | 159 | | var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList(); |
| | 160 | |
|
| 23 | 161 | | foreach (var mappedName in mappedNames) |
| 1 | 162 | | { |
| 1 | 163 | | if (mappedName != oldTypeName) |
| 0 | 164 | | { |
| 0 | 165 | | _logger.LogDebug("Mapped type {OldTypeName} to {MappedTypeName}", oldTypeName, mappedName); |
| | 166 | |
|
| 0 | 167 | | if (!mappedTypeLookup.ContainsKey(mappedName)) |
| 0 | 168 | | { |
| 0 | 169 | | mappedTypeLookup[mappedName] = new List<Type>(); |
| 0 | 170 | | } |
| | 171 | |
|
| 0 | 172 | | mappedTypeLookup[mappedName].Add(oldType); |
| 0 | 173 | | } |
| 1 | 174 | | } |
| 7 | 175 | | } |
| | 176 | |
|
| | 177 | | // Find added types |
| 36 | 178 | | foreach (var newType in newTypesList) |
| 9 | 179 | | { |
| 9 | 180 | | var newTypeName = newType.FullName ?? newType.Name; |
| 9 | 181 | | bool foundMatch = false; |
| | 182 | |
|
| | 183 | | // Check direct match |
| 9 | 184 | | if (oldTypesByFullName.ContainsKey(newTypeName)) |
| 5 | 185 | | { |
| 5 | 186 | | foundMatch = true; |
| 5 | 187 | | } |
| | 188 | | else |
| 4 | 189 | | { |
| | 190 | | // Check if any old type maps to this new type |
| 20 | 191 | | foreach (var oldType in oldTypesList) |
| 4 | 192 | | { |
| 4 | 193 | | var oldTypeName = oldType.FullName ?? oldType.Name; |
| 4 | 194 | | var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList(); |
| | 195 | |
|
| 14 | 196 | | foreach (var mappedName in mappedNames) |
| 1 | 197 | | { |
| 1 | 198 | | if (string.Equals(mappedName, newTypeName, StringComparison.Ordinal)) |
| 0 | 199 | | { |
| 0 | 200 | | foundMatch = true; |
| 0 | 201 | | _logger.LogDebug( |
| 0 | 202 | | "Found mapped type: {OldTypeName} -> {NewTypeName}", |
| 0 | 203 | | oldTypeName, |
| 0 | 204 | | newTypeName); |
| 0 | 205 | | break; |
| | 206 | | } |
| 1 | 207 | | } |
| | 208 | |
|
| 4 | 209 | | if (foundMatch) |
| 0 | 210 | | { |
| 0 | 211 | | break; |
| | 212 | | } |
| 4 | 213 | | } |
| | 214 | |
|
| | 215 | | // Check for auto-mapping if enabled |
| 4 | 216 | | if (!foundMatch && _nameMapper.ShouldAutoMapType(newTypeName)) |
| 0 | 217 | | { |
| 0 | 218 | | if (TryFindTypeBySimpleName(newTypeName, oldTypesList, out var matchedOldTypeName)) |
| 0 | 219 | | { |
| 0 | 220 | | foundMatch = true; |
| 0 | 221 | | _logger.LogDebug( |
| 0 | 222 | | "Auto-mapped type {NewTypeName} to {OldTypeName} by simple name", |
| 0 | 223 | | newTypeName, |
| 0 | 224 | | matchedOldTypeName); |
| 0 | 225 | | } |
| 0 | 226 | | } |
| 4 | 227 | | } |
| | 228 | |
|
| 9 | 229 | | if (!foundMatch) |
| 4 | 230 | | { |
| 4 | 231 | | differences.Add(_differenceCalculator.CalculateAddedType(newType)); |
| 4 | 232 | | } |
| 9 | 233 | | } |
| | 234 | |
|
| | 235 | | // Find removed types |
| 32 | 236 | | foreach (var oldType in oldTypesList) |
| 7 | 237 | | { |
| 7 | 238 | | var oldTypeName = oldType.FullName ?? oldType.Name; |
| 7 | 239 | | bool foundMatch = false; |
| | 240 | |
|
| | 241 | | // Check direct match |
| 7 | 242 | | if (newTypesByFullName.ContainsKey(oldTypeName)) |
| 5 | 243 | | { |
| 5 | 244 | | foundMatch = true; |
| 5 | 245 | | } |
| | 246 | | else |
| 2 | 247 | | { |
| | 248 | | // Check mapped names |
| 2 | 249 | | var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList(); |
| | 250 | |
|
| 8 | 251 | | foreach (var mappedName in mappedNames) |
| 1 | 252 | | { |
| 1 | 253 | | if (newTypesByFullName.ContainsKey(mappedName)) |
| 0 | 254 | | { |
| 0 | 255 | | foundMatch = true; |
| 0 | 256 | | break; |
| | 257 | | } |
| 1 | 258 | | } |
| | 259 | |
|
| | 260 | | // Check for auto-mapping if enabled |
| 2 | 261 | | if (!foundMatch && _nameMapper.ShouldAutoMapType(oldTypeName)) |
| 0 | 262 | | { |
| 0 | 263 | | if (TryFindTypeBySimpleName(oldTypeName, newTypesList, out var matchedNewTypeName)) |
| 0 | 264 | | { |
| 0 | 265 | | foundMatch = true; |
| 0 | 266 | | _logger.LogDebug( |
| 0 | 267 | | "Auto-mapped type {OldTypeName} to {NewTypeName} by simple name", |
| 0 | 268 | | oldTypeName, |
| 0 | 269 | | matchedNewTypeName); |
| 0 | 270 | | } |
| 0 | 271 | | } |
| 2 | 272 | | } |
| | 273 | |
|
| 7 | 274 | | if (!foundMatch) |
| 2 | 275 | | { |
| 2 | 276 | | differences.Add(_differenceCalculator.CalculateRemovedType(oldType)); |
| 2 | 277 | | } |
| 7 | 278 | | } |
| | 279 | |
|
| | 280 | | // Find modified types - direct matches |
| 32 | 281 | | foreach (var oldType in oldTypesList) |
| 7 | 282 | | { |
| 7 | 283 | | var oldTypeName = oldType.FullName ?? oldType.Name; |
| | 284 | |
|
| | 285 | | // Check direct match first |
| 7 | 286 | | if (newTypesByFullName.TryGetValue(oldTypeName, out var newType)) |
| 5 | 287 | | { |
| | 288 | | // Compare the types |
| 5 | 289 | | var memberDifferences = CompareMembers(oldType, newType).ToList(); |
| 5 | 290 | | differences.AddRange(memberDifferences); |
| | 291 | |
|
| | 292 | | // Check for type-level changes (e.g., accessibility, base class, interfaces) |
| 5 | 293 | | var typeDifference = _differenceCalculator.CalculateTypeChanges(oldType, newType); |
| 5 | 294 | | if (typeDifference != null) |
| 1 | 295 | | { |
| 1 | 296 | | differences.Add(typeDifference); |
| 1 | 297 | | } |
| 5 | 298 | | } |
| | 299 | | else |
| 2 | 300 | | { |
| | 301 | | // Check mapped names |
| 2 | 302 | | var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList(); |
| | 303 | |
|
| 8 | 304 | | foreach (var mappedName in mappedNames) |
| 1 | 305 | | { |
| 1 | 306 | | if (newTypesByFullName.TryGetValue(mappedName, out var mappedNewType)) |
| 0 | 307 | | { |
| 0 | 308 | | _logger.LogDebug("Comparing mapped types: {OldTypeName} -> {MappedTypeName}", oldTypeName, mappe |
| | 309 | |
|
| | 310 | | // Compare the types |
| 0 | 311 | | var memberDifferences = CompareMembers(oldType, mappedNewType).ToList(); |
| 0 | 312 | | 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 |
| 0 | 316 | | var mappedOldTypeName = _nameMapper.MapTypeName(oldType.Name); |
| 0 | 317 | | var areTypeNamesEquivalent = string.Equals(mappedOldTypeName, mappedNewType.Name, StringComparis |
| | 318 | |
|
| 0 | 319 | | var typeDifference = _differenceCalculator.CalculateTypeChanges(oldType, mappedNewType, areTypeN |
| 0 | 320 | | if (typeDifference != null) |
| 0 | 321 | | { |
| 0 | 322 | | differences.Add(typeDifference); |
| 0 | 323 | | } |
| | 324 | |
|
| 0 | 325 | | break; |
| | 326 | | } |
| 1 | 327 | | } |
| 2 | 328 | | } |
| 7 | 329 | | } |
| | 330 | |
|
| 6 | 331 | | return differences; |
| 6 | 332 | | } |
| | 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) |
| 0 | 342 | | { |
| 0 | 343 | | matchedTypeName = null; |
| | 344 | |
|
| | 345 | | // Extract simple type name for auto-mapping |
| 0 | 346 | | int lastDotIndex = typeName.LastIndexOf('.'); |
| 0 | 347 | | if (lastDotIndex <= 0) |
| 0 | 348 | | { |
| 0 | 349 | | return false; |
| | 350 | | } |
| | 351 | |
|
| 0 | 352 | | string simpleTypeName = typeName.Substring(lastDotIndex + 1); |
| | 353 | |
|
| | 354 | | // Look for any type with the same simple name |
| 0 | 355 | | foreach (var candidateType in candidateTypes) |
| 0 | 356 | | { |
| 0 | 357 | | var candidateTypeName = candidateType.FullName ?? candidateType.Name; |
| 0 | 358 | | int candidateLastDotIndex = candidateTypeName.LastIndexOf('.'); |
| | 359 | |
|
| 0 | 360 | | if (candidateLastDotIndex > 0) |
| 0 | 361 | | { |
| 0 | 362 | | string candidateSimpleTypeName = candidateTypeName.Substring(candidateLastDotIndex + 1); |
| | 363 | |
|
| 0 | 364 | | if (string.Equals( |
| 0 | 365 | | simpleTypeName, |
| 0 | 366 | | candidateSimpleTypeName, |
| 0 | 367 | | _nameMapper.Configuration.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal |
| 0 | 368 | | { |
| 0 | 369 | | matchedTypeName = candidateTypeName; |
| 0 | 370 | | return true; |
| | 371 | | } |
| 0 | 372 | | } |
| 0 | 373 | | } |
| | 374 | |
|
| 0 | 375 | | return false; |
| 0 | 376 | | } |
| | 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) |
| 10 | 385 | | { |
| 10 | 386 | | if (oldType == null) |
| 1 | 387 | | { |
| 1 | 388 | | throw new ArgumentNullException(nameof(oldType)); |
| | 389 | | } |
| | 390 | |
|
| 9 | 391 | | if (newType == null) |
| 1 | 392 | | { |
| 1 | 393 | | throw new ArgumentNullException(nameof(newType)); |
| | 394 | | } |
| | 395 | |
|
| 8 | 396 | | var differences = new List<ApiDifference>(); |
| | 397 | |
|
| | 398 | | try |
| 8 | 399 | | { |
| | 400 | | // Extract members from both types |
| 8 | 401 | | var oldMembers = _apiExtractor.ExtractTypeMembers(oldType).ToList(); |
| 8 | 402 | | var newMembers = _apiExtractor.ExtractTypeMembers(newType).ToList(); |
| | 403 | |
|
| 8 | 404 | | _logger.LogDebug( |
| 8 | 405 | | "Found {OldMemberCount} members in old type and {NewMemberCount} members in new type", |
| 8 | 406 | | oldMembers.Count, |
| 8 | 407 | | newMembers.Count); // Find added members (exist in new but not in old) |
| 28 | 408 | | foreach (var newMember in newMembers) |
| 2 | 409 | | { |
| 2 | 410 | | var equivalentOldMember = FindEquivalentMember(newMember, oldMembers); |
| 2 | 411 | | if (equivalentOldMember == null) |
| 1 | 412 | | { |
| 1 | 413 | | _logger.LogDebug("Found added member: {MemberName}", newMember.FullName); |
| 1 | 414 | | var addedDifference = _differenceCalculator.CalculateAddedMember(newMember); |
| 1 | 415 | | differences.Add(addedDifference); |
| 1 | 416 | | } |
| 2 | 417 | | } |
| | 418 | |
|
| | 419 | | // Find removed members (exist in old but not in new) |
| 28 | 420 | | foreach (var oldMember in oldMembers) |
| 2 | 421 | | { |
| 2 | 422 | | var equivalentNewMember = FindEquivalentMember(oldMember, newMembers); |
| 2 | 423 | | if (equivalentNewMember == null) |
| 1 | 424 | | { |
| 1 | 425 | | _logger.LogDebug("Found removed member: {MemberName}", oldMember.FullName); |
| 1 | 426 | | var removedDifference = _differenceCalculator.CalculateRemovedMember(oldMember); |
| 1 | 427 | | differences.Add(removedDifference); |
| 1 | 428 | | } |
| 2 | 429 | | } |
| | 430 | |
|
| | 431 | | // Find modified members (exist in both but with differences) |
| 28 | 432 | | foreach (var oldMember in oldMembers) |
| 2 | 433 | | { |
| 2 | 434 | | var equivalentNewMember = FindEquivalentMember(oldMember, newMembers); |
| 2 | 435 | | if (equivalentNewMember != null) |
| 1 | 436 | | { |
| | 437 | | // Check if the members are truly different or just equivalent via type mappings |
| 1 | 438 | | if (AreSignaturesEquivalent(oldMember.Signature, equivalentNewMember.Signature)) |
| 0 | 439 | | { |
| | 440 | | // Members are equivalent via type mappings - no difference to report |
| 0 | 441 | | _logger.LogDebug( |
| 0 | 442 | | "Members are equivalent via type mappings: {OldSignature} <-> {NewSignature}", |
| 0 | 443 | | oldMember.Signature, |
| 0 | 444 | | equivalentNewMember.Signature); |
| 0 | 445 | | continue; |
| | 446 | | } |
| | 447 | |
|
| | 448 | | // Members match but have other differences beyond type mappings |
| 1 | 449 | | var memberDifference = _differenceCalculator.CalculateMemberChanges(oldMember, equivalentNewMember); |
| 1 | 450 | | if (memberDifference != null) |
| 1 | 451 | | { |
| 1 | 452 | | _logger.LogDebug("Found modified member: {MemberName}", oldMember.FullName); |
| 1 | 453 | | differences.Add(memberDifference); |
| 1 | 454 | | } |
| 1 | 455 | | } |
| 2 | 456 | | } |
| | 457 | |
|
| 8 | 458 | | return differences; |
| | 459 | | } |
| 0 | 460 | | catch (Exception ex) |
| 0 | 461 | | { |
| 0 | 462 | | _logger.LogError( |
| 0 | 463 | | ex, |
| 0 | 464 | | "Error comparing members of types {OldType} and {NewType}", |
| 0 | 465 | | oldType.FullName, |
| 0 | 466 | | newType.FullName); |
| 0 | 467 | | return Enumerable.Empty<ApiDifference>(); |
| | 468 | | } |
| 8 | 469 | | } |
| | 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) |
| 1 | 477 | | { |
| 1 | 478 | | if (string.IsNullOrEmpty(signature)) |
| 0 | 479 | | { |
| 0 | 480 | | return signature; |
| | 481 | | } |
| | 482 | |
|
| 1 | 483 | | var mappedSignature = signature; |
| | 484 | |
|
| | 485 | | // Check if we have type mappings configured |
| 1 | 486 | | if (_nameMapper.Configuration?.TypeMappings == null) |
| 0 | 487 | | { |
| 0 | 488 | | return mappedSignature; |
| | 489 | | } |
| | 490 | |
|
| | 491 | | // Apply all type mappings to the signature |
| 3 | 492 | | foreach (var mapping in _nameMapper.Configuration.TypeMappings) |
| 0 | 493 | | { |
| | 494 | | // Replace the type name in the signature |
| | 495 | | // We need to be careful to only replace whole type names, not partial matches |
| 0 | 496 | | 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 |
| 0 | 499 | | var oldTypeNameOnly = mapping.Key.Split('.').Last(); |
| 0 | 500 | | var newTypeNameOnly = mapping.Value.Split('.').Last(); |
| | 501 | |
|
| | 502 | | // Only if we had a namespace |
| 0 | 503 | | if (oldTypeNameOnly != mapping.Key) |
| 0 | 504 | | { |
| 0 | 505 | | mappedSignature = ReplaceTypeNameInSignature(mappedSignature, oldTypeNameOnly, newTypeNameOnly); |
| 0 | 506 | | } |
| 0 | 507 | | } |
| | 508 | |
|
| 1 | 509 | | return mappedSignature; |
| 1 | 510 | | } |
| | 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) |
| 0 | 520 | | { |
| | 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 |
| 0 | 524 | | 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 |
| 0 | 528 | | result = System.Text.RegularExpressions.Regex.Replace( |
| 0 | 529 | | result, |
| 0 | 530 | | $@"\b{System.Text.RegularExpressions.Regex.Escape(oldTypeName)}\b", |
| 0 | 531 | | 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 |
| 0 | 537 | | result = System.Text.RegularExpressions.Regex.Replace( |
| 0 | 538 | | result, |
| 0 | 539 | | $@"\b{System.Text.RegularExpressions.Regex.Escape(oldTypeName)}(?=\s*\()", |
| 0 | 540 | | newTypeName); |
| | 541 | |
|
| 0 | 542 | | return result; |
| 0 | 543 | | } |
| | 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) |
| 1 | 552 | | { |
| | 553 | | // Apply type mappings to the source signature to see if it matches the target |
| 1 | 554 | | var mappedSourceSignature = ApplyTypeMappingsToSignature(sourceSignature); |
| | 555 | |
|
| 1 | 556 | | return string.Equals(mappedSourceSignature, targetSignature, StringComparison.Ordinal); |
| 1 | 557 | | } |
| | 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) |
| 6 | 566 | | { |
| | 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 |
| 6 | 569 | | var sameNameMember = targetMembers.FirstOrDefault(m => |
| 9 | 570 | | m.Name == sourceMember.Name && |
| 9 | 571 | | m.FullName == sourceMember.FullName); |
| | 572 | |
|
| 6 | 573 | | if (sameNameMember != null) |
| 3 | 574 | | { |
| 3 | 575 | | 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 |
| 9 | 580 | | foreach (var targetMember in targetMembers) |
| 0 | 581 | | { |
| | 582 | | // Check if source maps to target (source signature with mappings applied == target signature) |
| 0 | 583 | | if (AreSignaturesEquivalent(sourceMember.Signature, targetMember.Signature)) |
| 0 | 584 | | { |
| 0 | 585 | | return targetMember; |
| | 586 | | } |
| | 587 | |
|
| | 588 | | // Also check the reverse: if target maps to source (target signature with mappings applied == source signat |
| 0 | 589 | | if (AreSignaturesEquivalent(targetMember.Signature, sourceMember.Signature)) |
| 0 | 590 | | { |
| 0 | 591 | | return targetMember; |
| | 592 | | } |
| 0 | 593 | | } |
| | 594 | |
|
| 3 | 595 | | return null; |
| 6 | 596 | | } |
| | 597 | | } |