///|
fn has_file(files : Array[String], expected : StringView) -> Bool {
let expected = expected.to_lower().to_owned()
files.any(fn(file) { file.to_lower() == expected })
}
///|
fn has_file_named(files : Array[String], names : Array[String]) -> Bool {
files.any(fn(file) {
let normalized = file.replace_all(old="\\", new="/").to_lower()
names.any(fn(name) {
normalized == name || normalized.has_suffix("/\{name}")
})
})
}
///|
fn has_workflow(files : Array[String]) -> Bool {
files.any(fn(file) {
let normalized = file.replace_all(old="\\", new="/").to_lower()
normalized.has_prefix(".github/workflows/") &&
(normalized.has_suffix(".yml") || normalized.has_suffix(".yaml"))
})
}
///|
fn valid_repository_url(value : StringView) -> Bool {
let value = value.trim()
value.has_prefix("https://") ||
value.has_prefix("http://") ||
(value.has_prefix("git@") && value.contains(":"))
}
///|
fn add_finding(
findings : Array[Finding],
severity : Severity,
code : String,
path : String,
message : String,
) -> Unit {
findings.push({ severity, code, path, message })
}
///|
/// Audit pure scan facts. This function performs no file-system access and is
/// therefore portable and deterministic.
pub fn audit(facts : ScanFacts) -> AuditReport {
let findings = []
let project = facts.project
if project.name.is_empty() {
add_finding(findings, Error, "MS001", "moon.mod", "Module name is missing.")
}
if project.version.is_empty() {
add_finding(
findings,
Error,
"MS002",
"moon.mod",
"Module version is missing.",
)
}
if project.license.is_empty() {
add_finding(
findings,
Error,
"MS003",
"moon.mod",
"SPDX license identifier is missing.",
)
} else if !valid_spdx(project.license) {
add_finding(
findings,
Error,
"MS004",
"moon.mod",
"License is not a recognized SPDX expression: \{project.license}",
)
}
if project.repository.is_empty() {
add_finding(
findings,
Warning,
"MS005",
"moon.mod",
"Repository URL is missing; published packages should link to source.",
)
} else if !valid_repository_url(project.repository) {
add_finding(
findings,
Warning,
"MS014",
"moon.mod",
"Repository is not a recognized HTTP(S) or SSH Git URL.",
)
}
if !has_file_named(facts.files, ["license", "license.md", "license.txt"]) {
add_finding(
findings,
Error,
"MS006",
"LICENSE",
"No license file was found.",
)
}
if !has_file_named(facts.files, [
"readme", "readme.md", "readme.mbt.md", "readme.txt",
]) {
add_finding(
findings,
Error,
"MS007",
"README.md",
"No README file was found.",
)
}
if !has_workflow(facts.files) {
add_finding(
findings,
Warning,
"MS008",
".github/workflows",
"No GitHub Actions workflow was found.",
)
}
if !has_file_named(facts.files, ["changelog.md", "changes.md"]) {
add_finding(
findings,
Warning,
"MS009",
"CHANGELOG.md",
"No changelog was found.",
)
}
if !has_file(facts.files, ".gitignore") {
add_finding(
findings,
Info,
"MS010",
".gitignore",
"Consider adding a .gitignore file.",
)
}
match facts.license_text {
Some(text) =>
match detect_license(text) {
Some(detected) if normalize_license(project.license) != detected =>
add_finding(
findings,
Error,
"MS011",
"LICENSE",
"Manifest declares \{project.license}, but the license text looks like \{detected}.",
)
None =>
add_finding(
findings,
Warning,
"MS012",
"LICENSE",
"License text could not be identified automatically.",
)
_ => ()
}
None => ()
}
if facts.moonbit_source_lines == 0 {
add_finding(
findings,
Error,
"MS013",
"",
"No MoonBit source lines were found.",
)
}
let penalty = findings.fold(init=0, (score, finding) => {
score +
(match finding.severity {
Error => 18
Warning => 6
Info => 1
})
})
let score = if penalty >= 100 { 0 } else { 100 - penalty }
{ project, findings, moonbit_source_lines: facts.moonbit_source_lines, score }
}