///|
/// A grouped release task derived from one or more diagnostics.
pub(all) struct RemediationTask {
priority : Int
category : RuleCategory
title : String
actions : Array[String]
rule_ids : Array[String]
blocking : Bool
} derive(Debug, Eq)
///|
pub fn RemediationTask::new(
priority : Int,
category : RuleCategory,
title : String,
actions : Array[String],
rule_ids : Array[String],
blocking : Bool,
) -> RemediationTask {
{ priority, category, title, actions, rule_ids, blocking }
}
///|
/// Ordered plan that turns diagnostics into release work.
pub(all) struct RemediationPlan {
project : String
profile : String
tasks : Array[RemediationTask]
blocking_tasks : Int
advisory_tasks : Int
} derive(Debug, Eq)
///|
pub fn RemediationPlan::new(
project : String,
profile : String,
tasks : Array[RemediationTask],
) -> RemediationPlan {
let mut blocking_tasks = 0
let mut advisory_tasks = 0
for task in tasks {
if task.blocking {
blocking_tasks += 1
} else {
advisory_tasks += 1
}
}
{ project, profile, tasks, blocking_tasks, advisory_tasks }
}
///|
pub fn RemediationPlan::is_release_ready(self : RemediationPlan) -> Bool {
self.blocking_tasks == 0
}
///|
fn category_for_rule(rule_id : String) -> RuleCategory {
match find_rule_definition(rule_id) {
Some(rule) => rule.category
None => Release
}
}
///|
fn priority_for_severity(severity : Severity) -> Int {
match severity {
Error => 10
Warn => 20
Info => 30
}
}
///|
fn task_title(category : RuleCategory, severity : Severity) -> String {
let prefix = match severity {
Error => "Blocker"
Warn => "Release warning"
Info => "Quality improvement"
}
prefix + ": " + category.label()
}
///|
fn add_unique(values : Array[String], value : String) -> Unit {
if !values.contains(value) {
values.push(value)
}
}
///|
fn task_key(category : RuleCategory, severity : Severity) -> String {
category.label() + ":" + severity.label()
}
///|
fn task_index(keys : Array[String], key : String) -> Int? {
let mut index = 0
for item in keys {
if item == key {
return Some(index)
}
index += 1
}
None
}
///|
fn add_diagnostic_to_tasks(
tasks : Array[RemediationTask],
keys : Array[String],
diagnostic : Diagnostic,
) -> Unit {
let category = category_for_rule(diagnostic.code)
let key = task_key(category, diagnostic.severity)
let actions = if diagnostic.remediation.length() == 0 {
[diagnostic.detail]
} else {
diagnostic.remediation
}
match task_index(keys, key) {
Some(index) => {
let current = tasks[index]
let updated_actions = current.actions
let updated_rules = current.rule_ids
for action in actions {
add_unique(updated_actions, action)
}
add_unique(updated_rules, diagnostic.code)
tasks[index] = {
priority: current.priority,
category: current.category,
title: current.title,
actions: updated_actions,
rule_ids: updated_rules,
blocking: current.blocking,
}
}
None => {
keys.push(key)
tasks.push(
RemediationTask::new(
priority_for_severity(diagnostic.severity),
category,
task_title(category, diagnostic.severity),
actions,
[diagnostic.code],
diagnostic.severity == Error || diagnostic.severity == Warn,
),
)
}
}
}
///|
fn sort_tasks(tasks : Array[RemediationTask]) -> Array[RemediationTask] {
let sorted = tasks.copy()
for i in 0.. RemediationPlan {
let tasks : Array[RemediationTask] = []
let keys : Array[String] = []
for diagnostic in report.diagnostics {
add_diagnostic_to_tasks(tasks, keys, diagnostic)
}
RemediationPlan::new(report.root, report.profile, sort_tasks(tasks))
}
///|
pub fn render_remediation_markdown(plan : RemediationPlan) -> String {
let builder = StringBuilder()
builder.write_string("# Moon Doctor remediation plan\n\n")
builder.write_string("Project: " + plan.project + "\n")
builder.write_string("Profile: " + plan.profile + "\n\n")
if plan.tasks.length() == 0 {
builder.write_string(
"No remediation tasks. The project is ready for the selected profile.\n",
)
} else {
let mut number = 1
for task in plan.tasks {
builder.write_string("## " + "\{number}" + ". " + task.title + "\n\n")
builder.write_string("Rules: " + task.rule_ids.join(", ") + "\n\n")
for action in task.actions {
builder.write_string("- " + action + "\n")
}
builder.write_string("\n")
number += 1
}
}
builder.to_string()
}
///|
pub fn remediation_plan_to_json(plan : RemediationPlan) -> Json {
let tasks = plan.tasks.map(task => {
Json::object({
"priority": Json::number(task.priority.to_double()),
"category": Json::string(task.category.label()),
"title": Json::string(task.title),
"actions": Json::array(task.actions.map(Json::string)),
"rule_ids": Json::array(task.rule_ids.map(Json::string)),
"blocking": Json::boolean(task.blocking),
})
})
Json::object({
"project": Json::string(plan.project),
"profile": Json::string(plan.profile),
"blocking_tasks": Json::number(plan.blocking_tasks.to_double()),
"advisory_tasks": Json::number(plan.advisory_tasks.to_double()),
"tasks": Json::array(tasks),
})
}