< Summary

Information
Class: DotNetApiDiff.Reporting.ConsoleFormatter
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Reporting/ConsoleFormatter.cs
Line coverage
33%
Covered lines: 94
Uncovered lines: 186
Coverable lines: 280
Total lines: 410
Line coverage: 33.5%
Branch coverage
40%
Covered branches: 31
Total branches: 76
Branch coverage: 40.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Format(...)25%10429.41%
FormatForTests(...)100%3030100%
FormatHeader(...)0%620%
FormatSummary(...)100%210%
FormatBreakingChanges(...)0%7280%
FormatDetailedChanges(...)0%7280%
FormatChangeGroup(...)0%272160%
FormatChangeDetails(...)0%4260%
EscapeMarkup(...)0%620%

File(s)

/home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Reporting/ConsoleFormatter.cs

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using DotNetApiDiff.Interfaces;
 3using DotNetApiDiff.Models;
 4using Spectre.Console;
 5using System.Text;
 6
 7namespace DotNetApiDiff.Reporting;
 8
 9/// <summary>
 10/// Formatter for console output with colored text for different change types
 11/// </summary>
 12public class ConsoleFormatter : IReportFormatter
 13{
 14    private readonly bool _testMode;
 15
 16    /// <summary>
 17    /// Initializes a new instance of the <see cref="ConsoleFormatter"/> class.
 18    /// </summary>
 19    /// <param name="testMode">Whether to run in test mode (simplified output)</param>
 1220    public ConsoleFormatter(bool testMode = false)
 1221    {
 1222        _testMode = testMode;
 1223    }
 24
 25    /// <summary>
 26    /// Formats a comparison result for console output with colored text
 27    /// </summary>
 28    /// <param name="result">The comparison result to format</param>
 29    /// <returns>Formatted console output as a string</returns>
 30    public string Format(ComparisonResult result)
 931    {
 932        if (_testMode)
 933        {
 934            return FormatForTests(result);
 35        }
 36
 037        var output = new StringBuilder();
 38
 39        // Create header with assembly information
 040        output.AppendLine(FormatHeader(result));
 041        output.AppendLine();
 42
 43        // Format summary statistics
 044        output.AppendLine(FormatSummary(result));
 045        output.AppendLine();
 46
 47        // Format breaking changes (if any)
 048        if (result.HasBreakingChanges)
 049        {
 050            output.AppendLine(FormatBreakingChanges(result));
 051            output.AppendLine();
 052        }
 53
 54        // Format all differences by category
 055        output.AppendLine(FormatDetailedChanges(result));
 56
 057        return output.ToString();
 958    }
 59
 60    private string FormatForTests(ComparisonResult result)
 961    {
 962        var output = new StringBuilder();
 63
 64        // Header
 965        output.AppendLine("API Comparison Report");
 966        output.AppendLine($"Source Assembly: {Path.GetFileName(result.OldAssemblyPath)}");
 967        output.AppendLine($"Target Assembly: {Path.GetFileName(result.NewAssemblyPath)}");
 968        output.AppendLine($"Comparison Date: {result.ComparisonTimestamp:yyyy-MM-dd HH:mm:ss}");
 969        output.AppendLine($"Total Differences: {result.TotalDifferences}");
 70
 971        if (result.HasBreakingChanges)
 772        {
 2473            output.AppendLine($"Breaking Changes: {result.Differences.Count(d => d.IsBreakingChange)}");
 774        }
 75
 976        output.AppendLine();
 77
 78        // Summary
 979        output.AppendLine("Summary");
 980        output.AppendLine($"Added: {result.Summary.AddedCount}");
 981        output.AppendLine($"Removed: {result.Summary.RemovedCount}");
 982        output.AppendLine($"Modified: {result.Summary.ModifiedCount}");
 983        output.AppendLine($"Breaking Changes: {result.Summary.BreakingChangesCount}");
 984        output.AppendLine($"Total Changes: {result.Summary.TotalChanges}");
 985        output.AppendLine();
 86
 87        // Breaking Changes
 988        if (result.HasBreakingChanges)
 789        {
 790            output.AppendLine("Breaking Changes");
 6291            foreach (var change in result.Differences.Where(d => d.IsBreakingChange))
 1292            {
 1293                output.AppendLine($"{change.ElementType} | {change.ElementName} | {change.Description} | {change.Severit
 1294            }
 95
 796            output.AppendLine();
 797        }
 98
 99        // Group differences by change type
 27100        var addedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Added).ToList();
 27101        var removedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Removed).ToList();
 27102        var modifiedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Modified).ToList();
 27103        var excludedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Excluded).ToList();
 104
 105        // Added Items
 9106        if (addedItems.Any())
 5107        {
 5108            output.AppendLine($"Added Items ({addedItems.Count})");
 25109            foreach (var change in addedItems)
 5110            {
 5111                output.AppendLine($"{change.ElementType} | {change.ElementName} | {change.Description}");
 5112                if (!string.IsNullOrEmpty(change.NewSignature))
 4113                {
 4114                    output.AppendLine($"+ {change.NewSignature}");
 4115                }
 5116            }
 117
 5118            output.AppendLine();
 5119        }
 120
 121        // Removed Items
 9122        if (removedItems.Any())
 6123        {
 6124            output.AppendLine($"Removed Items ({removedItems.Count})");
 30125            foreach (var change in removedItems)
 6126            {
 6127                output.AppendLine($"{change.ElementType} | {change.ElementName} | {change.Description}");
 6128                if (!string.IsNullOrEmpty(change.OldSignature))
 4129                {
 4130                    output.AppendLine($"- {change.OldSignature}");
 4131                }
 6132            }
 133
 6134            output.AppendLine();
 6135        }
 136
 137        // Modified Items
 9138        if (modifiedItems.Any())
 6139        {
 6140            output.AppendLine($"Modified Items ({modifiedItems.Count})");
 30141            foreach (var change in modifiedItems)
 6142            {
 6143                output.AppendLine($"{change.ElementType} | {change.ElementName} | {change.Description}");
 6144                if (!string.IsNullOrEmpty(change.OldSignature))
 4145                {
 4146                    output.AppendLine($"- {change.OldSignature}");
 4147                }
 148
 6149                if (!string.IsNullOrEmpty(change.NewSignature))
 4150                {
 4151                    output.AppendLine($"+ {change.NewSignature}");
 4152                }
 6153            }
 154
 6155            output.AppendLine();
 6156        }
 157
 158        // Excluded Items
 9159        if (excludedItems.Any())
 1160        {
 1161            output.AppendLine($"Excluded Items ({excludedItems.Count})");
 5162            foreach (var change in excludedItems)
 1163            {
 1164                output.AppendLine($"{change.ElementType} | {change.ElementName} | {change.Description}");
 1165            }
 1166        }
 167
 9168        return output.ToString();
 9169    }
 170
 171    private string FormatHeader(ComparisonResult result)
 0172    {
 0173        var table = new Table();
 174
 0175        table.AddColumn("API Comparison Report");
 0176        table.AddColumn(new TableColumn("Value").RightAligned());
 177
 0178        table.AddRow("Source Assembly", Path.GetFileName(result.OldAssemblyPath));
 0179        table.AddRow("Target Assembly", Path.GetFileName(result.NewAssemblyPath));
 0180        table.AddRow("Comparison Date", result.ComparisonTimestamp.ToString("yyyy-MM-dd HH:mm:ss"));
 0181        table.AddRow("Total Differences", result.TotalDifferences.ToString());
 182
 0183        if (result.HasBreakingChanges)
 0184        {
 0185            table.AddRow("[bold red]Breaking Changes[/]", $"[bold red]{result.Differences.Count(d => d.IsBreakingChange)
 0186        }
 187
 188        // Render the table to a string using AnsiConsole
 0189        using var writer = new StringWriter();
 0190        var console = AnsiConsole.Create(new AnsiConsoleSettings
 0191        {
 0192            Out = new AnsiConsoleOutput(writer),
 0193            ColorSystem = ColorSystemSupport.Standard
 0194        });
 0195        console.Write(table);
 0196        return writer.ToString();
 0197    }
 198
 199    private string FormatSummary(ComparisonResult result)
 0200    {
 0201        var panel = new Panel(new Rows(
 0202            new Text("Summary Statistics"),
 0203            new Text(" "),
 0204            new Markup($"Added: [green]{result.Summary.AddedCount}[/]"),
 0205            new Markup($"Removed: [red]{result.Summary.RemovedCount}[/]"),
 0206            new Markup($"Modified: [yellow]{result.Summary.ModifiedCount}[/]"),
 0207            new Markup($"Breaking Changes: [bold red]{result.Summary.BreakingChangesCount}[/]"),
 0208            new Text(" "),
 0209            new Markup($"Total Changes: [blue]{result.Summary.TotalChanges}[/]")))
 0210        {
 0211            Header = new PanelHeader("Summary"),
 0212            Border = BoxBorder.Rounded,
 0213            Expand = true
 0214        };
 215
 216        // Render the panel to a string using AnsiConsole
 0217        using var writer = new StringWriter();
 0218        var console = AnsiConsole.Create(new AnsiConsoleSettings
 0219        {
 0220            Out = new AnsiConsoleOutput(writer),
 0221            ColorSystem = ColorSystemSupport.Standard
 0222        });
 0223        console.Write(panel);
 0224        return writer.ToString();
 0225    }
 226
 227    private string FormatBreakingChanges(ComparisonResult result)
 0228    {
 0229        var breakingChanges = result.Differences.Where(d => d.IsBreakingChange).ToList();
 230
 0231        if (!breakingChanges.Any())
 0232        {
 0233            return string.Empty;
 234        }
 235
 0236        var table = new Table();
 0237        table.AddColumn("Type");
 0238        table.AddColumn("Element");
 0239        table.AddColumn("Description");
 0240        table.AddColumn("Severity");
 241
 0242        table.Title = new TableTitle("[bold red]Breaking Changes[/]");
 0243        table.Border = TableBorder.Rounded;
 244
 0245        foreach (var change in breakingChanges)
 0246        {
 0247            string severityText = change.Severity switch
 0248            {
 0249                SeverityLevel.Critical => $"[bold red]{change.Severity}[/]",
 0250                SeverityLevel.Error => $"[red]{change.Severity}[/]",
 0251                SeverityLevel.Warning => $"[yellow]{change.Severity}[/]",
 0252                _ => $"[blue]{change.Severity}[/]"
 0253            };
 254
 0255            table.AddRow(
 0256                change.ElementType.ToString(),
 0257                change.ElementName,
 0258                change.Description,
 0259                severityText);
 0260        }
 261
 262        // Render the table to a string using AnsiConsole
 0263        using var writer = new StringWriter();
 0264        var console = AnsiConsole.Create(new AnsiConsoleSettings
 0265        {
 0266            Out = new AnsiConsoleOutput(writer),
 0267            ColorSystem = ColorSystemSupport.Standard
 0268        });
 0269        console.Write(table);
 0270        return writer.ToString();
 0271    }
 272
 273    private string FormatDetailedChanges(ComparisonResult result)
 0274    {
 0275        var output = new StringBuilder();
 276
 277        // Group differences by change type
 0278        var addedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Added).ToList();
 0279        var removedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Removed).ToList();
 0280        var modifiedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Modified).ToList();
 0281        var excludedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Excluded).ToList();
 282
 283        // Format added items
 0284        if (addedItems.Any())
 0285        {
 0286            output.AppendLine(FormatChangeGroup("Added Items", addedItems, "green"));
 0287            output.AppendLine();
 0288        }
 289
 290        // Format removed items
 0291        if (removedItems.Any())
 0292        {
 0293            output.AppendLine(FormatChangeGroup("Removed Items", removedItems, "red"));
 0294            output.AppendLine();
 0295        }
 296
 297        // Format modified items
 0298        if (modifiedItems.Any())
 0299        {
 0300            output.AppendLine(FormatChangeGroup("Modified Items", modifiedItems, "yellow"));
 0301            output.AppendLine();
 0302        }
 303
 304        // Format excluded items
 0305        if (excludedItems.Any())
 0306        {
 0307            output.AppendLine(FormatChangeGroup("Excluded Items", excludedItems, "gray"));
 0308        }
 309
 0310        return output.ToString();
 0311    }
 312
 313    private string FormatChangeGroup(string title, List<ApiDifference> changes, string color)
 0314    {
 0315        var table = new Table();
 316
 0317        table.AddColumn("Type");
 0318        table.AddColumn("Element");
 0319        table.AddColumn("Details");
 320
 0321        if (changes.Any(c => c.IsBreakingChange))
 0322        {
 0323            table.AddColumn("Breaking");
 0324        }
 325
 326        // Ensure color is not empty or null to avoid Spectre Console parsing errors
 0327        var safeColor = string.IsNullOrWhiteSpace(color) ? "default" : color;
 0328        table.Title = new TableTitle($"[bold {safeColor}]{title}[/] ({changes.Count})");
 0329        table.Border = TableBorder.Rounded;
 330
 331        // Group changes by element type for better organization
 0332        var groupedChanges = changes.GroupBy(c => c.ElementType).OrderBy(g => g.Key);
 333
 0334        foreach (var group in groupedChanges)
 0335        {
 0336            foreach (var change in group.OrderBy(c => c.ElementName))
 0337            {
 0338                var row = new List<string>
 0339                {
 0340                    change.ElementType.ToString(),
 0341                    string.IsNullOrWhiteSpace(change.ElementName) ? "[dim]<unnamed>[/]" : change.ElementName,
 0342                    FormatChangeDetails(change)
 0343                };
 344
 0345                if (changes.Any(c => c.IsBreakingChange))
 0346                {
 0347                    row.Add(change.IsBreakingChange ? "[red]Yes[/]" : "No");
 0348                }
 349
 0350                table.AddRow(row.ToArray());
 0351            }
 352
 353            // Add a separator between groups
 0354            if (group.Key != groupedChanges.Last().Key)
 0355            {
 0356                table.AddEmptyRow();
 0357            }
 0358        }
 359
 360        // Render the table to a string using AnsiConsole
 0361        using var writer = new StringWriter();
 0362        var console = AnsiConsole.Create(new AnsiConsoleSettings
 0363        {
 0364            Out = new AnsiConsoleOutput(writer),
 0365            ColorSystem = ColorSystemSupport.Standard
 0366        });
 0367        console.Write(table);
 0368        return writer.ToString();
 0369    }
 370
 371    private string FormatChangeDetails(ApiDifference change)
 0372    {
 373        // Escape any markup in the description to prevent parsing errors
 0374        var safeDescription = string.IsNullOrWhiteSpace(change.Description)
 0375            ? "No description available"
 0376            : EscapeMarkup(change.Description);
 377
 0378        var details = new StringBuilder(safeDescription);
 379
 0380        if (!string.IsNullOrWhiteSpace(change.OldSignature))
 0381        {
 0382            details.AppendLine();
 0383            details.AppendLine($"[red]- {EscapeMarkup(change.OldSignature)}[/]");
 0384        }
 385
 0386        if (!string.IsNullOrWhiteSpace(change.NewSignature))
 0387        {
 0388            details.AppendLine();
 0389            details.AppendLine($"[green]+ {EscapeMarkup(change.NewSignature)}[/]");
 0390        }
 391
 0392        return details.ToString();
 0393    }
 394
 395    /// <summary>
 396    /// Escapes markup characters to prevent Spectre Console parsing errors
 397    /// </summary>
 398    /// <param name="text">The text to escape</param>
 399    /// <returns>Escaped text safe for Spectre Console</returns>
 400    private string EscapeMarkup(string text)
 0401    {
 0402        if (string.IsNullOrEmpty(text))
 0403        {
 0404            return text;
 405        }
 406
 407        // Escape square brackets which are used for markup
 0408        return text.Replace("[", "[[").Replace("]", "]]");
 0409    }
 410}