diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-23 01:51:10 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-23 01:59:59 +0900 |
| commit | eb1e21c059d83f0af9786e4d3cace80afe8456a2 (patch) | |
| tree | 750d5f2816c9086d62066f86255f7dea545ef203 /crates/mozart/src/commands/validate.rs | |
| parent | 7090482473902f53365f96ba4364dd115e53601a (diff) | |
| download | php-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/commands/validate.rs')
| -rw-r--r-- | crates/mozart/src/commands/validate.rs | 144 |
1 files changed, 143 insertions, 1 deletions
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] |
