///|
/// Counted file group used for an explainable project inventory summary.
pub(all) struct MetricGroup {
name : String
files : Int
lines : Int
characters : Int
} derive(Debug, Eq)
///|
pub fn MetricGroup::new(name : String) -> MetricGroup {
{ name, files: 0, lines: 0, characters: 0 }
}
///|
pub fn MetricGroup::add_file(
self : MetricGroup,
file : ProjectFile,
) -> MetricGroup {
{
name: self.name,
files: self.files + 1,
lines: self.lines + file.line_count,
characters: self.characters + file.char_count,
}
}
///|
/// Structural metrics produced entirely from local repository contents.
pub(all) struct ProjectMetrics {
root : String
total_files : Int
total_directories : Int
total_lines : Int
total_characters : Int
moonbit_source : MetricGroup
moonbit_tests : MetricGroup
markdown_docs : MetricGroup
workflows : MetricGroup
config_files : MetricGroup
fixture_files : MetricGroup
example_files : MetricGroup
package_count : Int
local_dependency_edges : Int
ignored_path_count : Int
} derive(Debug, Eq)
///|
fn category_for_file(file : ProjectFile) -> String {
if file.path.has_suffix("_test.mbt") || file.path.has_suffix("_wbtest.mbt") {
"moonbit_tests"
} else if file.path.has_suffix(".mbt") {
"moonbit_source"
} else if file.path.has_suffix(".md") {
"markdown_docs"
} else if file.path.has_prefix(".github/workflows/") {
"workflows"
} else if file.path.has_suffix(".json") ||
file.path.has_suffix(".yml") ||
file.path.has_suffix(".yaml") {
"config_files"
} else if file.path.has_prefix("fixtures/") {
"fixture_files"
} else if file.path.has_prefix("examples/") {
"example_files"
} else {
"other"
}
}
///|
fn update_group(group : MetricGroup, file : ProjectFile) -> MetricGroup {
group.add_file(file)
}
///|
pub fn calculate_project_metrics(
inventory : ProjectInventory,
graph : PackageGraph,
) -> ProjectMetrics {
let mut source = MetricGroup::new("MoonBit source")
let mut tests = MetricGroup::new("MoonBit tests")
let mut docs = MetricGroup::new("Markdown documentation")
let mut workflows = MetricGroup::new("GitHub workflows")
let mut configs = MetricGroup::new("Configuration")
let mut fixtures = MetricGroup::new("Fixtures")
let mut examples = MetricGroup::new("Examples")
let mut characters = 0
for file in inventory.files {
characters += file.char_count
match category_for_file(file) {
"moonbit_source" => source = update_group(source, file)
"moonbit_tests" => tests = update_group(tests, file)
"markdown_docs" => docs = update_group(docs, file)
"workflows" => workflows = update_group(workflows, file)
"config_files" => configs = update_group(configs, file)
"fixture_files" => fixtures = update_group(fixtures, file)
"example_files" => examples = update_group(examples, file)
_ => ()
}
}
{
root: inventory.root,
total_files: inventory.files.length(),
total_directories: inventory.directories.length(),
total_lines: inventory.total_lines(),
total_characters: characters,
moonbit_source: source,
moonbit_tests: tests,
markdown_docs: docs,
workflows,
config_files: configs,
fixture_files: fixtures,
example_files: examples,
package_count: graph.package_count(),
local_dependency_edges: graph.edge_count(),
ignored_path_count: inventory.ignored_paths.length(),
}
}
///|
fn metric_group_to_json(group : MetricGroup) -> Json {
Json::object({
"name": Json::string(group.name),
"files": Json::number(group.files.to_double()),
"lines": Json::number(group.lines.to_double()),
"characters": Json::number(group.characters.to_double()),
})
}
///|
pub fn project_metrics_to_json(metrics : ProjectMetrics) -> Json {
Json::object({
"root": Json::string(metrics.root),
"total_files": Json::number(metrics.total_files.to_double()),
"total_directories": Json::number(metrics.total_directories.to_double()),
"total_lines": Json::number(metrics.total_lines.to_double()),
"total_characters": Json::number(metrics.total_characters.to_double()),
"moonbit_source": metric_group_to_json(metrics.moonbit_source),
"moonbit_tests": metric_group_to_json(metrics.moonbit_tests),
"markdown_docs": metric_group_to_json(metrics.markdown_docs),
"workflows": metric_group_to_json(metrics.workflows),
"config_files": metric_group_to_json(metrics.config_files),
"fixture_files": metric_group_to_json(metrics.fixture_files),
"example_files": metric_group_to_json(metrics.example_files),
"package_count": Json::number(metrics.package_count.to_double()),
"local_dependency_edges": Json::number(
metrics.local_dependency_edges.to_double(),
),
"ignored_path_count": Json::number(metrics.ignored_path_count.to_double()),
})
}
///|
fn maturity_signal(name : String, complete : Bool, evidence : String) -> String {
(if complete { "PASS" } else { "GAP" }) + " " + name + ": " + evidence
}
///|
pub fn render_project_metrics(metrics : ProjectMetrics) -> String {
let builder = StringBuilder()
builder.write_string("Moon Doctor project metrics\n")
builder.write_string("Root: " + metrics.root + "\n")
builder.write_string(
"Files: \{metrics.total_files}, directories: \{metrics.total_directories}, lines: \{metrics.total_lines}\n\n",
)
for
group in [
metrics.moonbit_source,
metrics.moonbit_tests,
metrics.markdown_docs,
metrics.workflows,
metrics.config_files,
metrics.fixture_files,
metrics.example_files,
] {
builder.write_string(
"- " + group.name + ": \{group.files} files, \{group.lines} lines\n",
)
}
builder.write_string("\nMaturity signals\n")
builder.write_string(
maturity_signal(
"source",
metrics.moonbit_source.files > 0,
"\{metrics.moonbit_source.files} source files",
) +
"\n",
)
builder.write_string(
maturity_signal(
"tests",
metrics.moonbit_tests.files > 0,
"\{metrics.moonbit_tests.files} test files",
) +
"\n",
)
builder.write_string(
maturity_signal(
"documentation",
metrics.markdown_docs.files > 0,
"\{metrics.markdown_docs.files} Markdown files",
) +
"\n",
)
builder.write_string(
maturity_signal(
"CI",
metrics.workflows.files > 0,
"\{metrics.workflows.files} workflow files",
) +
"\n",
)
builder.write_string(
maturity_signal(
"packages",
metrics.package_count > 0,
"\{metrics.package_count} packages",
) +
"\n",
)
builder.to_string()
}
///|
pub fn metrics_markdown(metrics : ProjectMetrics) -> String {
let builder = StringBuilder()
builder.write_string("| Metric | Files | Lines |\n| --- | ---: | ---: |\n")
for
group in [
metrics.moonbit_source,
metrics.moonbit_tests,
metrics.markdown_docs,
metrics.workflows,
metrics.config_files,
metrics.fixture_files,
metrics.example_files,
] {
builder.write_string(
"| " + group.name + " | \{group.files} | \{group.lines} |\n",
)
}
builder.to_string()
}