use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use serde_yml::Value;
use crate::cop::EnabledState;
use super::gem_path;
use super::load_recursive::load_config_recursive;
use super::merge::merge_layer_into;
use super::standard::{standard_gem_config_path, PLUGIN_GEM_DEPARTMENTS};
use super::types::ConfigLayer;
fn yaml_string_list(val: &Value) -> Vec<String> {
match val {
Value::String(s) => vec![s.clone()],
Value::Sequence(seq) => seq
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
_ => vec![],
}
}
pub(crate) fn collect_yaml_strings(val: &Value) -> Vec<String> {
yaml_string_list(val)
}
fn collect_require_gems(map: &serde_yml::Mapping) -> Vec<String> {
let mut gems = Vec::new();
for key in &["plugins", "require"] {
if let Some(val) = map.get(Value::String(key.to_string())) {
gems.extend(yaml_string_list(val));
}
}
gems.dedup();
gems
}
fn needs_wrapper(gems: &[String], injected: &[(usize, String)], name: &str) -> bool {
!gems.iter().any(|g| g == name) && !injected.iter().any(|(_, g)| g == name)
}
fn inject_one_wrapper(gems: &[String], injected: &mut Vec<(usize, String)>, wrapper: &str, rubocop: &str) {
for (i, gem) in gems.iter().enumerate() {
if gem == wrapper && needs_wrapper(gems, injected, rubocop) {
injected.push((i, rubocop.to_string()));
}
}
}
fn inject_standard_wrappers(gems: &mut Vec<String>) {
let mut injected = Vec::new();
inject_one_wrapper(gems, &mut injected, "standard-rails", "rubocop-rails");
inject_one_wrapper(
gems,
&mut injected,
"standard-performance",
"rubocop-performance",
);
for (i, gem) in injected.into_iter().rev() {
gems.insert(i, gem);
}
}
fn gem_config_rel_path(gem_name: &str, ruby_version: Option<f64>) -> Option<String> {
if gem_name.starts_with("rubocop-") {
Some("config/default.yml".into())
} else {
standard_gem_config_path(gem_name, ruby_version).map(|path| path.into())
}
}
fn resolve_named_gem(
gem_name: &str,
working_dir: &Path,
gem_cache: Option<&HashMap<String, PathBuf>>,
) -> Option<PathBuf> {
if let Some(path) = gem_cache.and_then(|c| c.get(gem_name)) {
return Some(path.clone());
}
match gem_path::resolve_gem_path(gem_name, working_dir) {
Ok(p) => Some(p),
Err(e) => {
eprintln!("warning: require '{}': {e:#}", gem_name);
None
}
}
}
fn merge_require_layer(base_layer: &mut ConfigLayer, gem_name: &str, layer: ConfigLayer) {
let _ = gem_name;
merge_layer_into(base_layer, &layer, None);
}
fn load_require_fallback(
gem_name: &str,
gem_root: &Path,
working_dir: &Path,
visited: &mut HashSet<PathBuf>,
gem_cache: Option<&HashMap<String, PathBuf>>,
base_layer: &mut ConfigLayer,
) {
if gem_name.starts_with("rubocop-") {
return;
}
let fallback = gem_root.join("config").join("base.yml");
if !fallback.exists() {
return;
}
match load_config_recursive(&fallback, working_dir, visited, gem_cache) {
Ok(layer) => merge_layer_into(base_layer, &layer, None),
Err(e) => {
eprintln!(
"warning: failed to load default config for {}: {e:#}",
gem_name
);
}
}
}
fn load_one_require_gem(
gem_name: &str,
ruby_version: Option<f64>,
working_dir: &Path,
visited: &mut HashSet<PathBuf>,
gem_cache: Option<&HashMap<String, PathBuf>>,
base_layer: &mut ConfigLayer,
) {
let Some(config_rel_path) = gem_config_rel_path(gem_name, ruby_version) else {
return;
};
let Some(gem_root) = resolve_named_gem(gem_name, working_dir, gem_cache) else {
return;
};
let config_file = gem_root.join(&config_rel_path);
if !config_file.exists() {
load_require_fallback(
gem_name,
&gem_root,
working_dir,
visited,
gem_cache,
base_layer,
);
return;
}
match load_config_recursive(&config_file, working_dir, visited, gem_cache) {
Ok(layer) => merge_require_layer(base_layer, gem_name, layer),
Err(e) => {
eprintln!(
"warning: failed to load default config for {}: {e:#}",
gem_name
);
}
}
}
fn record_require_enabled(base_layer: &mut ConfigLayer) {
base_layer.require_enabled_cops = base_layer
.cop_configs
.iter()
.filter(|(_, c)| c.enabled == EnabledState::True)
.map(|(n, _)| n.clone())
.collect();
base_layer.require_enabled_depts = base_layer
.department_configs
.iter()
.filter(|(_, c)| c.enabled == EnabledState::True)
.map(|(n, _)| n.clone())
.collect();
}
fn register_plugin_depts(base_layer: &mut ConfigLayer, gems: &[String]) {
base_layer.require_known_cops = base_layer.cop_configs.keys().cloned().collect();
base_layer.require_departments = base_layer.department_configs.keys().cloned().collect();
for gem_name in gems {
for (dept, gem) in PLUGIN_GEM_DEPARTMENTS {
if gem_name.as_str() == *gem {
base_layer.require_departments.insert(dept.to_string());
}
}
}
}
pub(crate) fn process_require_plugins(
map: &serde_yml::Mapping,
ruby_version: Option<f64>,
working_dir: &Path,
visited: &mut HashSet<PathBuf>,
gem_cache: Option<&HashMap<String, PathBuf>>,
base_layer: &mut ConfigLayer,
) {
let mut gems = collect_require_gems(map);
inject_standard_wrappers(&mut gems);
let visited_before = visited.clone();
for gem_name in &gems {
load_one_require_gem(
gem_name,
ruby_version,
working_dir,
visited,
gem_cache,
base_layer,
);
}
record_require_enabled(base_layer);
*visited = visited_before;
register_plugin_depts(base_layer, &gems);
}