///|
pub(all) enum AuditSeverity {
Error
Warning
Info
} derive(Eq, @debug.Debug)
///|
pub(all) enum AuditCode {
DuplicateOutput
OverlappingOutput
MissingInput
EmptyCommand
UnreachableTask
PhonyTaskInfo
} derive(Eq, @debug.Debug)
///|
pub struct AuditFinding {
severity : AuditSeverity
code : AuditCode
task : String
related_task : String?
path : String?
message : String
} derive(Eq, @debug.Debug)
///|
pub struct AuditReport {
stats : ProjectStats
findings : Array[AuditFinding]
} derive(@debug.Debug)
///|
pub fn audit_project(project : Project) -> AuditReport raise MoonforgeError {
let stats = project_stats(project)
let findings = []
let names = list_tasks(project)
let reachable = task_order(project, Some(stats.default_target))
for item in duplicate_outputs(project) {
let (path, left, right) = item
findings.push({
severity: Error,
code: DuplicateOutput,
task: left,
related_task: Some(right),
path: Some(path),
message: "two tasks declare the same output",
})
}
for item in overlapping_output_findings(project) {
let (left, right) = item
findings.push({
severity: Error,
code: OverlappingOutput,
task: left,
related_task: Some(right),
path: None,
message: "task outputs overlap by path prefix",
})
}
for item in missing_input_findings(project) {
let (task_name, input) = item
findings.push({
severity: Error,
code: MissingInput,
task: task_name,
related_task: None,
path: Some(input),
message: "declared input is absent and not produced by a dependency",
})
}
for name in names {
let task = get_task(project, name)
if task.cmd.trim(chars=" \n\r\t").is_empty() {
findings.push({
severity: Error,
code: EmptyCommand,
task: name,
related_task: None,
path: None,
message: "task has an empty command",
})
}
if task.is_effectively_phony() {
findings.push({
severity: Info,
code: PhonyTaskInfo,
task: task.name,
related_task: None,
path: None,
message: "task always runs because it is phony or has no outputs",
})
}
if !reachable.contains(name) {
findings.push({
severity: Warning,
code: UnreachableTask,
task: name,
related_task: None,
path: None,
message: "task is not reachable from the default target",
})
}
}
{ stats, findings }
}
///|
pub fn audit_code_label(code : AuditCode) -> String {
match code {
DuplicateOutput => "duplicate-output"
OverlappingOutput => "overlapping-output"
MissingInput => "missing-input"
EmptyCommand => "empty-command"
UnreachableTask => "unreachable-task"
PhonyTaskInfo => "phony-task"
}
}
///|
pub fn audit_severity_label(severity : AuditSeverity) -> String {
match severity {
Error => "error"
Warning => "warning"
Info => "info"
}
}
///|
pub fn audit_status(report : AuditReport) -> String {
for finding in report.findings {
if finding.severity is Error {
return "error"
}
}
for finding in report.findings {
if finding.severity is Warning {
return "warning"
}
}
"clean"
}
///|
pub fn render_audit_report(
project : Project,
) -> Array[String] raise MoonforgeError {
let report = audit_project(project)
let lines = [
"MoonForge audit",
"status: \{audit_status(report)}",
"tasks: \{report.stats.total_tasks}",
"default target: \{report.stats.default_target}",
"max dependency depth: \{report.stats.max_depth}",
"declared inputs: \{report.stats.total_declared_inputs}",
"declared outputs: \{report.stats.total_declared_outputs}",
"findings: \{report.findings.length()}",
]
for finding in report.findings {
let related = match finding.related_task {
Some(name) => " with \{name}"
None => ""
}
let path = match finding.path {
Some(value) => " path=\{value}"
None => ""
}
lines.push(
"[\{audit_severity_label(finding.severity)}] \{audit_code_label(finding.code)} task=\{finding.task}\{related}\{path}: \{finding.message}",
)
}
lines
}