use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static CONSECUTIVE_CD_UP: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"cd\s+\.\.").unwrap());
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line.trim_start().starts_with('#') {
continue;
}
if !CONSECUTIVE_CD_UP.is_match(line) {
continue;
}
if i + 1 < lines.len() {
let next_line = lines[i + 1];
if !next_line.trim_start().starts_with('#') && CONSECUTIVE_CD_UP.is_match(next_line) {
let diagnostic = Diagnostic::new(
"SC2103",
Severity::Warning,
"Use pushd/popd or cd - instead of multiple cd .. commands",
Span::new(line_num, 1, line_num, line.len() + 1),
);
result.add(diagnostic);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2103_consecutive_cd_up() {
let code = r#"
cd /some/deep/path
# do work
cd ..
cd ..
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2103");
assert_eq!(result.diagnostics[0].severity, Severity::Warning);
}
#[test]
fn test_sc2103_single_cd_up_ok() {
let code = r#"
cd /some/path
# do work
cd ..
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_cd_up_with_other_commands() {
let code = r#"
cd /some/deep/path
# do work
cd ..
echo "Back one level"
cd ..
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_three_consecutive_cd_up() {
let code = r#"
cd /very/deep/path
cd ..
cd ..
cd ..
"#;
let result = check(code);
assert!(!result.diagnostics.is_empty());
}
#[test]
fn test_sc2103_cd_to_named_directory() {
let code = r#"
cd /some/path
cd /another/path
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_comment_between_cd() {
let code = r#"
cd ..
# comment
cd ..
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_cd_dash_ok() {
let code = r#"
cd /some/path
cd -
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_pushd_popd_ok() {
let code = r#"
pushd /some/path
# do work
popd
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_no_cd_commands() {
let code = r#"
echo "No cd commands here"
ls -la
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2103_cd_up_in_function() {
let code = r#"
function cleanup() {
cd ..
cd ..
}
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
}