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
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
<a id="english"></a>
# Sloc

[English](#english) | [中文](#中文)

[![CI](https://github.com/coldhighsun/Sloc/actions/workflows/ci.yml/badge.svg)](https://github.com/coldhighsun/Sloc/actions/workflows/ci.yml)
[![NuGet Version](https://img.shields.io/nuget/v/Sloc)](https://www.nuget.org/packages/Sloc)
[![NuGet Downloads](https://img.shields.io/nuget/dt/Sloc?label=nuget%20downloads)](https://www.nuget.org/packages/Sloc)
[![GitHub All Releases](https://img.shields.io/github/downloads/coldhighsun/Sloc/total?label=release%20downloads)](https://github.com/coldhighsun/Sloc/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![.NET](https://img.shields.io/badge/.NET-8.0-512BD4)
[![GitHub release](https://img.shields.io/github/v/release/coldhighsun/Sloc)](https://github.com/coldhighsun/Sloc/releases/latest)
![Winget](https://img.shields.io/winget/v/coldhighsun.sloc)
![GitHub last commit](https://img.shields.io/github/last-commit/coldhighsun/Sloc)

---

[English](#english) | [中文](#中文)

Sloc (**S**ource **L**ines **O**f **C**ode) is a .NET global command-line tool for counting lines of source code. It analyzes files individually, distinguishing **code lines**, **comment lines**, and **blank lines**, then aggregates results by programming language. It supports 66 auto-detected languages, five output formats (table, JSON, HTML, CSV, Markdown), per-file detail view, and comment health indicators.
Sloc (**S**ource **L**ines **O**f **C**ode) is a .NET global command-line tool for counting lines of source code.

## Features

Expand Down Expand Up @@ -218,13 +224,7 @@ This project is released under the [MIT License](https://opensource.org/licenses

[English](#english) | [中文](#中文)

[![CI](https://github.com/coldhighsun/Sloc/actions/workflows/ci.yml/badge.svg)](https://github.com/coldhighsun/Sloc/actions/workflows/ci.yml)
[![NuGet Version](https://img.shields.io/nuget/v/Sloc)](https://www.nuget.org/packages/Sloc)
[![NuGet Downloads](https://img.shields.io/nuget/dt/Sloc)](https://www.nuget.org/packages/Sloc)
[![GitHub All Releases](https://img.shields.io/github/downloads/coldhighsun/Sloc/total?label=release%20downloads)](https://github.com/coldhighsun/Sloc/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Sloc(**S**ource **L**ines **O**f **C**ode)是一个用于统计源代码行数的 .NET 全局命令行工具。它会逐文件分析代码,区分**代码行**、**注释行**和**空行**,并按编程语言进行聚合汇总,支持 66 种语言自动识别、五种输出格式(表格、JSON、HTML、CSV、Markdown)、逐文件明细视图以及注释健康度指标。
Sloc(**S**ource **L**ines **O**f **C**ode)是一个用于统计源代码行数的 .NET 全局命令行工具。

## 功能特性

Expand Down
647 changes: 344 additions & 303 deletions src/Sloc.Cli/AnalyzeHandler.cs

Large diffs are not rendered by default.

111 changes: 58 additions & 53 deletions src/Sloc.Cli/Output/CsvRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ public CsvRenderer(TextWriter? writer = null)
}

/// <inheritdoc />
public void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false)
/// <remarks>
/// <paramref name="sourcePath"/> is accepted only to satisfy <see cref="IResultRenderer"/>
/// and is intentionally ignored, for the same reason this renderer has no
/// report-generation-time field (see the class remarks).
/// </remarks>
public void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false, string? sourcePath = null)
{
ArgumentNullException.ThrowIfNull(summary);

Expand All @@ -52,105 +57,94 @@ public void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool det
}
}

private void RenderSkipped(AnalysisSummary summary)
private static string Escape(string field)
{
_writer.Write("\r\n");
WriteRow(["Path", "Reason"]);

foreach (var entry in summary.Skipped)
if (field.IndexOfAny([',', '"', '\r', '\n']) < 0)
{
WriteRow([entry.Path, entry.Reason]);
return field;
}

return "\"" + field.Replace("\"", "\"\"") + "\"";
}

private void RenderByLanguage(AnalysisSummary summary, bool noHealth)
private static string HealthCell(CommentHealthLevel health) =>
health == CommentHealthLevel.NotApplicable ? string.Empty : health.ToString();

private void RenderByFile(AnalysisSummary summary, bool noHealth)
{
var header = new List<string> { "Language", "Files", "Code", "Comment", "Blank", "Total" };
var header = new List<string> { "Path", "Language", "Code", "Comment", "Blank", "Total" };
if (!noHealth)
{
header.Add("Health");
}

WriteRow(header);

foreach (var language in summary.ByLanguage)
foreach (var file in summary.Files)
{
var row = new List<string>
{
language.Language,
language.Files.ToString(),
language.Code.ToString(),
language.Comment.ToString(),
language.Blank.ToString(),
language.Total.ToString()
file.Path,
file.Language,
file.Code.ToString(),
file.Comment.ToString(),
file.Blank.ToString(),
file.Total.ToString()
};
if (!noHealth)
{
row.Add(HealthCell(language.Health));
row.Add(HealthCell(file.Health));
}

WriteRow(row);
}

WriteTotalRow(summary, noHealth, summary.FileCount.ToString());
WriteTotalRow(summary, noHealth, string.Empty);
}

private void RenderByFile(AnalysisSummary summary, bool noHealth)
private void RenderByLanguage(AnalysisSummary summary, bool noHealth)
{
var header = new List<string> { "Path", "Language", "Code", "Comment", "Blank", "Total" };
var header = new List<string> { "Language", "Files", "Code", "Comment", "Blank", "Total" };
if (!noHealth)
{
header.Add("Health");
}

WriteRow(header);

foreach (var file in summary.Files)
foreach (var language in summary.ByLanguage)
{
var row = new List<string>
{
file.Path,
file.Language,
file.Code.ToString(),
file.Comment.ToString(),
file.Blank.ToString(),
file.Total.ToString()
language.Language,
language.Files.ToString(),
language.Code.ToString(),
language.Comment.ToString(),
language.Blank.ToString(),
language.Total.ToString()
};
if (!noHealth)
{
row.Add(HealthCell(file.Health));
row.Add(HealthCell(language.Health));
}

WriteRow(row);
}

WriteTotalRow(summary, noHealth, string.Empty);
WriteTotalRow(summary, noHealth, summary.FileCount.ToString());
}

// The second column is the file/language count for the by-language table and blank for
// the by-file table; every numeric column carries the run-wide total.
private void WriteTotalRow(AnalysisSummary summary, bool noHealth, string secondColumn)
private void RenderSkipped(AnalysisSummary summary)
{
var row = new List<string>
{
"Total",
secondColumn,
summary.Code.ToString(),
summary.Comment.ToString(),
summary.Blank.ToString(),
summary.Total.ToString()
};
if (!noHealth)
_writer.Write("\r\n");
WriteRow(["Path", "Reason"]);

foreach (var entry in summary.Skipped)
{
row.Add(string.Empty);
WriteRow([entry.Path, entry.Reason]);
}

WriteRow(row);
}

private static string HealthCell(CommentHealthLevel health) =>
health == CommentHealthLevel.NotApplicable ? string.Empty : health.ToString();

private void WriteRow(IReadOnlyList<string> fields)
{
var sb = new StringBuilder();
Expand All @@ -169,13 +163,24 @@ private void WriteRow(IReadOnlyList<string> fields)
_writer.Write("\r\n");
}

private static string Escape(string field)
// The second column is the file/language count for the by-language table and blank for
// the by-file table; every numeric column carries the run-wide total.
private void WriteTotalRow(AnalysisSummary summary, bool noHealth, string secondColumn)
{
if (field.IndexOfAny([',', '"', '\r', '\n']) < 0)
var row = new List<string>
{
return field;
"Total",
secondColumn,
summary.Code.ToString(),
summary.Comment.ToString(),
summary.Blank.ToString(),
summary.Total.ToString()
};
if (!noHealth)
{
row.Add(string.Empty);
}

return "\"" + field.Replace("\"", "\"\"") + "\"";
WriteRow(row);
}
}
}
85 changes: 43 additions & 42 deletions src/Sloc.Cli/Output/HtmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,55 +206,15 @@ public HtmlRenderer(TextWriter? writer = null, DateTimeOffset? generatedAt = nul
}

/// <inheritdoc />
public void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false)
public void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false, string? sourcePath = null)
{
ArgumentNullException.ThrowIfNull(summary);

var sb = new StringBuilder();
BuildDocument(sb, summary, byFile, noHealth, detailed);
BuildDocument(sb, summary, byFile, noHealth, detailed, sourcePath);
_writer.Write(sb);
}

private void BuildDocument(StringBuilder sb, AnalysisSummary summary, bool byFile, bool noHealth, bool detailed)
{
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"en\">");
sb.AppendLine("<head>");
sb.AppendLine(" <meta charset=\"UTF-8\">");
sb.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
sb.AppendLine($" <title>{Encode("Sloc Report")}</title>");
sb.AppendLine(" <style>");
sb.AppendLine(Css);
sb.AppendLine(" </style>");
sb.AppendLine("</head>");
sb.AppendLine("<body>");
sb.AppendLine($"<h1>{Encode("Sloc Report")}</h1>");
var generated = _generatedAt.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
sb.AppendLine($"<p class=\"meta\">{"Generated:"} {Encode(generated)} &nbsp;|&nbsp; {summary.FileCount:N0} {"files"} &nbsp;|&nbsp; {summary.Total:N0} {"total lines"}</p>");

if (detailed || !byFile)
{
BuildLanguageSection(sb, summary, noHealth);
}

if (detailed || byFile)
{
BuildFileSection(sb, summary, noHealth);
}

if (summary.Skipped.Count > 0)
{
BuildSkippedSection(sb, summary);
}

sb.AppendLine("<script>");
sb.AppendLine(Script);
sb.AppendLine("</script>");

sb.AppendLine("</body>");
sb.AppendLine("</html>");
}

private static void BuildFileSection(StringBuilder sb, AnalysisSummary summary, bool noHealth)
{
sb.AppendLine($"<h2>{Encode("By File")}</h2>");
Expand Down Expand Up @@ -598,6 +558,47 @@ private static string ToRelative(string path)
}
}

private void BuildDocument(StringBuilder sb, AnalysisSummary summary, bool byFile, bool noHealth, bool detailed, string? sourcePath)
{
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"en\">");
sb.AppendLine("<head>");
sb.AppendLine(" <meta charset=\"UTF-8\">");
sb.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
sb.AppendLine($" <title>{Encode("Sloc Report")}</title>");
sb.AppendLine(" <style>");
sb.AppendLine(Css);
sb.AppendLine(" </style>");
sb.AppendLine("</head>");
sb.AppendLine("<body>");
sb.AppendLine($"<h1>{Encode("Sloc Report")}</h1>");
var generated = _generatedAt.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
var sourceMeta = string.IsNullOrEmpty(sourcePath) ? string.Empty : $" &nbsp;|&nbsp; {"Source:"} {Encode(sourcePath)}";
sb.AppendLine($"<p class=\"meta\">{"Generated:"} {Encode(generated)} &nbsp;|&nbsp; {summary.FileCount:N0} {"files"} &nbsp;|&nbsp; {summary.Total:N0} {"total lines"}{sourceMeta}</p>");

if (detailed || !byFile)
{
BuildLanguageSection(sb, summary, noHealth);
}

if (detailed || byFile)
{
BuildFileSection(sb, summary, noHealth);
}

if (summary.Skipped.Count > 0)
{
BuildSkippedSection(sb, summary);
}

sb.AppendLine("<script>");
sb.AppendLine(Script);
sb.AppendLine("</script>");

sb.AppendLine("</body>");
sb.AppendLine("</html>");
}

private sealed class FileEntry
{
public required FileAnalysis File
Expand Down
7 changes: 6 additions & 1 deletion src/Sloc.Cli/Output/IResultRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ public interface IResultRenderer
/// When <see langword="true"/>, emit both the by-language summary and the per-file
/// breakdown together instead of one or the other.
/// </param>
void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false);
/// <param name="sourcePath">
/// The path, list file, or git commit/tree-ish that was analyzed, included in report
/// metadata (where supported) so a saved or shared report can be traced back to its
/// source. <see langword="null"/> omits it.
/// </param>
void Render(AnalysisSummary summary, bool byFile, bool noHealth, bool detailed = false, string? sourcePath = null);
}
Loading
Loading