aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-23 01:51:10 +0900
committernsfisis <nsfisis@gmail.com>2026-02-23 01:59:59 +0900
commiteb1e21c059d83f0af9786e4d3cace80afe8456a2 (patch)
tree750d5f2816c9086d62066f86255f7dea545ef203 /crates/mozart/src
parent7090482473902f53365f96ba4364dd115e53601a (diff)
downloadphp-mozart-eb1e21c059d83f0af9786e4d3cace80afe8456a2.tar.gz
php-mozart-eb1e21c059d83f0af9786e4d3cace80afe8456a2.tar.zst
php-mozart-eb1e21c059d83f0af9786e4d3cace80afe8456a2.zip
fix(validate): warn on orphan scripts-descriptions/aliases and honor config.lock
- Detect scripts-descriptions and scripts-aliases keys referencing non-existent scripts and emit warnings matching Composer's behavior - Respect config.lock=false in composer.json to skip lock file checks unless --check-lock is explicitly passed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src')
-rw-r--r--crates/mozart/src/commands/self_update.rs7
-rw-r--r--crates/mozart/src/commands/validate.rs144
2 files changed, 147 insertions, 4 deletions
diff --git a/crates/mozart/src/commands/self_update.rs b/crates/mozart/src/commands/self_update.rs
index 0c2f0f6..a0cd59d 100644
--- a/crates/mozart/src/commands/self_update.rs
+++ b/crates/mozart/src/commands/self_update.rs
@@ -460,9 +460,10 @@ fn clean_backups(data_dir: &Path, except: Option<&Path>) -> anyhow::Result<()> {
if is_backup {
if let Some(exc) = except
- && path == exc {
- continue;
- }
+ && path == exc
+ {
+ continue;
+ }
std::fs::remove_file(&path)
.map_err(|e| anyhow::anyhow!("Could not remove backup {}: {e}", path.display()))?;
}
diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs
index 614a1cc..8c6b6c3 100644
--- a/crates/mozart/src/commands/validate.rs
+++ b/crates/mozart/src/commands/validate.rs
@@ -66,6 +66,17 @@ impl ValidationResult {
}
}
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+fn should_check_lock(args: &ValidateArgs, manifest: &serde_json::Value) -> bool {
+ let config_lock_enabled = manifest
+ .get("config")
+ .and_then(|c| c.get("lock"))
+ .and_then(|v| v.as_bool())
+ .unwrap_or(true);
+ (!args.no_check_lock && config_lock_enabled) || args.check_lock
+}
+
// ─── Entry point ─────────────────────────────────────────────────────────────
pub async fn execute(
@@ -126,7 +137,7 @@ pub async fn execute(
// Check lock file freshness
let mut lock_errors: Vec<String> = Vec::new();
- let check_lock = !args.no_check_lock || args.check_lock;
+ let check_lock = should_check_lock(args, &json_value);
if check_lock {
check_lock_freshness(&content, &file, &mut lock_errors);
}
@@ -201,6 +212,7 @@ fn validate_manifest(
check_commit_references(obj, result);
check_empty_psr_prefixes(obj, result);
check_minimum_stability(obj, result);
+ check_scripts_orphans(obj, result);
}
// ─── Individual checks ───────────────────────────────────────────────────────
@@ -396,6 +408,38 @@ fn check_minimum_stability(
}
}
+/// Warn about keys in scripts-descriptions or scripts-aliases that have no matching script.
+fn check_scripts_orphans(
+ obj: &serde_json::Map<String, serde_json::Value>,
+ result: &mut ValidationResult,
+) {
+ let script_keys: std::collections::HashSet<&str> = obj
+ .get("scripts")
+ .and_then(|v| v.as_object())
+ .map(|m| m.keys().map(|k| k.as_str()).collect())
+ .unwrap_or_default();
+
+ if let Some(descriptions) = obj.get("scripts-descriptions").and_then(|v| v.as_object()) {
+ for key in descriptions.keys() {
+ if !script_keys.contains(key.as_str()) {
+ result.warnings.push(format!(
+ "Description for non-existent script \"{key}\" found in \"scripts-descriptions\""
+ ));
+ }
+ }
+ }
+
+ if let Some(aliases) = obj.get("scripts-aliases").and_then(|v| v.as_object()) {
+ for key in aliases.keys() {
+ if !script_keys.contains(key.as_str()) {
+ result.warnings.push(format!(
+ "Aliases for non-existent script \"{key}\" found in \"scripts-aliases\""
+ ));
+ }
+ }
+ }
+}
+
// ─── Dependency validation ───────────────────────────────────────────────
fn validate_dependencies(
@@ -1107,6 +1151,104 @@ mod tests {
assert!(lock_errors[0].contains("not up to date"));
}
+ // ── check_scripts_orphans ──────────────────────────────────────────────
+
+ #[test]
+ fn test_validate_scripts_descriptions_orphan_warns() {
+ let json = r#"{
+ "name": "vendor/pkg",
+ "license": "MIT",
+ "scripts": {"build": "make build"},
+ "scripts-descriptions": {"build": "Build the project", "nonexistent": "Ghost script"}
+ }"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ result
+ .warnings
+ .iter()
+ .any(|w| w.contains("nonexistent") && w.contains("scripts-descriptions")),
+ "expected orphan warning for scripts-descriptions, got: {:?}",
+ result.warnings
+ );
+ assert!(
+ !result
+ .warnings
+ .iter()
+ .any(|w| w.contains("\"build\"") && w.contains("scripts-descriptions")),
+ "should not warn about existing script 'build'"
+ );
+ }
+
+ #[test]
+ fn test_validate_scripts_aliases_orphan_warns() {
+ let json = r#"{
+ "name": "vendor/pkg",
+ "license": "MIT",
+ "scripts": {"build": "make build"},
+ "scripts-aliases": {"build": ["b"], "ghost": ["g"]}
+ }"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ result
+ .warnings
+ .iter()
+ .any(|w| w.contains("ghost") && w.contains("scripts-aliases")),
+ "expected orphan warning for scripts-aliases, got: {:?}",
+ result.warnings
+ );
+ assert!(
+ !result
+ .warnings
+ .iter()
+ .any(|w| w.contains("\"build\"") && w.contains("scripts-aliases")),
+ "should not warn about existing script 'build'"
+ );
+ }
+
+ #[test]
+ fn test_validate_scripts_valid_no_orphan_warning() {
+ let json = r#"{
+ "name": "vendor/pkg",
+ "license": "MIT",
+ "scripts": {"build": "make build", "test": "phpunit"},
+ "scripts-descriptions": {"build": "Build the project", "test": "Run tests"},
+ "scripts-aliases": {"build": ["b"], "test": ["t"]}
+ }"#;
+ let result = parse_and_validate(json, &make_args());
+ assert!(
+ !result
+ .warnings
+ .iter()
+ .any(|w| w.contains("scripts-descriptions") || w.contains("scripts-aliases")),
+ "should produce no orphan warnings when all keys match, got: {:?}",
+ result.warnings
+ );
+ }
+
+ // ── should_check_lock ──────────────────────────────────────────────────
+
+ #[test]
+ fn test_should_check_lock_config_false_disables() {
+ let args = make_args();
+ let manifest = serde_json::json!({"config": {"lock": false}});
+ assert!(!should_check_lock(&args, &manifest));
+ }
+
+ #[test]
+ fn test_should_check_lock_config_false_overridden_by_flag() {
+ let mut args = make_args();
+ args.check_lock = true;
+ let manifest = serde_json::json!({"config": {"lock": false}});
+ assert!(should_check_lock(&args, &manifest));
+ }
+
+ #[test]
+ fn test_should_check_lock_defaults_to_true() {
+ let args = make_args();
+ let manifest = serde_json::json!({"name": "vendor/pkg"});
+ assert!(should_check_lock(&args, &manifest));
+ }
+
// ── Full manifest: valid package ───────────────────────────────────────
#[test]