///|
pub(all) enum LintLevel {
LintInfo
LintWarning
LintError
} derive(Eq, @debug.Debug)
///|
pub fn LintLevel::to_text(self : LintLevel) -> String {
match self {
LintInfo => "info"
LintWarning => "warning"
LintError => "error"
}
}
///|
pub struct LintIssue {
level : LintLevel
code : String
subject : String
message : String
} derive(Eq, @debug.Debug)
///|
pub fn LintIssue::level(self : LintIssue) -> LintLevel {
self.level
}
///|
pub fn LintIssue::code(self : LintIssue) -> String {
self.code
}
///|
pub fn LintIssue::subject(self : LintIssue) -> String {
self.subject
}
///|
pub fn LintIssue::message(self : LintIssue) -> String {
self.message
}
///|
pub fn LintIssue::to_text(self : LintIssue) -> String {
self.level.to_text() +
" " +
self.code +
" " +
self.subject +
": " +
self.message
}
///|
pub struct LintReport {
issues : Array[LintIssue]
} derive(Eq, @debug.Debug)
///|
pub fn LintReport::issues(self : LintReport) -> Array[LintIssue] {
self.issues.copy()
}
///|
pub fn LintReport::is_clean(self : LintReport) -> Bool {
self.issues.is_empty()
}
///|
pub fn LintReport::has_errors(self : LintReport) -> Bool {
for issue in self.issues {
if issue.level == LintError {
return true
}
}
false
}
///|
pub fn LintReport::has_code(self : LintReport, code : String) -> Bool {
for issue in self.issues {
if issue.code == code {
return true
}
}
false
}
///|
fn rule_has_obligation(rule : GovernanceRule) -> Bool {
rule.approvals > 0 ||
!rule.checks.is_empty() ||
!rule.labels.is_empty() ||
!rule.forbidden.is_empty() ||
rule.max_lines is Some(_) ||
rule.release_note ||
!rule.allow_binary
}
///|
fn owner_has_global_coverage(rule : OwnerRule) -> Bool {
rule.pattern.source() == "**" || rule.pattern.source() == "**/*"
}
///|
pub fn lint_policy(policy : Policy) -> LintReport {
let issues : Array[LintIssue] = []
let mut global_owner = false
for index, owner in policy.owner_rules {
if owner_has_global_coverage(owner) {
global_owner = true
}
for later_index = index + 1
later_index < policy.owner_rules.length()
later_index = later_index + 1 {
let later = policy.owner_rules[later_index]
if owner.pattern.source() == later.pattern.source() {
issues.push({
level: LintWarning,
code: "lint.owner.overridden",
subject: owner.pattern.source(),
message: "a later owner rule with the same pattern always wins",
})
break
}
}
}
if policy.require_owned && !global_owner {
issues.push({
level: LintWarning,
code: "lint.owner.no_global_coverage",
subject: "OWNER",
message: "ownership is required but no ** or **/* fallback owner exists",
})
}
if policy.owner_rules.is_empty() && policy.require_owned {
issues.push({
level: LintError,
code: "lint.owner.none",
subject: "OWNER",
message: "every change will be rejected because ownership is required and no owner rules exist",
})
}
if policy.rules.is_empty() {
issues.push({
level: LintInfo,
code: "lint.rule.none",
subject: "RULE",
message: "the policy relies only on defaults and ownership",
})
}
for rule in policy.rules {
if !rule_has_obligation(rule) {
issues.push({
level: LintWarning,
code: "lint.rule.empty",
subject: rule.name,
message: "the rule matches paths but adds no governance obligation",
})
}
if rule.approvals > 0 && policy.owner_rules.is_empty() {
issues.push({
level: LintWarning,
code: "lint.approval.unscoped",
subject: rule.name,
message: "approval quorum has no owner rules and therefore accepts any principal",
})
}
}
LintReport::{ issues, }
}
///|
pub fn LintReport::to_text(self : LintReport) -> String {
let output = StringBuilder()
output.write_string("MOONCHANGE_LINT 1")
if self.issues.is_empty() {
output.write_string("\nCLEAN yes")
} else {
output.write_string("\nCLEAN no")
for issue in self.issues {
output.write_string("\nISSUE " + issue.to_text())
}
}
output.to_string()
}
///|
pub fn PathDecision::to_text(self : PathDecision) -> String {
let output = StringBuilder()
let max_lines = match self.max_lines {
Some(value) => value.to_string()
None => "-"
}
let release_note = if self.release_note { "yes" } else { "no" }
let binary = if self.allow_binary { "allow" } else { "deny" }
let forbidden = self.forbidden.map(fn(kind) { kind.to_text() })
output.write_string(
"MOONCHANGE_EXPLAIN 1\nPATH " +
self.path +
"\nOWNERS " +
format_optional_csv(self.owners) +
"\nOWNER_PATTERN " +
self.owner_pattern.unwrap_or("-") +
"\nRULES " +
format_optional_csv(self.matched_rules) +
"\nAPPROVALS " +
self.approvals.to_string() +
"\nCHECKS " +
format_optional_csv(self.checks) +
"\nLABELS " +
format_optional_csv(self.labels) +
"\nFORBID " +
format_optional_csv(forbidden) +
"\nMAX_LINES " +
max_lines +
"\nRELEASE_NOTE " +
release_note +
"\nBINARY " +
binary,
)
output.to_string()
}