aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-04 14:30:22 +0900
committernsfisis <nsfisis@gmail.com>2026-05-04 14:30:22 +0900
commitc59a923669c57adbf6e5eecce7feae59afcf0aac (patch)
treeedecd7142497841bd9fa7b39c29ea5a3cb28ad8d /crates/mozart/src/commands
parent0f381821e3ec32d82e9ad3d574caac1e4182c423 (diff)
downloadphp-mozart-c59a923669c57adbf6e5eecce7feae59afcf0aac.tar.gz
php-mozart-c59a923669c57adbf6e5eecce7feae59afcf0aac.tar.zst
php-mozart-c59a923669c57adbf6e5eecce7feae59afcf0aac.zip
feat(validate): warn on deprecated SPDX license identifiers
Mirror Composer's Util\ConfigValidator::validate() license handling: treat empty string and empty array as missing, accept array form, and emit deprecation warnings (with GPL-specific -only/-or-later suggestions) for identifiers flagged deprecated in the SPDX database.
Diffstat (limited to 'crates/mozart/src/commands')
-rw-r--r--crates/mozart/src/commands/validate.rs183
1 files changed, 176 insertions, 7 deletions
diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs
index e2345c8..061845e 100644
--- a/crates/mozart/src/commands/validate.rs
+++ b/crates/mozart/src/commands/validate.rs
@@ -1,7 +1,15 @@
use clap::Args;
use mozart_core::console::Verbosity;
use mozart_core::console_format;
+use regex::Regex;
use std::path::{Path, PathBuf};
+use std::sync::LazyLock;
+
+static DEPRECATED_GPL_OR_LATER_RE: LazyLock<Regex> =
+ LazyLock::new(|| Regex::new(r"(?i)^[AL]?GPL-[123](\.[01])?\+$").unwrap());
+
+static DEPRECATED_GPL_BARE_RE: LazyLock<Regex> =
+ LazyLock::new(|| Regex::new(r"(?i)^[AL]?GPL-[123](\.[01])?$").unwrap());
#[derive(Args)]
pub struct ValidateArgs {
@@ -261,14 +269,55 @@ fn check_name(obj: &serde_json::Map<String, serde_json::Value>, result: &mut Val
}
}
-/// Check the "license" field: warn if absent.
+/// Check the "license" field: warn if absent or empty, and warn on deprecated SPDX
+/// identifiers. Mirrors Composer's `Util\ConfigValidator::validate()`.
fn check_license(obj: &serde_json::Map<String, serde_json::Value>, result: &mut ValidationResult) {
- if obj.get("license").is_none() {
- result.warnings.push(
- "No license specified, it is recommended to do so. \
- For closed-source software you may use \"proprietary\" as license."
- .to_string(),
- );
+ let no_license_msg = "No license specified, it is recommended to do so. \
+ For closed-source software you may use \"proprietary\" as license."
+ .to_string();
+
+ // Composer's `empty($manifest['license'])` is true for missing, null, "", and [].
+ let licenses: Vec<&str> = match obj.get("license") {
+ None | Some(serde_json::Value::Null) => {
+ result.warnings.push(no_license_msg);
+ return;
+ }
+ Some(serde_json::Value::String(s)) if s.is_empty() => {
+ result.warnings.push(no_license_msg);
+ return;
+ }
+ Some(serde_json::Value::Array(arr)) if arr.is_empty() => {
+ result.warnings.push(no_license_msg);
+ return;
+ }
+ Some(serde_json::Value::String(s)) => vec![s.as_str()],
+ Some(serde_json::Value::Array(arr)) => arr.iter().filter_map(|v| v.as_str()).collect(),
+ // Non-string, non-array — schema validation handles the type error elsewhere.
+ Some(_) => return,
+ };
+
+ for license in licenses {
+ if license == "proprietary" {
+ continue;
+ }
+ if !mozart_core::validation::is_license_deprecated(license) {
+ continue;
+ }
+ let warning = if DEPRECATED_GPL_OR_LATER_RE.is_match(license) {
+ let suggested = format!("{}-or-later", license.replace('+', ""));
+ format!(
+ "License \"{license}\" is a deprecated SPDX license identifier, use \"{suggested}\" instead"
+ )
+ } else if DEPRECATED_GPL_BARE_RE.is_match(license) {
+ format!(
+ "License \"{license}\" is a deprecated SPDX license identifier, use \"{license}-only\" or \"{license}-or-later\" instead"
+ )
+ } else {
+ format!(
+ "License \"{license}\" is a deprecated SPDX license identifier, see https://spdx.org/licenses/"
+ )
+ };
+ result.warnings.push(warning);
}
}
@@ -791,6 +840,126 @@ mod tests {
assert!(!result.warnings.iter().any(|w| w.contains("No license")));
}
+ #[test]
+ fn test_validate_empty_license_string_warns() {
+ let json = r#"{"name": "vendor/pkg", "license": ""}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(result.warnings.iter().any(|w| w.contains("No license")));
+ }
+
+ #[test]
+ fn test_validate_empty_license_array_warns() {
+ let json = r#"{"name": "vendor/pkg", "license": []}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(result.warnings.iter().any(|w| w.contains("No license")));
+ }
+
+ #[test]
+ fn test_validate_proprietary_license_no_warning() {
+ let json = r#"{"name": "vendor/pkg", "license": "proprietary"}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(!result.warnings.iter().any(|w| w.contains("license")));
+ }
+
+ #[test]
+ fn test_validate_unknown_license_no_deprecation_warning() {
+ // Unknown identifiers don't get a deprecation warning here (Composer's
+ // ConfigValidator only checks deprecation, not validity).
+ let json = r#"{"name": "vendor/pkg", "license": "totally-not-a-license"}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ !result
+ .warnings
+ .iter()
+ .any(|w| w.contains("deprecated SPDX"))
+ );
+ }
+
+ #[test]
+ fn test_validate_deprecated_gpl_bare_warns() {
+ let json = r#"{"name": "vendor/pkg", "license": "GPL-2.0"}"#;
+ let result = parse_and_validate(json, &make_args());
+ let warning = result
+ .warnings
+ .iter()
+ .find(|w| w.contains("deprecated SPDX"))
+ .expect("expected deprecation warning");
+ assert!(warning.contains(r#""GPL-2.0""#), "got: {warning}");
+ assert!(warning.contains("\"GPL-2.0-only\""), "got: {warning}");
+ assert!(warning.contains("\"GPL-2.0-or-later\""), "got: {warning}");
+ }
+
+ #[test]
+ fn test_validate_deprecated_gpl_or_later_warns() {
+ let json = r#"{"name": "vendor/pkg", "license": "GPL-3.0+"}"#;
+ let result = parse_and_validate(json, &make_args());
+ let warning = result
+ .warnings
+ .iter()
+ .find(|w| w.contains("deprecated SPDX"))
+ .expect("expected deprecation warning");
+ assert!(warning.contains(r#""GPL-3.0+""#), "got: {warning}");
+ assert!(warning.contains("\"GPL-3.0-or-later\""), "got: {warning}");
+ }
+
+ #[test]
+ fn test_validate_deprecated_agpl_bare_warns() {
+ let json = r#"{"name": "vendor/pkg", "license": "AGPL-1.0"}"#;
+ let result = parse_and_validate(json, &make_args());
+ let warning = result
+ .warnings
+ .iter()
+ .find(|w| w.contains("deprecated SPDX"))
+ .expect("expected deprecation warning");
+ assert!(warning.contains("\"AGPL-1.0-only\""), "got: {warning}");
+ assert!(warning.contains("\"AGPL-1.0-or-later\""), "got: {warning}");
+ }
+
+ #[test]
+ fn test_validate_deprecated_non_gpl_uses_generic_message() {
+ // Pick any non-GPL deprecated identifier; eCos-2.0 was deprecated in SPDX.
+ // If this changes upstream, replace with another deprecated id.
+ let json = r#"{"name": "vendor/pkg", "license": "eCos-2.0"}"#;
+ let result = parse_and_validate(json, &make_args());
+ if let Some(warning) = result
+ .warnings
+ .iter()
+ .find(|w| w.contains("deprecated SPDX"))
+ {
+ assert!(
+ warning.contains("https://spdx.org/licenses/"),
+ "expected generic message, got: {warning}"
+ );
+ }
+ }
+
+ #[test]
+ fn test_validate_array_license_checks_each() {
+ let json = r#"{"name": "vendor/pkg", "license": ["MIT", "GPL-2.0"]}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ result
+ .warnings
+ .iter()
+ .any(|w| w.contains("deprecated SPDX") && w.contains("GPL-2.0")),
+ "expected deprecation warning for GPL-2.0 in array form, got: {:?}",
+ result.warnings,
+ );
+ }
+
+ #[test]
+ fn test_validate_non_deprecated_license_no_warning() {
+ let json = r#"{"name": "vendor/pkg", "license": "MIT"}"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ !result
+ .warnings
+ .iter()
+ .any(|w| w.contains("deprecated SPDX")),
+ "MIT is not deprecated, should not warn",
+ );
+ }
+
// ── check_version_field ────────────────────────────────────────────────
#[test]