< Summary

Information
Class: NexusLabs.Needlr.Generators.BreadcrumbWriter
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/BreadcrumbWriter.cs
Line coverage
91%
Covered lines: 45
Uncovered lines: 4
Coverable lines: 49
Total lines: 132
Line coverage: 91.8%
Branch coverage
83%
Covered branches: 25
Total branches: 30
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Level()100%11100%
.ctor(...)100%11100%
WriteInlineComment(...)100%22100%
WriteVerboseBox(...)66.66%7675%
WriteFileHeader(...)100%22100%
GetRelativeSourcePath(...)88.88%1818100%
PadRight(...)50%2266.66%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/BreadcrumbWriter.cs

#LineLine coverage
 1using System.Text;
 2
 3namespace NexusLabs.Needlr.Generators;
 4
 5/// <summary>
 6/// Helper class for writing documentation breadcrumbs to generated source code.
 7/// Respects the configured <see cref="BreadcrumbLevel"/> to control output verbosity.
 8/// </summary>
 9internal sealed class BreadcrumbWriter
 10{
 11    private const int BoxWidth = 73;
 12    private const string BoxTopLeft = "┌";
 13    private const string BoxTopRight = "┐";
 14    private const string BoxBottomLeft = "└";
 15    private const string BoxBottomRight = "┘";
 16    private const string BoxVertical = "│";
 17    private const string BoxHorizontal = "─";
 18
 29831219    public BreadcrumbLevel Level { get; }
 20
 45821    public BreadcrumbWriter(BreadcrumbLevel level)
 22    {
 45823        Level = level;
 45824    }
 25
 26    /// <summary>
 27    /// Writes a simple inline comment. Only outputs at Minimal or Verbose level.
 28    /// </summary>
 29    public void WriteInlineComment(StringBuilder sb, string indent, string comment)
 30    {
 3049631        if (Level == BreadcrumbLevel.None)
 118732            return;
 33
 2930934        sb.AppendLine($"{indent}// {comment}");
 2930935    }
 36
 37    /// <summary>
 38    /// Writes a verbose box comment with multiple lines. Only outputs at Verbose level.
 39    /// At Minimal level, outputs a simple inline comment with the title.
 40    /// </summary>
 41    public void WriteVerboseBox(StringBuilder sb, string indent, string title, params string[] lines)
 42    {
 1522643        if (Level == BreadcrumbLevel.None)
 044            return;
 45
 1522646        if (Level == BreadcrumbLevel.Minimal)
 47        {
 048            sb.AppendLine($"{indent}// {title}");
 049            return;
 50        }
 51
 52        // Verbose: Full box
 1522653        var horizontalLine = new string('─', BoxWidth);
 1522654        sb.AppendLine($"{indent}// {BoxTopLeft}{horizontalLine}{BoxTopRight}");
 1522655        sb.AppendLine($"{indent}// {BoxVertical} {PadRight(title, BoxWidth - 1)}{BoxVertical}");
 56
 9156657        foreach (var line in lines)
 58        {
 3055759            sb.AppendLine($"{indent}// {BoxVertical} {PadRight(line, BoxWidth - 1)}{BoxVertical}");
 60        }
 61
 1522662        sb.AppendLine($"{indent}// {BoxBottomLeft}{horizontalLine}{BoxBottomRight}");
 1522663    }
 64
 65    /// <summary>
 66    /// Writes the file header with breadcrumb level indicator. Only at Verbose level.
 67    /// </summary>
 68    public void WriteFileHeader(StringBuilder sb, string assemblyName, string fileDescription)
 69    {
 101270        sb.AppendLine("// <auto-generated/>");
 71        // Suppress obsolete-type warnings in generated code. Consumers may mark types
 72        // [Obsolete] to signal "migrate away" while still needing them in DI. Skipping
 73        // their registration would silently break injection; suppressing the warning
 74        // lets the typeof() reference compile under TreatWarningsAsErrors.
 101275        sb.AppendLine("#pragma warning disable CS0618 // Type or member is obsolete");
 101276        sb.AppendLine("#pragma warning disable CS0619 // Type or member is obsolete (error-level)");
 77
 101278        if (Level == BreadcrumbLevel.Verbose)
 79        {
 5980            sb.AppendLine("// ═══════════════════════════════════════════════════════════════════════════════");
 5981            sb.AppendLine($"// {fileDescription}");
 5982            sb.AppendLine($"// Assembly: {assemblyName}");
 5983            sb.AppendLine($"// Breadcrumb Level: {Level}");
 5984            sb.AppendLine("// ═══════════════════════════════════════════════════════════════════════════════");
 85        }
 101286    }
 87
 88    /// <summary>
 89    /// Gets a relative path from an absolute path, using the project directory as base.
 90    /// Falls back to just the filename if project root is unavailable.
 91    /// </summary>
 92    public static string GetRelativeSourcePath(string? absolutePath, string? projectDirectory)
 93    {
 4552894        if (absolutePath == null || absolutePath.Length == 0)
 4552295            return "[unknown]";
 96
 97        // Normalize path separators for cross-platform compatibility
 698        var normalizedPath = absolutePath.Replace("\\", "/");
 99
 6100        if (projectDirectory == null || projectDirectory.Length == 0)
 101        {
 102            // Fallback to just the filename - use LastIndexOf for cross-platform
 2103            var lastSlash = normalizedPath.LastIndexOf('/');
 2104            return lastSlash >= 0 ? normalizedPath.Substring(lastSlash + 1) : normalizedPath;
 105        }
 106
 4107        var normalizedProjectDir = projectDirectory.Replace("\\", "/");
 108        // Ensure project dir doesn't end with slash for consistent comparison
 4109        if (normalizedProjectDir.EndsWith("/"))
 1110            normalizedProjectDir = normalizedProjectDir.Substring(0, normalizedProjectDir.Length - 1);
 111
 112        // Try to make it relative
 4113        if (normalizedPath.StartsWith(normalizedProjectDir, StringComparison.OrdinalIgnoreCase))
 114        {
 3115            var relative = normalizedPath.Substring(normalizedProjectDir.Length);
 3116            if (relative.StartsWith("/"))
 3117                relative = relative.Substring(1);
 3118            return relative;
 119        }
 120
 121        // Couldn't make relative, just return filename
 1122        var lastSlashFallback = normalizedPath.LastIndexOf('/');
 1123        return lastSlashFallback >= 0 ? normalizedPath.Substring(lastSlashFallback + 1) : normalizedPath;
 124    }
 125
 126    private static string PadRight(string text, int totalWidth)
 127    {
 45783128        if (text.Length >= totalWidth)
 0129            return text.Substring(0, totalWidth);
 45783130        return text + new string(' ', totalWidth - text.Length);
 131    }
 132}