//! ref: composer/src/Composer/Command/ValidateCommand.php use anyhow::Result; use shirabe_external_packages::symfony::component::console::input::InputInterface; use shirabe_external_packages::symfony::component::console::output::OutputInterface; use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; use crate::composer::Composer; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::factory::Factory; use crate::io::IOInterface; use crate::package::loader::ValidatingArrayLoader; use crate::plugin::CommandEvent; use crate::plugin::PluginEvents; use crate::util::ConfigValidator; use crate::util::Filesystem; #[derive(Debug)] pub struct ValidateCommand { base_command_data: BaseCommandData, } impl ValidateCommand { pub fn configure(&mut self) { self.set_name("validate") .set_description("Validates a composer.json and composer.lock") .set_definition(&[ InputOption::new( "no-check-all", None, Some(InputOption::VALUE_NONE), "Do not validate requires for overly strict/loose constraints", None, ) .unwrap() .into(), InputOption::new( "check-lock", None, Some(InputOption::VALUE_NONE), "Check if lock file is up to date (even when config.lock is false)", None, ) .unwrap() .into(), InputOption::new( "no-check-lock", None, Some(InputOption::VALUE_NONE), "Do not check if lock file is up to date", None, ) .unwrap() .into(), InputOption::new( "no-check-publish", None, Some(InputOption::VALUE_NONE), "Do not check for publish errors", None, ) .unwrap() .into(), InputOption::new( "no-check-version", None, Some(InputOption::VALUE_NONE), "Do not report a warning if the version field is present", None, ) .unwrap() .into(), InputOption::new( "with-dependencies", Some(shirabe_php_shim::PhpMixed::String("A".to_string())), Some(InputOption::VALUE_NONE), "Also validate the composer.json of all installed dependencies", None, ) .unwrap() .into(), InputOption::new( "strict", None, Some(InputOption::VALUE_NONE), "Return a non-zero exit code for warnings as well as errors", None, ) .unwrap() .into(), InputArgument::new( "file", Some(InputArgument::OPTIONAL), "path to composer.json file", None, ) .unwrap() .into(), ]) .set_help( "The validate command validates a given composer.json and composer.lock\n\n\ Exit codes in case of errors are:\n\ 1 validation warning(s), only when --strict is given\n\ 2 validation error(s)\n\ 3 file unreadable or missing\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#validate", ); } pub fn execute( &mut self, input: &dyn InputInterface, output: &dyn OutputInterface, ) -> Result { let file = input .get_argument("file") .as_string_opt() .map(|s| s.to_string()) .map(Ok) .unwrap_or_else(Factory::get_composer_file)?; // TODO(phase-b): get_io() takes &mut self via BaseCommand; clone_box to release the borrow. let io_box = self.get_io().clone_box(); let io: &dyn IOInterface = io_box.as_ref(); if !std::path::Path::new(&file).exists() { io.write_error(&format!("{} not found.", file)); return Ok(3); } if !Filesystem::is_readable(&file) { io.write_error(&format!("{} is not readable.", file)); return Ok(3); } let validator = ConfigValidator::new(io.clone_box()); let check_all = if input.get_option("no-check-all").as_bool().unwrap_or(false) { 0 } else { ValidatingArrayLoader::CHECK_ALL }; let check_publish = !input .get_option("no-check-publish") .as_bool() .unwrap_or(false); let check_lock = !input.get_option("no-check-lock").as_bool().unwrap_or(false); let check_version = if input .get_option("no-check-version") .as_bool() .unwrap_or(false) { 0 } else { ConfigValidator::CHECK_VERSION }; let is_strict = input.get_option("strict").as_bool().unwrap_or(false); let (mut errors, mut publish_errors, mut warnings) = validator.validate(&file, check_all, check_version); let mut lock_errors: Vec = vec![]; let mut composer = self.create_composer_instance(input, io, None, false, None)?; let check_lock = (check_lock && composer .get_config() .borrow_mut() .get("lock") .as_bool() .unwrap_or(true)) || input.get_option("check-lock").as_bool().unwrap_or(false); // TODO(phase-b): get_missing_requirement_info needs &package from composer while // locker holds &mut composer; cloning lock state isn't trivial. Use todo!() for the // package-arg subexpression below. let locker = composer.get_locker_mut(); if locker.is_locked() && !locker.is_fresh()? { lock_errors.push("- The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update` or `composer update `.".to_string()); } if locker.is_locked() { // TODO(phase-b): borrows composer twice; use todo!() for the package arg. lock_errors.extend(locker.get_missing_requirement_info(todo!(), true)?); } self.output_result( io, &file, &mut errors, &mut warnings, check_publish, &mut publish_errors, check_lock, &mut lock_errors, true, ); let exit_code = if !errors.is_empty() { 2 } else if is_strict && !warnings.is_empty() { 1 } else { 0 }; let mut exit_code = exit_code; if input .get_option("with-dependencies") .as_bool() .unwrap_or(false) { let packages = composer .get_repository_manager() .get_local_repository() .get_packages(); for package in packages { let path = composer .get_installation_manager_mut() .get_install_path(package.as_ref()); let path = match path { Some(p) => p, None => continue, }; let dep_file = format!("{}/composer.json", path); if std::path::Path::new(&path).is_dir() && std::path::Path::new(&dep_file).exists() { let (mut dep_errors, mut dep_publish_errors, mut dep_warnings) = validator.validate(&dep_file, check_all, check_version); self.output_result( io, package.get_pretty_name(), &mut dep_errors, &mut dep_warnings, check_publish, &mut dep_publish_errors, false, &mut vec![], false, ); let dep_code = if !dep_errors.is_empty() { 2 } else if is_strict && !dep_warnings.is_empty() { 1 } else { 0 }; exit_code = exit_code.max(dep_code); } } } // TODO(plugin): dispatch CommandEvent let command_event = CommandEvent::new(PluginEvents::COMMAND, "validate", input, output); let event_code = composer .get_event_dispatcher() .borrow_mut() .dispatch(Some(command_event.get_name()), None)?; Ok(exit_code.max(event_code)) } fn output_result( &self, io: &dyn IOInterface, name: &str, errors: &mut Vec, warnings: &mut Vec, check_publish: bool, publish_errors: &mut Vec, check_lock: bool, lock_errors: &mut Vec, print_schema_url: bool, ) { let mut do_print_schema_url = false; if !errors.is_empty() { io.write_error(&format!( "{} is invalid, the following errors/warnings were found:", name )); } else if !publish_errors.is_empty() && check_publish { io.write_error(&format!( "{} is valid for simple usage with Composer but has", name )); io.write_error( "strict errors that make it unable to be published as a package", ); do_print_schema_url = print_schema_url; } else if !warnings.is_empty() { io.write_error(&format!( "{} is valid, but with a few warnings", name )); do_print_schema_url = print_schema_url; } else if !lock_errors.is_empty() { io.write(&format!( "{} is valid but your composer.lock has some {}", name, if check_lock { "errors" } else { "warnings" } )); } else { io.write(&format!("{} is valid", name)); } if do_print_schema_url { io.write_error("See https://getcomposer.org/doc/04-schema.md for details on the schema"); } if !errors.is_empty() { *errors = errors.iter().map(|e| format!("- {}", e)).collect(); errors.insert(0, "# General errors".to_string()); } if !warnings.is_empty() { *warnings = warnings.iter().map(|w| format!("- {}", w)).collect(); warnings.insert(0, "# General warnings".to_string()); } let mut extra_warnings: Vec = vec![]; if !publish_errors.is_empty() && check_publish { *publish_errors = publish_errors.iter().map(|e| format!("- {}", e)).collect(); publish_errors.insert(0, "# Publish errors".to_string()); errors.extend(publish_errors.drain(..)); } if !lock_errors.is_empty() { if check_lock { lock_errors.insert(0, "# Lock file errors".to_string()); errors.extend(lock_errors.drain(..)); } else { lock_errors.insert(0, "# Lock file warnings".to_string()); extra_warnings.extend(lock_errors.drain(..)); } } let all_warnings: Vec = warnings.iter().cloned().chain(extra_warnings).collect(); for msg in errors.iter() { if msg.starts_with('#') { io.write_error(&format!("{}", msg)); } else { io.write_error(msg); } } for msg in &all_warnings { if msg.starts_with('#') { io.write_error(&format!("{}", msg)); } else { io.write_error(msg); } } } } impl HasBaseCommandData for ValidateCommand { fn base_command_data(&self) -> &BaseCommandData { &self.base_command_data } fn base_command_data_mut(&mut self) -> &mut BaseCommandData { &mut self.base_command_data } }