use super::{ConfigIssue, PathEntry, Severity};
use std::collections::HashMap;
pub fn analyze_path_entries(source: &str) -> Vec<PathEntry> {
let mut entries = Vec::new();
let mut seen = HashMap::new();
let mut line_num = 0;
for line in source.lines() {
line_num += 1;
if let Some(path) = extract_path_addition(line) {
let is_duplicate = seen.contains_key(&path);
if is_duplicate {
let _original_line = seen.get(&path).copied();
} else {
seen.insert(path.clone(), line_num);
}
entries.push(PathEntry {
line: line_num,
path,
is_duplicate,
});
}
}
entries
}
fn extract_path_addition(line: &str) -> Option<String> {
let line = line.trim();
if line.starts_with('#') {
return None;
}
if !line.contains("PATH") || !line.contains('=') {
return None;
}
let parts: Vec<&str> = line.split('=').collect();
if parts.len() < 2 {
return None;
}
let value = parts[1].trim();
let value = value.trim_matches('"').trim_matches('\'');
if let Some(colon_pos) = value.find(':') {
let path = &value[..colon_pos];
Some(path.to_string())
} else {
None
}
}
pub fn detect_duplicate_paths(entries: &[PathEntry]) -> Vec<ConfigIssue> {
let mut issues = Vec::new();
for entry in entries {
if entry.is_duplicate {
issues.push(ConfigIssue {
rule_id: "CONFIG-001".to_string(),
severity: Severity::Warning,
message: format!(
"Duplicate PATH entry: '{}' (already added earlier)",
entry.path
),
line: entry.line,
column: 0,
suggestion: Some(format!(
"Remove this line - '{}' is already in PATH",
entry.path
)),
});
}
}
issues
}
pub fn deduplicate_path_entries(source: &str) -> String {
let entries = analyze_path_entries(source);
let mut seen_paths = HashMap::new();
let mut result = Vec::new();
let mut line_num = 0;
for entry in &entries {
if !entry.is_duplicate {
seen_paths.insert(entry.line, true);
}
}
for line in source.lines() {
line_num += 1;
let is_path_line = extract_path_addition(line).is_some();
if is_path_line {
if seen_paths.contains_key(&line_num) {
result.push(line.to_string());
}
} else {
result.push(line.to_string());
}
}
result.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_001_extract_path_addition_basic() {
let line = r#"export PATH="/usr/local/bin:$PATH""#;
let result = extract_path_addition(line);
assert_eq!(result, Some("/usr/local/bin".to_string()));
}
#[test]
fn test_config_001_extract_path_addition_with_braces() {
let line = r#"export PATH="/opt/homebrew/bin:${PATH}""#;
let result = extract_path_addition(line);
assert_eq!(result, Some("/opt/homebrew/bin".to_string()));
}
#[test]
fn test_config_001_extract_path_addition_without_export() {
let line = r#"PATH="/home/user/.cargo/bin:$PATH""#;
let result = extract_path_addition(line);
assert_eq!(result, Some("/home/user/.cargo/bin".to_string()));
}
#[test]
fn test_config_001_ignore_comments() {
let line = r#"# export PATH="/usr/local/bin:$PATH""#;
let result = extract_path_addition(line);
assert_eq!(result, None);
}
#[test]
fn test_config_001_analyze_no_duplicates() {
let source = r#"
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
"#;
let entries = analyze_path_entries(source);
assert_eq!(entries.len(), 3);
assert!(!entries[0].is_duplicate);
assert!(!entries[1].is_duplicate);
assert!(!entries[2].is_duplicate);
}
#[test]
fn test_config_001_analyze_with_duplicates() {
let source = r#"
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
"#;
let entries = analyze_path_entries(source);
assert_eq!(entries.len(), 3);
assert!(!entries[0].is_duplicate); assert!(!entries[1].is_duplicate); assert!(entries[2].is_duplicate); }
#[test]
fn test_config_001_detect_duplicate_paths() {
let source = r#"
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
"#;
let entries = analyze_path_entries(source);
let issues = detect_duplicate_paths(&entries);
assert_eq!(issues.len(), 2); assert_eq!(issues[0].rule_id, "CONFIG-001");
assert_eq!(issues[0].severity, Severity::Warning);
assert!(issues[0].message.contains("/usr/local/bin"));
assert_eq!(issues[0].line, 4); assert_eq!(issues[1].line, 6); }
#[test]
fn test_config_001_deduplicate_removes_duplicates() {
let source = r#"export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH""#;
let expected = r#"export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH""#;
let result = deduplicate_path_entries(source);
assert_eq!(result, expected);
}
#[test]
fn test_config_001_deduplicate_preserves_non_path_lines() {
let source = r#"# My .bashrc
export EDITOR="vim"
export PATH="/usr/local/bin:$PATH"
alias ll='ls -la'
export PATH="/usr/local/bin:$PATH"
echo "Welcome!""#;
let expected = r#"# My .bashrc
export EDITOR="vim"
export PATH="/usr/local/bin:$PATH"
alias ll='ls -la'
echo "Welcome!""#;
let result = deduplicate_path_entries(source);
assert_eq!(result, expected);
}
#[test]
fn test_config_001_deduplicate_preserves_order() {
let source = r#"export PATH="/first:$PATH"
export PATH="/second:$PATH"
export PATH="/third:$PATH"
export PATH="/second:$PATH"
export PATH="/first:$PATH""#;
let expected = r#"export PATH="/first:$PATH"
export PATH="/second:$PATH"
export PATH="/third:$PATH""#;
let result = deduplicate_path_entries(source);
assert_eq!(result, expected);
}
#[test]
fn test_config_001_empty_input() {
let source = "";
let entries = analyze_path_entries(source);
let result = deduplicate_path_entries(source);
assert_eq!(entries.len(), 0);
assert_eq!(result, "");
}
#[test]
fn test_config_001_no_path_entries() {
let source = r#"export EDITOR="vim"
alias ll='ls -la'
echo "Hello""#;
let entries = analyze_path_entries(source);
let result = deduplicate_path_entries(source);
assert_eq!(entries.len(), 0);
assert_eq!(result, source);
}
}