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

Large diffs are not rendered by default.

230 changes: 230 additions & 0 deletions src/Sloc.Cli/Analysis/AnalyzeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using Sloc.Core.Models;

namespace Sloc.Cli.Analysis;

/// <summary>
/// The parsed options for an analysis run.
/// </summary>
public sealed class AnalyzeOptions
{
/// <summary>
/// When set, a previously saved JSON report to compare the current run against; the
/// output becomes a diff of line counts rather than the normal report.
/// </summary>
public string? BaselinePath
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, a per-file breakdown is shown.
/// </summary>
public bool ByFile
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, JSON and HTML output includes both the by-language
/// summary and the per-file breakdown together (only meaningful for those formats).
/// </summary>
public bool Detailed
{
get; init;
}

/// <summary>
/// Language display names to exclude (e.g. <c>"Markdown"</c>).
/// </summary>
public IReadOnlyList<string> ExcludeLangs { get; init; } = [];

/// <summary>
/// Glob patterns of files to exclude.
/// </summary>
public IReadOnlyList<string> Excludes { get; init; } = [];

/// <summary>
/// Whether to descend into symlinked/junctioned directories rather than skip them.
/// Defaults to <see langword="false"/>.
/// </summary>
public bool FollowSymlinks
{
get; init;
}

/// <summary>
/// The output format.
/// </summary>
public OutputFormat Format
{
get; init;
}

/// <summary>
/// When set, a commit/tree-ish to analyze the repository tree of as it existed at
/// that commit, without checking it out. <see cref="Path"/> is used as the repo root
/// to query. Mutually exclusive with <see cref="ListFile"/>.
/// </summary>
public string? GitHash
{
get; init;
}

/// <summary>
/// Language display names to include (e.g. <c>"C#"</c>). When empty, all languages
/// are considered.
/// </summary>
public IReadOnlyList<string> IncludeLangs { get; init; } = [];

/// <summary>
/// Glob patterns of files to include.
/// </summary>
public IReadOnlyList<string> Includes { get; init; } = [];

/// <summary>
/// When <see langword="true"/>, files with unknown extensions are included.
/// </summary>
public bool IncludeUnknown
{
get; init;
}

/// <summary>
/// The maximum number of files to analyze in parallel. When <see langword="null"/>
/// or non-positive, <see cref="Environment.ProcessorCount"/> is used. Set to 1 for
/// fully sequential analysis.
/// </summary>
public int? Jobs
{
get; init;
}

/// <summary>
/// When set, a file listing paths (one per line) to analyze directly instead of
/// scanning <see cref="Path"/>. <c>-</c> reads the list from stdin.
/// </summary>
public string? ListFile
{
get; init;
}

/// <summary>
/// When set, the run fails (returns <see cref="ExitCode.ThresholdNotMet"/>) if the
/// overall comment percentage (comment lines / total lines) is below this value.
/// </summary>
public double? MinCommentPct
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, the Complexity column/field is hidden.
/// </summary>
public bool NoComplexity
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, the Comment Health column and percentage
/// breakdowns are hidden.
/// </summary>
public bool NoHealth
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, suppresses the live table and progress bar while
/// still printing the banner and result.
/// </summary>
public bool NoProgress
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, subdirectories are not scanned.
/// </summary>
public bool NoRecursive
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, skips the GitHub check for a newer release.
/// </summary>
public bool NoUpdateCheck
{
get; init;
}

/// <summary>
/// The output file path for Json and Html formats.
/// When <see langword="null"/>, a default name is used.
/// </summary>
public string? OutputFile
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, the per-file console output is paginated.
/// </summary>
public bool Paged
{
get; init;
}

/// <summary>
/// The file or directory to analyze.
/// </summary>
public required string Path
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, suppresses the version banner, progress UI, and the
/// "Saved to" message, leaving only the result output.
/// </summary>
public bool Quiet
{
get; init;
}

/// <summary>
/// Whether to exclude files marked <c>linguist-vendored</c> or <c>linguist-generated</c>
/// in <c>.gitattributes</c> files discovered under the scan root. Defaults to
/// <see langword="true"/>.
/// </summary>
public bool RespectGitAttributes { get; init; } = true;

/// <summary>
/// Whether to honor <c>.gitignore</c> files discovered under the scan root.
/// Defaults to <see langword="true"/>.
/// </summary>
public bool RespectGitignore { get; init; } = true;

/// <summary>
/// The key by which the per-language summary is ordered.
/// </summary>
public LanguageSort Sort { get; init; } = LanguageSort.Total;

/// <summary>
/// When set, keeps only the first this-many languages in the summary after sorting.
/// </summary>
public int? Top
{
get; init;
}

/// <summary>
/// When <see langword="true"/>, files with identical content are only counted once;
/// later duplicates are reported as skipped rather than double-counted.
/// </summary>
public bool Unique
{
get; init;
}
}
27 changes: 27 additions & 0 deletions src/Sloc.Cli/Analysis/ExitCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Sloc.Cli.Analysis;

/// <summary>
/// Process exit codes returned by the CLI.
/// </summary>
public static class ExitCode
{
/// <summary>
/// The requested path was not found or could not be read.
/// </summary>
public const int Error = 1;

/// <summary>
/// The run completed successfully.
/// </summary>
public const int Success = 0;

/// <summary>
/// A configured threshold (e.g. <c>--min-comment-pct</c>) was not met.
/// </summary>
public const int ThresholdNotMet = 2;

/// <summary>
/// An unexpected error occurred.
/// </summary>
public const int Unexpected = 3;
}
69 changes: 69 additions & 0 deletions src/Sloc.Cli/Analysis/LiveAggregator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Sloc.Core.Models;

namespace Sloc.Cli.Analysis;

/// <summary>
/// Thread-safe incremental aggregator of per-language counts, used by
/// <see cref="AnalyzeHandler"/> to refresh the live table without re-aggregating every
/// analyzed file on each tick.
/// </summary>
internal sealed class LiveAggregator(LanguageSort sortBy, int? top)
{
private readonly Dictionary<string, Counts> _byLanguage = new(StringComparer.OrdinalIgnoreCase);
private readonly object _gate = new();
private int _files;

public int FilesProcessed
{
get
{
lock (_gate)
{
return _files;
}
}
}

public void Add(FileAnalysis analysis)
{
lock (_gate)
{
_files++;
_byLanguage.TryGetValue(analysis.Language, out var counts);
_byLanguage[analysis.Language] = new Counts(
counts.Files + 1,
counts.Code + analysis.Code,
counts.Comment + analysis.Comment,
counts.Blank + analysis.Blank,
analysis.Complexity is { } complexity ? counts.ComplexityTotal + complexity : counts.ComplexityTotal,
counts.HasComplexitySupport || analysis.Complexity.HasValue);
}
}

public AnalysisSummary ToSummary()
{
lock (_gate)
{
var byLanguage = _byLanguage
.Select(entry => new LanguageStatistics
{
Language = entry.Key,
Files = entry.Value.Files,
Code = entry.Value.Code,
Comment = entry.Value.Comment,
Blank = entry.Value.Blank,
ComplexityTotal = entry.Value.HasComplexitySupport ? entry.Value.ComplexityTotal : null
});

var ordered = AnalysisSummary.OrderAndLimit(
byLanguage,
sortBy,
descending: sortBy != LanguageSort.Name,
top);

return new AnalysisSummary(ordered, _files);
}
}

private readonly record struct Counts(int Files, int Code, int Comment, int Blank, int ComplexityTotal = 0, bool HasComplexitySupport = false);
}
32 changes: 32 additions & 0 deletions src/Sloc.Cli/Analysis/OutputFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Sloc.Cli.Analysis;

/// <summary>
/// The supported output formats.
/// </summary>
public enum OutputFormat
{
/// <summary>
/// A human-readable, colored table.
/// </summary>
Table,

/// <summary>
/// Machine-readable JSON.
/// </summary>
Json,

/// <summary>
/// A human-readable HTML report.
/// </summary>
Html,

/// <summary>
/// Comma-separated values (spreadsheet-friendly).
/// </summary>
Csv,

/// <summary>
/// GitHub-Flavored Markdown tables (documentation-friendly).
/// </summary>
Markdown
}
Loading