From f749a47804cd296a3059cd3f8079c62dbaa5fdc0 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 07:26:48 +0900 Subject: refactor: merge split inherent impl blocks into one per type Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/command/init_command.rs | 1496 ++++++++++++++-------------- 1 file changed, 747 insertions(+), 749 deletions(-) (limited to 'crates/shirabe/src/command/init_command.rs') diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index 99a7de00..f2bf19d7 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -84,352 +84,716 @@ impl InitCommand { .expect("InitCommand::configure uses static, valid metadata"); command } -} -impl Command for InitCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("init")?; - self.set_description("Creates a basic composer.json file in current directory"); - self.set_definition(&[ - InputOption::new("name", None, Some(InputOption::VALUE_REQUIRED), "Name of the package", None).unwrap().into(), - InputOption::new("description", None, Some(InputOption::VALUE_REQUIRED), "Description of package", None).unwrap().into(), - InputOption::new("author", None, Some(InputOption::VALUE_REQUIRED), "Author name of package", None).unwrap().into(), - InputOption::new("type", None, Some(InputOption::VALUE_REQUIRED), "Type of package (e.g. library, project, metapackage, composer-plugin)", None).unwrap().into(), - InputOption::new("homepage", None, Some(InputOption::VALUE_REQUIRED), "Homepage of package", None).unwrap().into(), - InputOption::new6("require", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(), - InputOption::new6("require-dev", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(), - InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), &format!("Minimum stability (empty or one of: {})", implode(", ", &base_package::STABILITIES.keys().map(|k| k.to_string()).collect::>())), None).unwrap().into(), - InputOption::new("license", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_REQUIRED), "License of package", None).unwrap().into(), - InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories, either by URL or using JSON arrays", None).unwrap().into(), - InputOption::new("autoload", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_REQUIRED), "Add PSR-4 autoload mapping. Maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/)", None).unwrap().into(), - ]); - self.set_help( - "The init command creates a basic composer.json file\n\ - in the current directory.\n\ - \n\ - shirabe init\n\ - \n\ - Read more at https://getcomposer.org/doc/03-cli.md#init", - ); - Ok(()) + fn parse_author_string( + &self, + author: &str, + ) -> anyhow::Result>> { + let mut m: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!(r#"/^(?P[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P.+?)>)?$/u"#), + author, + Some(&mut m), + ) { + let email = m.get(&CaptureKey::ByName("email".to_string())).cloned(); + if let Some(ref email) = email + && !self.is_valid_email(email) + { + return Err(InvalidArgumentException { + message: format!("Invalid email \"{}\"", email), + code: 0, + } + .into()); + } + + let mut result: IndexMap> = IndexMap::new(); + result.insert( + "name".to_string(), + Some(trim( + &m.get(&CaptureKey::ByName("name".to_string())) + .cloned() + .unwrap_or_default(), + None, + )), + ); + result.insert("email".to_string(), email); + + return Ok(result); + } + + Err(InvalidArgumentException { + message: "Invalid author string. Must be in the formats: Jane Doe or John Smith " + .to_string(), + code: 0, + } + .into()) } - /// @throws \Seld\JsonLint\ParsingException - fn execute( + pub(crate) fn format_authors( &self, - input: std::rc::Rc>, - output: std::rc::Rc>, - ) -> anyhow::Result { - let io = self.get_io(); + author: &str, + ) -> anyhow::Result>> { + let parsed = self.parse_author_string(author)?; + let mut author_map: IndexMap = IndexMap::new(); + let name = parsed.get("name").cloned().unwrap_or(None); + let email = parsed.get("email").cloned().unwrap_or(None); + if let Some(name) = name { + author_map.insert("name".to_string(), PhpMixed::String(name)); + } + if let Some(email) = email { + author_map.insert("email".to_string(), PhpMixed::String(email)); + } - let allowlist: Vec = vec![ - "name".to_string(), - "description".to_string(), - "author".to_string(), - "type".to_string(), - "homepage".to_string(), - "require".to_string(), - "require-dev".to_string(), - "stability".to_string(), - "license".to_string(), - "autoload".to_string(), - ]; - let filtered_input: IndexMap = array_intersect_key( - &input.borrow().get_options(), - &array_flip_strings(&allowlist), - ) - .into_iter() - .collect(); - let mut options = shirabe_php_shim::array_filter_map(&filtered_input, |val: &PhpMixed| { - !matches!(val, PhpMixed::Null) && !matches!(val, PhpMixed::List(l) if l.is_empty()) - }); + Ok(vec![author_map]) + } - if options.contains_key("name") - && !Preg::is_match( - php_regex!(r"{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D"), - options - .get("name") - .and_then(|v| v.as_string()) - .unwrap_or(""), - ) + /// Extract namespace from package's vendor name. + /// + /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" + pub fn namespace_from_package_name(&self, package_name: &str) -> Option { + if package_name.is_empty() || strpos(package_name, "/").is_none() { + return None; + } + + let namespace: Vec = array_map( + |part: &String| { + let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part); + let part = ucwords(&part); + str_replace(" ", "", &part) + }, + &explode("/", package_name), + ); + + Some(implode("\\", &namespace)) + } + + pub(crate) fn get_git_config(&self) -> IndexMap { + if self.git_config.borrow().is_some() { + return self.git_config.borrow().clone().unwrap_or_default(); + } + + let mut process = ProcessExecutor::new(Some(self.get_io().clone())); + + let mut output = String::new(); + if process.execute_args( + &["git".to_string(), "config".to_string(), "-l".to_string()], + &mut output, + None, + ) == 0 { - return Err(InvalidArgumentException { - message: format!( - "The package name {} is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+", - options.get("name").and_then(|v| v.as_string()).unwrap_or("") - ), - code: 0, + *self.git_config.borrow_mut() = Some(IndexMap::new()); + let mut m: IndexMap> = IndexMap::new(); + if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) { + let keys: Vec = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); + let values: Vec = + m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); + for (key, value) in keys.iter().zip(values.iter()) { + self.git_config + .borrow_mut() + .as_mut() + .unwrap() + .insert(key.clone(), value.clone()); + } } - .into()); + + return self.git_config.borrow().clone().unwrap_or_default(); } - if options.contains_key("author") { - let author = options - .get("author") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - options.insert( - "authors".to_string(), - PhpMixed::List( - self.format_authors(&author)? - .into_iter() - .map(|m| PhpMixed::Array(m.into_iter().collect())) - .collect(), - ), - ); - options.shift_remove("author"); + *self.git_config.borrow_mut() = Some(IndexMap::new()); + IndexMap::new() + } + + /// Checks the local .gitignore file for the Composer vendor directory. + /// + /// Tested patterns include: + /// "/$vendor" + /// "$vendor" + /// "$vendor/" + /// "/$vendor/" + /// "/$vendor/*" + /// "$vendor/*" + pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { + if !file_exists(ignore_file) { + return false; } - let repositories: Vec = input - .borrow() - .get_option("repository")? - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if (repositories.len() as i64) > 0 { - let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( - Some(io.clone()), - None, - )?)); - for repo in &repositories { - let repo_config = - RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; - let entry = options - .entry("repositories".to_string()) - .or_insert_with(|| PhpMixed::List(vec![])); - if let PhpMixed::List(list) = entry { - list.push(PhpMixed::Array(repo_config.into_iter().collect())); - } + let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None)); + + let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default(); + for line in &lines { + if Preg::is_match(&pattern, line) { + return true; } } - if options.contains_key("stability") { - let stab = options.shift_remove("stability").unwrap_or(PhpMixed::Null); - options.insert("minimum-stability".to_string(), stab); + false + } + + pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { + let mut contents = String::new(); + if file_exists(ignore_file) { + contents = file_get_contents(ignore_file).unwrap_or_default(); + + if strpos(&contents, "\n") != Some(0) { + contents.push('\n'); + } } - let require_value = if options.contains_key("require") { - let req_list: Vec = options - .get("require") - .and_then(|v| v.as_list()) - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - let formatted = self.format_requirements(req_list)?; - if formatted.is_empty() { - // PHP: new \stdClass — empty JSON object - PhpMixed::Object(IndexMap::new()) - } else { - PhpMixed::Array( - formatted - .into_iter() - .map(|(k, v)| (k, PhpMixed::String(v))) - .collect(), - ) - } - } else { - // PHP: new \stdClass - PhpMixed::Object(IndexMap::new()) - }; - options.insert("require".to_string(), require_value); - - if options.contains_key("require-dev") { - let req_list: Vec = options - .get("require-dev") - .and_then(|v| v.as_list()) - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - let formatted = self.format_requirements(req_list)?; - let value = if formatted.is_empty() { - PhpMixed::Object(IndexMap::new()) - } else { - PhpMixed::Array( - formatted - .into_iter() - .map(|(k, v)| (k, PhpMixed::String(v))) - .collect(), - ) - }; - options.insert("require-dev".to_string(), value); - } - - // --autoload - create autoload object - let mut autoload_path: Option = None; - if options.contains_key("autoload") { - let ap = options - .get("autoload") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(); - autoload_path = Some(ap.clone()); - let name = input - .borrow() - .get_option("name")? - .as_string() - .unwrap_or("") - .to_string(); - let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); - let mut psr4 = IndexMap::new(); - psr4.insert(format!("{}\\", namespace), PhpMixed::String(ap)); - let mut autoload_obj = IndexMap::new(); - autoload_obj.insert("psr-4".to_string(), PhpMixed::Array(psr4)); - options.insert("autoload".to_string(), PhpMixed::Array(autoload_obj)); - } - - let file_obj = JsonFile::new(Factory::get_composer_file()?, None, None)?; - let options_for_encode: IndexMap = options.clone().into_iter().collect(); - let json = JsonFile::encode(&PhpMixed::Array(options_for_encode.clone()))?; - - if input.borrow().is_interactive() { - io.write_error3(&format!("\n{}\n", json), true, io_interface::NORMAL); - if !io.ask_confirmation( - "Do you confirm generation [yes]? ".to_string(), - true, - ) { - io.write_error3("Command aborted", true, io_interface::NORMAL); - - return Ok(1); - } - } else { - io.write_error3( - &format!("Writing {}", file_obj.get_path()), - true, - io_interface::NORMAL, - ); - } - - file_obj.write(PhpMixed::Array(options_for_encode))?; - let validate_result = file_obj.validate_schema(JsonFile::LAX_SCHEMA, None); - if let Err(e) = validate_result { - // try to downcast to JsonValidationException - if let Some(json_err) = e.downcast_ref::() { - io.write_error3( - "Schema validation error, aborting", - true, - io_interface::NORMAL, - ); - let errors = format!( - " - {}", - implode(&format!("{} - ", PHP_EOL), json_err.get_errors()) - ); - io.write_error3( - &format!("{}:{}{}", json_err.get_message(), PHP_EOL, errors), - true, - io_interface::NORMAL, - ); - let path_to_unlink = file_obj.get_path().to_string(); - let _ = Silencer::call(|| { - shirabe_php_shim::unlink(&path_to_unlink); - Ok::<(), anyhow::Error>(()) - }); - - return Ok(1); - } - return Err(e); - } - - // --autoload - Create src folder - if let Some(ref ap) = autoload_path { - let mut filesystem = Filesystem::new(None); - filesystem.ensure_directory_exists(ap); + file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); + } - // dump-autoload only for projects without added dependencies. - if !self.has_dependencies(&options) { - self.run_dump_autoload_command(output.clone()); - } - } + /// For testing only: invoke the private `parse_author_string`. + pub fn __parse_author_string( + &self, + author: &str, + ) -> anyhow::Result>> { + self.parse_author_string(author) + } - if input.borrow().is_interactive() && is_dir(".git") { - let mut ignore_file = realpath(".gitignore").unwrap_or_default(); + /// For testing only: invoke the crate-private `format_authors`. + pub fn __format_authors( + &self, + author: &str, + ) -> anyhow::Result>> { + self.format_authors(author) + } - if ignore_file.is_empty() { - ignore_file = format!("{}/.gitignore", realpath(".").unwrap_or_default()); - } + /// For testing only: invoke the crate-private `get_git_config`. + pub fn __get_git_config(&self) -> IndexMap { + self.get_git_config() + } - if !self.has_vendor_ignore(&ignore_file, "vendor") { - let question = "Would you like the vendor directory added to your .gitignore [yes]? ".to_string(); + /// For testing only: invoke the crate-private `has_vendor_ignore`. + pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { + self.has_vendor_ignore(ignore_file, vendor) + } - if io.ask_confirmation(question, true) { - self.add_vendor_ignore(&ignore_file, "/vendor/"); - } - } - } + /// For testing only: invoke the crate-private `add_vendor_ignore`. + pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { + self.add_vendor_ignore(ignore_file, vendor) + } - let question = - "Would you like to install dependencies now [yes]? ".to_string(); - if input.borrow().is_interactive() - && self.has_dependencies(&options) - && io.ask_confirmation(question, true) - { - self.update_dependencies(output); - } + pub(crate) fn is_valid_email(&self, email: &str) -> bool { + shirabe_php_shim::filter_var_email(email) + } - // --autoload - Show post-install configuration info - if autoload_path.is_some() { - let name = input - .borrow() - .get_option("name")? - .as_string() - .unwrap_or("") - .to_string(); - let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); + fn update_dependencies(&self, output: std::rc::Rc>) { + let result = (|| -> anyhow::Result { + let application = self + .get_application() + .expect("a Composer command's application is always set"); + let update_command = application.borrow_mut().find("update")?; + self.reset_composer()?; + let input: std::rc::Rc> = + std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); + let command = update_command.borrow(); + command.run(input, output) + })(); - io.write_error3( - &format!( - "PSR-4 autoloading configured. Use \"namespace {};\" in {}", - namespace, - autoload_path.as_deref().unwrap_or("") - ), - true, - io_interface::NORMAL, + if result.is_err() { + self.get_io().borrow().write_error( + "Could not update dependencies. Run `composer update` to see more information.", ); - io.write_error3("Include the Composer autoloader with: require 'vendor/autoload.php';", true, io_interface::NORMAL); } - - Ok(0) } - fn initialize( + fn run_dump_autoload_command( &self, - input: std::rc::Rc>, output: std::rc::Rc>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input.clone(), output.clone())?; - - if !input.borrow().is_interactive() { - if input.borrow().get_option("name")?.is_null() { - let name = self.get_default_package_name(); - input - .borrow_mut() - .set_option("name", PhpMixed::from(name)) - .expect("name option is defined"); - } + ) { + let result = (|| -> anyhow::Result { + let application = self + .get_application() + .expect("a Composer command's application is always set"); + let command = application.borrow_mut().find("dump-autoload")?; + self.reset_composer()?; + let input: std::rc::Rc> = + std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); + let command = command.borrow(); + command.run(input, output) + })(); - if input.borrow().get_option("author")?.is_null() { - let author = self.get_default_author(); - input - .borrow_mut() - .set_option("author", PhpMixed::from(author)) - .expect("author option is defined"); - } + if result.is_err() { + self.get_io() + .borrow() + .write_error("Could not run dump-autoload."); } - - Ok(()) } - fn interact( + fn has_dependencies(&self, options: &IndexMap) -> bool { + let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null); + let requires_arr_empty = match &requires { + PhpMixed::Array(m) => m.is_empty(), + PhpMixed::List(l) => l.is_empty(), + PhpMixed::Null => true, + _ => false, + }; + let dev_requires = options.get("require-dev").cloned(); + let dev_requires_arr_empty = match &dev_requires { + Some(PhpMixed::Array(m)) => m.is_empty(), + Some(PhpMixed::List(l)) => l.is_empty(), + Some(PhpMixed::Null) | None => true, + _ => false, + }; + + !requires_arr_empty || !dev_requires_arr_empty + } + + fn sanitize_package_name_component(&self, name: &str) -> String { + let name = Preg::replace( + php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), + "$1$3-$2$4", + name, + ); + let name = strtolower(&name); + let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name); + + Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name) + } + + fn get_default_package_name(&self) -> String { + let git = self.get_git_config(); + let cwd = realpath(".").unwrap_or_default(); + let name = basename(&cwd); + let name = self.sanitize_package_name_component(&name); + + let mut vendor = name.clone(); + let composer_default_vendor = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_VENDOR") + .map(|value| value.to_string_lossy().into_owned()); + let server_username = PHP_SERVER + .lock() + .unwrap() + .get("USERNAME") + .map(|value| value.to_string_lossy().into_owned()); + let server_user = PHP_SERVER + .lock() + .unwrap() + .get("USER") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_vendor + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = composer_default_vendor.unwrap_or_default(); + } else if git.contains_key("github.user") { + vendor = git.get("github.user").cloned().unwrap_or_default(); + } else if !empty( + &server_username + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = server_username.unwrap_or_default(); + } else if !empty( + &server_user + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + vendor = server_user.unwrap_or_default(); + } else if !get_current_user().is_empty() { + vendor = get_current_user(); + } + + let vendor = self.sanitize_package_name_component(&vendor); + + format!("{}/{}", vendor, name) + } + + fn get_default_author(&self) -> Option { + let git = self.get_git_config(); + + let mut author_name: Option = None; + let composer_default_author = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_AUTHOR") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_author + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + author_name = composer_default_author; + } else if git.contains_key("user.name") { + author_name = git.get("user.name").cloned(); + } + + let mut author_email: Option = None; + let composer_default_email = PHP_SERVER + .lock() + .unwrap() + .get("COMPOSER_DEFAULT_EMAIL") + .map(|value| value.to_string_lossy().into_owned()); + if !empty( + &composer_default_email + .clone() + .map(PhpMixed::String) + .unwrap_or(PhpMixed::Null), + ) { + author_email = composer_default_email; + } else if git.contains_key("user.email") { + author_email = git.get("user.email").cloned(); + } + + if let (Some(name), Some(email)) = (author_name, author_email) { + return Some(format!("{} <{}>", name, email)); + } + + None + } +} + +impl Command for InitCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("init")?; + self.set_description("Creates a basic composer.json file in current directory"); + self.set_definition(&[ + InputOption::new("name", None, Some(InputOption::VALUE_REQUIRED), "Name of the package", None).unwrap().into(), + InputOption::new("description", None, Some(InputOption::VALUE_REQUIRED), "Description of package", None).unwrap().into(), + InputOption::new("author", None, Some(InputOption::VALUE_REQUIRED), "Author name of package", None).unwrap().into(), + InputOption::new("type", None, Some(InputOption::VALUE_REQUIRED), "Type of package (e.g. library, project, metapackage, composer-plugin)", None).unwrap().into(), + InputOption::new("homepage", None, Some(InputOption::VALUE_REQUIRED), "Homepage of package", None).unwrap().into(), + InputOption::new6("require", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(), + InputOption::new6("require-dev", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(), + InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), &format!("Minimum stability (empty or one of: {})", implode(", ", &base_package::STABILITIES.keys().map(|k| k.to_string()).collect::>())), None).unwrap().into(), + InputOption::new("license", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_REQUIRED), "License of package", None).unwrap().into(), + InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories, either by URL or using JSON arrays", None).unwrap().into(), + InputOption::new("autoload", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_REQUIRED), "Add PSR-4 autoload mapping. Maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/)", None).unwrap().into(), + ]); + self.set_help( + "The init command creates a basic composer.json file\n\ + in the current directory.\n\ + \n\ + shirabe init\n\ + \n\ + Read more at https://getcomposer.org/doc/03-cli.md#init", + ); + Ok(()) + } + + /// @throws \Seld\JsonLint\ParsingException + fn execute( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result { + let io = self.get_io(); + + let allowlist: Vec = vec![ + "name".to_string(), + "description".to_string(), + "author".to_string(), + "type".to_string(), + "homepage".to_string(), + "require".to_string(), + "require-dev".to_string(), + "stability".to_string(), + "license".to_string(), + "autoload".to_string(), + ]; + let filtered_input: IndexMap = array_intersect_key( + &input.borrow().get_options(), + &array_flip_strings(&allowlist), + ) + .into_iter() + .collect(); + let mut options = shirabe_php_shim::array_filter_map(&filtered_input, |val: &PhpMixed| { + !matches!(val, PhpMixed::Null) && !matches!(val, PhpMixed::List(l) if l.is_empty()) + }); + + if options.contains_key("name") + && !Preg::is_match( + php_regex!(r"{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D"), + options + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or(""), + ) + { + return Err(InvalidArgumentException { + message: format!( + "The package name {} is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+", + options.get("name").and_then(|v| v.as_string()).unwrap_or("") + ), + code: 0, + } + .into()); + } + + if options.contains_key("author") { + let author = options + .get("author") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + options.insert( + "authors".to_string(), + PhpMixed::List( + self.format_authors(&author)? + .into_iter() + .map(|m| PhpMixed::Array(m.into_iter().collect())) + .collect(), + ), + ); + options.shift_remove("author"); + } + + let repositories: Vec = input + .borrow() + .get_option("repository")? + .as_list() + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + if (repositories.len() as i64) > 0 { + let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( + Some(io.clone()), + None, + )?)); + for repo in &repositories { + let repo_config = + RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; + let entry = options + .entry("repositories".to_string()) + .or_insert_with(|| PhpMixed::List(vec![])); + if let PhpMixed::List(list) = entry { + list.push(PhpMixed::Array(repo_config.into_iter().collect())); + } + } + } + + if options.contains_key("stability") { + let stab = options.shift_remove("stability").unwrap_or(PhpMixed::Null); + options.insert("minimum-stability".to_string(), stab); + } + + let require_value = if options.contains_key("require") { + let req_list: Vec = options + .get("require") + .and_then(|v| v.as_list()) + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + let formatted = self.format_requirements(req_list)?; + if formatted.is_empty() { + // PHP: new \stdClass — empty JSON object + PhpMixed::Object(IndexMap::new()) + } else { + PhpMixed::Array( + formatted + .into_iter() + .map(|(k, v)| (k, PhpMixed::String(v))) + .collect(), + ) + } + } else { + // PHP: new \stdClass + PhpMixed::Object(IndexMap::new()) + }; + options.insert("require".to_string(), require_value); + + if options.contains_key("require-dev") { + let req_list: Vec = options + .get("require-dev") + .and_then(|v| v.as_list()) + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + let formatted = self.format_requirements(req_list)?; + let value = if formatted.is_empty() { + PhpMixed::Object(IndexMap::new()) + } else { + PhpMixed::Array( + formatted + .into_iter() + .map(|(k, v)| (k, PhpMixed::String(v))) + .collect(), + ) + }; + options.insert("require-dev".to_string(), value); + } + + // --autoload - create autoload object + let mut autoload_path: Option = None; + if options.contains_key("autoload") { + let ap = options + .get("autoload") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + autoload_path = Some(ap.clone()); + let name = input + .borrow() + .get_option("name")? + .as_string() + .unwrap_or("") + .to_string(); + let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); + let mut psr4 = IndexMap::new(); + psr4.insert(format!("{}\\", namespace), PhpMixed::String(ap)); + let mut autoload_obj = IndexMap::new(); + autoload_obj.insert("psr-4".to_string(), PhpMixed::Array(psr4)); + options.insert("autoload".to_string(), PhpMixed::Array(autoload_obj)); + } + + let file_obj = JsonFile::new(Factory::get_composer_file()?, None, None)?; + let options_for_encode: IndexMap = options.clone().into_iter().collect(); + let json = JsonFile::encode(&PhpMixed::Array(options_for_encode.clone()))?; + + if input.borrow().is_interactive() { + io.write_error3(&format!("\n{}\n", json), true, io_interface::NORMAL); + if !io.ask_confirmation( + "Do you confirm generation [yes]? ".to_string(), + true, + ) { + io.write_error3("Command aborted", true, io_interface::NORMAL); + + return Ok(1); + } + } else { + io.write_error3( + &format!("Writing {}", file_obj.get_path()), + true, + io_interface::NORMAL, + ); + } + + file_obj.write(PhpMixed::Array(options_for_encode))?; + let validate_result = file_obj.validate_schema(JsonFile::LAX_SCHEMA, None); + if let Err(e) = validate_result { + // try to downcast to JsonValidationException + if let Some(json_err) = e.downcast_ref::() { + io.write_error3( + "Schema validation error, aborting", + true, + io_interface::NORMAL, + ); + let errors = format!( + " - {}", + implode(&format!("{} - ", PHP_EOL), json_err.get_errors()) + ); + io.write_error3( + &format!("{}:{}{}", json_err.get_message(), PHP_EOL, errors), + true, + io_interface::NORMAL, + ); + let path_to_unlink = file_obj.get_path().to_string(); + let _ = Silencer::call(|| { + shirabe_php_shim::unlink(&path_to_unlink); + Ok::<(), anyhow::Error>(()) + }); + + return Ok(1); + } + return Err(e); + } + + // --autoload - Create src folder + if let Some(ref ap) = autoload_path { + let mut filesystem = Filesystem::new(None); + filesystem.ensure_directory_exists(ap); + + // dump-autoload only for projects without added dependencies. + if !self.has_dependencies(&options) { + self.run_dump_autoload_command(output.clone()); + } + } + + if input.borrow().is_interactive() && is_dir(".git") { + let mut ignore_file = realpath(".gitignore").unwrap_or_default(); + + if ignore_file.is_empty() { + ignore_file = format!("{}/.gitignore", realpath(".").unwrap_or_default()); + } + + if !self.has_vendor_ignore(&ignore_file, "vendor") { + let question = "Would you like the vendor directory added to your .gitignore [yes]? ".to_string(); + + if io.ask_confirmation(question, true) { + self.add_vendor_ignore(&ignore_file, "/vendor/"); + } + } + } + + let question = + "Would you like to install dependencies now [yes]? ".to_string(); + if input.borrow().is_interactive() + && self.has_dependencies(&options) + && io.ask_confirmation(question, true) + { + self.update_dependencies(output); + } + + // --autoload - Show post-install configuration info + if autoload_path.is_some() { + let name = input + .borrow() + .get_option("name")? + .as_string() + .unwrap_or("") + .to_string(); + let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); + + io.write_error3( + &format!( + "PSR-4 autoloading configured. Use \"namespace {};\" in {}", + namespace, + autoload_path.as_deref().unwrap_or("") + ), + true, + io_interface::NORMAL, + ); + io.write_error3("Include the Composer autoloader with: require 'vendor/autoload.php';", true, io_interface::NORMAL); + } + + Ok(0) + } + + fn initialize( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input.clone(), output.clone())?; + + if !input.borrow().is_interactive() { + if input.borrow().get_option("name")?.is_null() { + let name = self.get_default_package_name(); + input + .borrow_mut() + .set_option("name", PhpMixed::from(name)) + .expect("name option is defined"); + } + + if input.borrow().get_option("author")?.is_null() { + let author = self.get_default_author(); + input + .borrow_mut() + .set_option("author", PhpMixed::from(author)) + .expect("author option is defined"); + } + } + + Ok(()) + } + + fn interact( &self, input: std::rc::Rc>, output: std::rc::Rc>, @@ -833,457 +1197,91 @@ impl Command for InitCommand { false, )? } else { - vec![] - }; - input.borrow_mut().set_option( - "require-dev", - PhpMixed::List(dev_requirements.into_iter().map(PhpMixed::String).collect()), - ); - - // --autoload - input and validation - let autoload = input - .borrow() - .get_option("autoload")? - .as_string() - .map(|s| s.to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "src/".to_string()); - let name_str = input - .borrow() - .get_option("name")? - .as_string() - .unwrap_or("") - .to_string(); - let namespace = self - .namespace_from_package_name(&name_str) - .unwrap_or_default(); - let autoload_for_validate = autoload.clone(); - let autoload_default = autoload.clone(); - let autoload_value = io.ask_and_validate( - format!( - "Add PSR-4 autoload mapping? Maps namespace \"{}\" to the entered relative path. [{}, n to skip]: ", - namespace, autoload - ), - Box::new(move |value: PhpMixed| -> anyhow::Result { - if value.is_null() { - return Ok(PhpMixed::String(autoload_for_validate.clone())); - } - - let value_str = value.as_string().unwrap_or("").to_string(); - if value_str == "n" || value_str == "no" { - return Ok(PhpMixed::Null); - } - - let value_or_default = if value_str.is_empty() { - autoload_for_validate.clone() - } else { - value_str - }; - - if !Preg::is_match(php_regex!(r"{^[^/][A-Za-z0-9\-_/]+/$}"), &value_or_default) - { - return Err(InvalidArgumentException { - message: format!( - "The src folder name \"{}\" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/", - value_or_default, - ), - code: 0, - } - .into()); - } - - Ok(PhpMixed::String(value_or_default)) - }), - None, - PhpMixed::String(autoload_default), - )?; - input.borrow_mut().set_option("autoload", autoload_value); - - Ok(()) - })(); - } - - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) - } - - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} - -impl BaseCommand for InitCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} - -impl InitCommand { - fn parse_author_string( - &self, - author: &str, - ) -> anyhow::Result>> { - let mut m: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!(r#"/^(?P[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P.+?)>)?$/u"#), - author, - Some(&mut m), - ) { - let email = m.get(&CaptureKey::ByName("email".to_string())).cloned(); - if let Some(ref email) = email - && !self.is_valid_email(email) - { - return Err(InvalidArgumentException { - message: format!("Invalid email \"{}\"", email), - code: 0, - } - .into()); - } - - let mut result: IndexMap> = IndexMap::new(); - result.insert( - "name".to_string(), - Some(trim( - &m.get(&CaptureKey::ByName("name".to_string())) - .cloned() - .unwrap_or_default(), - None, - )), - ); - result.insert("email".to_string(), email); - - return Ok(result); - } - - Err(InvalidArgumentException { - message: "Invalid author string. Must be in the formats: Jane Doe or John Smith " - .to_string(), - code: 0, - } - .into()) - } - - pub(crate) fn format_authors( - &self, - author: &str, - ) -> anyhow::Result>> { - let parsed = self.parse_author_string(author)?; - let mut author_map: IndexMap = IndexMap::new(); - let name = parsed.get("name").cloned().unwrap_or(None); - let email = parsed.get("email").cloned().unwrap_or(None); - if let Some(name) = name { - author_map.insert("name".to_string(), PhpMixed::String(name)); - } - if let Some(email) = email { - author_map.insert("email".to_string(), PhpMixed::String(email)); - } - - Ok(vec![author_map]) - } - - /// Extract namespace from package's vendor name. - /// - /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" - pub fn namespace_from_package_name(&self, package_name: &str) -> Option { - if package_name.is_empty() || strpos(package_name, "/").is_none() { - return None; - } - - let namespace: Vec = array_map( - |part: &String| { - let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part); - let part = ucwords(&part); - str_replace(" ", "", &part) - }, - &explode("/", package_name), - ); - - Some(implode("\\", &namespace)) - } - - pub(crate) fn get_git_config(&self) -> IndexMap { - if self.git_config.borrow().is_some() { - return self.git_config.borrow().clone().unwrap_or_default(); - } - - let mut process = ProcessExecutor::new(Some(self.get_io().clone())); - - let mut output = String::new(); - if process.execute_args( - &["git".to_string(), "config".to_string(), "-l".to_string()], - &mut output, - None, - ) == 0 - { - *self.git_config.borrow_mut() = Some(IndexMap::new()); - let mut m: IndexMap> = IndexMap::new(); - if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) { - let keys: Vec = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); - let values: Vec = - m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); - for (key, value) in keys.iter().zip(values.iter()) { - self.git_config - .borrow_mut() - .as_mut() - .unwrap() - .insert(key.clone(), value.clone()); - } - } - - return self.git_config.borrow().clone().unwrap_or_default(); - } - - *self.git_config.borrow_mut() = Some(IndexMap::new()); - IndexMap::new() - } - - /// Checks the local .gitignore file for the Composer vendor directory. - /// - /// Tested patterns include: - /// "/$vendor" - /// "$vendor" - /// "$vendor/" - /// "/$vendor/" - /// "/$vendor/*" - /// "$vendor/*" - pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { - if !file_exists(ignore_file) { - return false; - } - - let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None)); - - let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default(); - for line in &lines { - if Preg::is_match(&pattern, line) { - return true; - } - } - - false - } - - pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { - let mut contents = String::new(); - if file_exists(ignore_file) { - contents = file_get_contents(ignore_file).unwrap_or_default(); - - if strpos(&contents, "\n") != Some(0) { - contents.push('\n'); - } - } - - file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); - } - - /// For testing only: invoke the private `parse_author_string`. - pub fn __parse_author_string( - &self, - author: &str, - ) -> anyhow::Result>> { - self.parse_author_string(author) - } + vec![] + }; + input.borrow_mut().set_option( + "require-dev", + PhpMixed::List(dev_requirements.into_iter().map(PhpMixed::String).collect()), + ); - /// For testing only: invoke the crate-private `format_authors`. - pub fn __format_authors( - &self, - author: &str, - ) -> anyhow::Result>> { - self.format_authors(author) - } + // --autoload - input and validation + let autoload = input + .borrow() + .get_option("autoload")? + .as_string() + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "src/".to_string()); + let name_str = input + .borrow() + .get_option("name")? + .as_string() + .unwrap_or("") + .to_string(); + let namespace = self + .namespace_from_package_name(&name_str) + .unwrap_or_default(); + let autoload_for_validate = autoload.clone(); + let autoload_default = autoload.clone(); + let autoload_value = io.ask_and_validate( + format!( + "Add PSR-4 autoload mapping? Maps namespace \"{}\" to the entered relative path. [{}, n to skip]: ", + namespace, autoload + ), + Box::new(move |value: PhpMixed| -> anyhow::Result { + if value.is_null() { + return Ok(PhpMixed::String(autoload_for_validate.clone())); + } - /// For testing only: invoke the crate-private `get_git_config`. - pub fn __get_git_config(&self) -> IndexMap { - self.get_git_config() - } + let value_str = value.as_string().unwrap_or("").to_string(); + if value_str == "n" || value_str == "no" { + return Ok(PhpMixed::Null); + } - /// For testing only: invoke the crate-private `has_vendor_ignore`. - pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { - self.has_vendor_ignore(ignore_file, vendor) - } + let value_or_default = if value_str.is_empty() { + autoload_for_validate.clone() + } else { + value_str + }; - /// For testing only: invoke the crate-private `add_vendor_ignore`. - pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { - self.add_vendor_ignore(ignore_file, vendor) - } + if !Preg::is_match(php_regex!(r"{^[^/][A-Za-z0-9\-_/]+/$}"), &value_or_default) + { + return Err(InvalidArgumentException { + message: format!( + "The src folder name \"{}\" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/", + value_or_default, + ), + code: 0, + } + .into()); + } - pub(crate) fn is_valid_email(&self, email: &str) -> bool { - shirabe_php_shim::filter_var_email(email) - } + Ok(PhpMixed::String(value_or_default)) + }), + None, + PhpMixed::String(autoload_default), + )?; + input.borrow_mut().set_option("autoload", autoload_value); - fn update_dependencies(&self, output: std::rc::Rc>) { - let result = (|| -> anyhow::Result { - let application = self - .get_application() - .expect("a Composer command's application is always set"); - let update_command = application.borrow_mut().find("update")?; - self.reset_composer()?; - let input: std::rc::Rc> = - std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); - let command = update_command.borrow(); - command.run(input, output) + Ok(()) })(); - - if result.is_err() { - self.get_io().borrow().write_error( - "Could not update dependencies. Run `composer update` to see more information.", - ); - } } - fn run_dump_autoload_command( + fn complete( &self, - output: std::rc::Rc>, - ) { - let result = (|| -> anyhow::Result { - let application = self - .get_application() - .expect("a Composer command's application is always set"); - let command = application.borrow_mut().find("dump-autoload")?; - self.reset_composer()?; - let input: std::rc::Rc> = - std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); - let command = command.borrow(); - command.run(input, output) - })(); - - if result.is_err() { - self.get_io() - .borrow() - .write_error("Could not run dump-autoload."); - } - } - - fn has_dependencies(&self, options: &IndexMap) -> bool { - let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null); - let requires_arr_empty = match &requires { - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::List(l) => l.is_empty(), - PhpMixed::Null => true, - _ => false, - }; - let dev_requires = options.get("require-dev").cloned(); - let dev_requires_arr_empty = match &dev_requires { - Some(PhpMixed::Array(m)) => m.is_empty(), - Some(PhpMixed::List(l)) => l.is_empty(), - Some(PhpMixed::Null) | None => true, - _ => false, - }; - - !requires_arr_empty || !dev_requires_arr_empty - } - - fn sanitize_package_name_component(&self, name: &str) -> String { - let name = Preg::replace( - php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"), - "$1$3-$2$4", - name, - ); - let name = strtolower(&name); - let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name); - - Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name) + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) } - fn get_default_package_name(&self) -> String { - let git = self.get_git_config(); - let cwd = realpath(".").unwrap_or_default(); - let name = basename(&cwd); - let name = self.sanitize_package_name_component(&name); - - let mut vendor = name.clone(); - let composer_default_vendor = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_VENDOR") - .map(|value| value.to_string_lossy().into_owned()); - let server_username = PHP_SERVER - .lock() - .unwrap() - .get("USERNAME") - .map(|value| value.to_string_lossy().into_owned()); - let server_user = PHP_SERVER - .lock() - .unwrap() - .get("USER") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_vendor - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = composer_default_vendor.unwrap_or_default(); - } else if git.contains_key("github.user") { - vendor = git.get("github.user").cloned().unwrap_or_default(); - } else if !empty( - &server_username - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = server_username.unwrap_or_default(); - } else if !empty( - &server_user - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - vendor = server_user.unwrap_or_default(); - } else if !get_current_user().is_empty() { - vendor = get_current_user(); - } - - let vendor = self.sanitize_package_name_component(&vendor); + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} - format!("{}/{}", vendor, name) +impl BaseCommand for InitCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data } - fn get_default_author(&self) -> Option { - let git = self.get_git_config(); - - let mut author_name: Option = None; - let composer_default_author = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_AUTHOR") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_author - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - author_name = composer_default_author; - } else if git.contains_key("user.name") { - author_name = git.get("user.name").cloned(); - } - - let mut author_email: Option = None; - let composer_default_email = PHP_SERVER - .lock() - .unwrap() - .get("COMPOSER_DEFAULT_EMAIL") - .map(|value| value.to_string_lossy().into_owned()); - if !empty( - &composer_default_email - .clone() - .map(PhpMixed::String) - .unwrap_or(PhpMixed::Null), - ) { - author_email = composer_default_email; - } else if git.contains_key("user.email") { - author_email = git.get("user.email").cloned(); - } - - if let (Some(name), Some(email)) = (author_name, author_email) { - return Some(format!("{} <{}>", name, email)); - } - - None - } + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } -- cgit v1.3.1