Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ dotnet_diagnostic.SA1309.severity = none
# SA1200: Using directive should appear within a namespace declaration
dotnet_diagnostic.SA1200.severity = none

dotnet_diagnostic.SA1201.severity = none

dotnet_diagnostic.SA1202.severity = none

dotnet_diagnostic.SA1208.severity = none

# SA1633: File should have header
dotnet_diagnostic.SA1633.severity = none

Expand Down
4 changes: 2 additions & 2 deletions .kiro/specs/dotnet-api-diff/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ Each task should follow this git workflow:
- _Requirements: 2.3, 4.4_

- [ ] 4. Build comparison engine core functionality
- [ ] 4.1 Implement basic API comparison logic
- [x] 4.1 Implement basic API comparison logic
- Create ApiComparer class to identify additions, removals, and modifications
- Implement DifferenceCalculator for detailed change analysis
- Write unit tests comparing known assembly versions
- **Git Workflow**: Create branch `feature/task-4.1-api-comparer`, commit, push, and create PR
- _Requirements: 1.2, 1.3, 8.2, 8.3_

- [ ] 4.2 Add namespace and type mapping capabilities
- [x] 4.2 Add namespace and type mapping capabilities
- Implement NameMapper class for namespace and type name transformations
- Integrate mapping logic into ApiComparer workflow
- Create unit tests for various mapping scenarios including one-to-many mappings
Expand Down
178 changes: 175 additions & 3 deletions src/DotNetApiDiff/ApiExtraction/ApiComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection;
using DotNetApiDiff.Interfaces;
using DotNetApiDiff.Models;
using DotNetApiDiff.Models.Configuration;
using Microsoft.Extensions.Logging;

namespace DotNetApiDiff.ApiExtraction;
Expand All @@ -13,21 +14,25 @@ public class ApiComparer : IApiComparer
{
private readonly IApiExtractor _apiExtractor;
private readonly IDifferenceCalculator _differenceCalculator;
private readonly INameMapper _nameMapper;
private readonly ILogger<ApiComparer> _logger;

/// <summary>
/// Creates a new instance of the ApiComparer
/// </summary>
/// <param name="apiExtractor">API extractor for getting API members</param>
/// <param name="differenceCalculator">Calculator for detailed change analysis</param>
/// <param name="nameMapper">Mapper for namespace and type name transformations</param>
/// <param name="logger">Logger for diagnostic information</param>
public ApiComparer(
IApiExtractor apiExtractor,
IDifferenceCalculator differenceCalculator,
INameMapper nameMapper,
ILogger<ApiComparer> logger)
{
_apiExtractor = apiExtractor ?? throw new ArgumentNullException(nameof(apiExtractor));
_differenceCalculator = differenceCalculator ?? throw new ArgumentNullException(nameof(differenceCalculator));
_nameMapper = nameMapper ?? throw new ArgumentNullException(nameof(nameMapper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

Expand Down Expand Up @@ -133,11 +138,71 @@ public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnum
var oldTypesByFullName = oldTypesList.ToDictionary(t => t.FullName ?? t.Name);
var newTypesByFullName = newTypesList.ToDictionary(t => t.FullName ?? t.Name);

// Create a lookup for mapped types
var mappedTypeLookup = new Dictionary<string, List<Type>>();

// Build the mapped type lookup
foreach (var oldType in oldTypesList)
{
var oldTypeName = oldType.FullName ?? oldType.Name;
var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();

foreach (var mappedName in mappedNames)
{
if (mappedName != oldTypeName)
{
_logger.LogDebug("Mapped type {OldTypeName} to {MappedTypeName}", oldTypeName, mappedName);

if (!mappedTypeLookup.ContainsKey(mappedName))
{
mappedTypeLookup[mappedName] = new List<Type>();
}

mappedTypeLookup[mappedName].Add(oldType);
}
}
}

// Find added types
foreach (var newType in newTypesList)
{
var newTypeName = newType.FullName ?? newType.Name;
if (!oldTypesByFullName.ContainsKey(newTypeName))
bool foundMatch = false;

// Check direct match
if (oldTypesByFullName.ContainsKey(newTypeName))
{
foundMatch = true;
}
else
{
// Check if this type matches any mapped old types
var mappedOldNames = _nameMapper.MapFullTypeName(newTypeName).ToList();

foreach (var mappedName in mappedOldNames)
{
if (oldTypesByFullName.ContainsKey(mappedName))
{
foundMatch = true;
break;
}
}

// Check for auto-mapping if enabled
if (!foundMatch && _nameMapper.ShouldAutoMapType(newTypeName))
{
if (TryFindTypeBySimpleName(newTypeName, oldTypesList, out var matchedOldTypeName))
{
foundMatch = true;
_logger.LogDebug(
"Auto-mapped type {NewTypeName} to {OldTypeName} by simple name",
newTypeName,
matchedOldTypeName);
}
}
}

if (!foundMatch)
{
differences.Add(_differenceCalculator.CalculateAddedType(newType));
}
Expand All @@ -147,16 +212,53 @@ public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnum
foreach (var oldType in oldTypesList)
{
var oldTypeName = oldType.FullName ?? oldType.Name;
if (!newTypesByFullName.ContainsKey(oldTypeName))
bool foundMatch = false;

// Check direct match
if (newTypesByFullName.ContainsKey(oldTypeName))
{
foundMatch = true;
}
else
{
// Check mapped names
var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();

foreach (var mappedName in mappedNames)
{
if (newTypesByFullName.ContainsKey(mappedName))
{
foundMatch = true;
break;
}
}

// Check for auto-mapping if enabled
if (!foundMatch && _nameMapper.ShouldAutoMapType(oldTypeName))
{
if (TryFindTypeBySimpleName(oldTypeName, newTypesList, out var matchedNewTypeName))
{
foundMatch = true;
_logger.LogDebug(
"Auto-mapped type {OldTypeName} to {NewTypeName} by simple name",
oldTypeName,
matchedNewTypeName);
}
}
}

if (!foundMatch)
{
differences.Add(_differenceCalculator.CalculateRemovedType(oldType));
}
}

// Find modified types
// Find modified types - direct matches
foreach (var oldType in oldTypesList)
{
var oldTypeName = oldType.FullName ?? oldType.Name;

// Check direct match first
if (newTypesByFullName.TryGetValue(oldTypeName, out var newType))
{
// Compare the types
Expand All @@ -170,11 +272,81 @@ public IEnumerable<ApiDifference> CompareTypes(IEnumerable<Type> oldTypes, IEnum
differences.Add(typeDifference);
}
}
else
{
// Check mapped names
var mappedNames = _nameMapper.MapFullTypeName(oldTypeName).ToList();

foreach (var mappedName in mappedNames)
{
if (newTypesByFullName.TryGetValue(mappedName, out var mappedNewType))
{
_logger.LogDebug("Comparing mapped types: {OldTypeName} -> {MappedTypeName}", oldTypeName, mappedName);

// Compare the types
var memberDifferences = CompareMembers(oldType, mappedNewType).ToList();
differences.AddRange(memberDifferences);

// Check for type-level changes
var typeDifference = _differenceCalculator.CalculateTypeChanges(oldType, mappedNewType);
if (typeDifference != null)
{
differences.Add(typeDifference);
}

break;
}
}
}
}

return differences;
}

/// <summary>
/// Tries to find a matching type by simple name (without namespace)
/// </summary>
/// <param name="typeName">The type name to find a match for</param>
/// <param name="candidateTypes">List of candidate types to search</param>
/// <param name="matchedTypeName">The matched type name, if found</param>
/// <returns>True if a match was found, false otherwise</returns>
private bool TryFindTypeBySimpleName(string typeName, IEnumerable<Type> candidateTypes, out string? matchedTypeName)
{
matchedTypeName = null;

// Extract simple type name for auto-mapping
int lastDotIndex = typeName.LastIndexOf('.');
if (lastDotIndex <= 0)
{
return false;
}

string simpleTypeName = typeName.Substring(lastDotIndex + 1);

// Look for any type with the same simple name
foreach (var candidateType in candidateTypes)
{
var candidateTypeName = candidateType.FullName ?? candidateType.Name;
int candidateLastDotIndex = candidateTypeName.LastIndexOf('.');

if (candidateLastDotIndex > 0)
{
string candidateSimpleTypeName = candidateTypeName.Substring(candidateLastDotIndex + 1);

if (string.Equals(
simpleTypeName,
candidateSimpleTypeName,
_nameMapper.Configuration.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
matchedTypeName = candidateTypeName;
return true;
}
}
}

return false;
}

/// <summary>
/// Compares members (methods, properties, fields) of two types
/// </summary>
Expand Down
Loading
Loading