use std::path::Path;
use crate::config::migrate::parsers::eslint::{self, ESLintConfig};
use crate::config::migrate::{ConversionResult, DetectedConfig, MigrationWarning, WarningSeverity};
pub(crate) fn convert(
detected: &DetectedConfig,
project_root: &Path,
dry_run: bool,
) -> Result<ConversionResult, String> {
let config = eslint::parse(&detected.path)?;
let eslint_js = generate_eslint_js(&config);
let config_dir = project_root.join(".linthis/configs/javascript");
let config_path = config_dir.join(".eslintrc.js");
let mut result = ConversionResult {
created_files: Vec::new(),
changes: Vec::new(),
warnings: Vec::new(),
};
result.changes.push(format!(
"Create {} (migrated from {})",
config_path.display(),
detected.path.display()
));
if !config.ignores.is_empty() {
result.changes.push(format!(
"Add {} exclude pattern(s) to config.toml",
config.ignores.len()
));
}
let filename = detected
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if filename.starts_with("eslint.config") {
result.warnings.push(MigrationWarning {
source: "eslint".to_string(),
message: "ESLint flat config (v9+) detected. The migrated config uses legacy format. \
Consider manually updating to flat config format for ESLint v9+."
.to_string(),
severity: WarningSeverity::Warning,
});
}
if !dry_run {
std::fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create directory: {}", e))?;
std::fs::write(&config_path, eslint_js)
.map_err(|e| format!("Failed to write config: {}", e))?;
result.created_files.push(config_path);
if !config.ignores.is_empty() {
update_config_excludes(project_root, &config.ignores)?;
}
}
Ok(result)
}
fn generate_eslint_js(config: &ESLintConfig) -> String {
let mut lines = vec![
"// Migrated from existing ESLint config by linthis".to_string(),
"// Review and adjust as needed".to_string(),
String::new(),
"module.exports = {".to_string(),
];
if !config.env.is_empty() {
lines.push(" env: {".to_string());
for env in &config.env {
lines.push(format!(" '{}': true,", env));
}
lines.push(" },".to_string());
}
if !config.extends.is_empty() {
let extends_str = config
.extends
.iter()
.map(|e| format!("'{}'", e))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(" extends: [{}],", extends_str));
}
if !config.plugins.is_empty() {
let plugins_str = config
.plugins
.iter()
.map(|p| format!("'{}'", p))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(" plugins: [{}],", plugins_str));
}
if let Some(ref parser) = config.parser {
lines.push(format!(" parser: '{}',", parser));
}
if !config.rules.is_empty() {
lines.push(" rules: {".to_string());
for (name, value) in &config.rules {
lines.push(format!(" '{}': {},", name, value.to_js_string()));
}
lines.push(" },".to_string());
}
lines.push("};".to_string());
lines.join("\n")
}
fn update_config_excludes(project_root: &Path, excludes: &[String]) -> Result<(), String> {
let config_path = project_root.join(".linthis/config.toml");
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
let content = if config_path.exists() {
std::fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config: {}", e))?
} else {
String::new()
};
let mut doc: toml_edit::DocumentMut = content
.parse()
.map_err(|e| format!("Failed to parse config: {}", e))?;
let excludes_array = doc
.entry("excludes")
.or_insert_with(|| toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new())))
.as_array_mut()
.ok_or("excludes is not an array")?;
for pattern in excludes {
let exists = excludes_array
.iter()
.any(|v| v.as_str() == Some(pattern.as_str()));
if !exists {
excludes_array.push(pattern.as_str());
}
}
std::fs::write(&config_path, doc.to_string())
.map_err(|e| format!("Failed to write config: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_eslint_js() {
let config = ESLintConfig {
extends: vec!["eslint:recommended".to_string()],
rules: vec![("semi".to_string(), eslint::RuleValue::Error)],
env: vec!["browser".to_string(), "node".to_string()],
plugins: vec![],
parser: None,
ignores: vec![],
};
let js = generate_eslint_js(&config);
assert!(js.contains("extends: ['eslint:recommended']"));
assert!(js.contains("'semi': 'error'"));
assert!(js.contains("'browser': true"));
}
}