// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 clbbbb
///|
pub struct LicenseUse {
path : String
expression : String
licenses : Array[String]
valid : Bool
} derive(Debug, Eq)
///|
pub fn license_use(
path : String,
expression : String,
licenses : Array[String],
valid : Bool,
) -> LicenseUse {
{ path, expression, licenses, valid }
}
///|
pub fn build_inventory(
paths : Array[String],
sources : Array[String],
) -> Array[LicenseUse] {
inventory_from_source_results(scan_sources(paths, sources))
}
///|
pub fn inventory_from_source_results(
results : Array[SourceLicense],
) -> Array[LicenseUse] {
let rows : Array[LicenseUse] = []
for item in results {
if item.found && item.valid {
match parse(item.expression).expr {
Some(expr) =>
rows.push(
license_use(item.path, item.expression, licenses(expr), true),
)
None => rows.push(license_use(item.path, item.expression, [], false))
}
} else {
rows.push(license_use(item.path, item.expression, [], false))
}
}
rows
}
///|
pub fn inventory_licenses(inventory : Array[LicenseUse]) -> Array[String] {
let rows : Array[String] = []
for item in inventory {
for id in item.licenses {
rows.push(id)
}
}
unique_strings(rows)
}
///|
pub fn files_using_license(
inventory : Array[LicenseUse],
id : String,
) -> Array[String] {
let target = canonical_license(id)
let rows : Array[String] = []
for item in inventory {
if contains_string(item.licenses, target) {
rows.push(item.path)
}
}
rows
}
///|
pub fn inventory_report(inventory : Array[LicenseUse]) -> String {
let rows : Array[String] = []
for id in inventory_licenses(inventory) {
rows.push(id + ": " + join_with(files_using_license(inventory, id), ","))
}
if rows.is_empty() {
"no valid license headers"
} else {
join_lines(rows)
}
}
///|
pub fn inventory_policy_errors(
inventory : Array[LicenseUse],
policy : Policy,
) -> Array[String] {
let rows : Array[String] = []
for item in inventory {
if !item.valid {
rows.push(item.path + ": invalid or missing SPDX header")
} else {
for error in policy_errors(item.expression, policy) {
rows.push(item.path + ": " + error)
}
}
}
rows
}
///|
pub fn inventory_policy_report(
inventory : Array[LicenseUse],
policy : Policy,
) -> String {
let rows = inventory_policy_errors(inventory, policy)
if rows.is_empty() {
"ok"
} else {
join_lines(rows)
}
}
///|
pub fn dominant_license(inventory : Array[LicenseUse]) -> String {
let ids = inventory_licenses(inventory)
let mut best = ""
let mut best_count = 0
for id in ids {
let count = files_using_license(inventory, id).length()
if count > best_count {
best = id
best_count = count
}
}
best
}
///|
pub fn mixed_license_warning(
inventory : Array[LicenseUse],
package_license : String,
) -> String {
let package_id = canonical_license(package_license)
let ids = inventory_licenses(inventory)
if ids.is_empty() {
"no valid source license headers"
} else if ids.length() == 1 && contains_string(ids, package_id) {
"source headers match package license"
} else if contains_string(ids, package_id) {
"source headers include additional licenses: " + join_with(ids, ",")
} else {
"source headers do not include package license " +
package_id +
": " +
join_with(ids, ",")
}
}
///|
pub fn inventory_markdown(inventory : Array[LicenseUse]) -> String {
let rows : Array[String] = ["# License inventory", ""]
for id in inventory_licenses(inventory) {
rows.push("## " + id)
rows.push("")
for path in files_using_license(inventory, id) {
rows.push("- `" + path + "`")
}
rows.push("")
}
if rows.length() == 2 {
rows.push("- no valid license headers")
}
join_lines(rows)
}