use super::sc2154::check;
fn flagged(src: &str) -> Vec<String> {
check(src)
.diagnostics
.iter()
.filter(|d| d.code == "SC2154")
.map(|d| d.message.split('\'').nth(1).unwrap_or_default().to_string())
.collect()
}
fn assert_clean(src: &str) {
let got = flagged(src);
assert!(got.is_empty(), "SC2154 false positive on {src:?}: {got:?}");
}
#[test]
fn semicolon_separated_assignments_are_all_tracked() {
assert_clean("repo=${1:-}; course=${2:-}; delete=${3:-}\necho \"$repo$course$delete\"");
}
#[test]
fn semicolon_without_spaces() {
assert_clean("a=1;b=2;c=3\necho \"$a$b$c\"");
}
#[test]
fn andand_and_oror_separated_assignments() {
assert_clean("a=1 && b=2\necho \"$a$b\"");
assert_clean("a=1 || b=2\necho \"$a$b\"");
assert_clean("a=1 && b=2 || c=3\necho \"$a$b$c\"");
}
#[test]
fn newline_separated_assignments_still_work() {
assert_clean("a=1\nb=2\necho \"$a$b\"");
}
#[test]
fn assignments_inside_a_brace_group_or_subshell() {
assert_clean("a=1; { b=2; }\necho \"$a$b\"");
assert_clean("a=1; ( b=2 )\necho \"$a$b\"");
}
#[test]
fn assignment_after_a_control_keyword_on_one_line() {
assert_clean("if true; then c=1; fi\necho \"$c\"");
assert_clean("for i in 1 2; do d=1; done\necho \"$d\"");
assert_clean("if false; then :; else e=1; fi\necho \"$e\"");
}
#[test]
fn assignment_in_a_case_branch_on_one_line() {
assert_clean("case x in x) g=1 ;; esac\necho \"$g\"");
}
#[test]
fn local_declares_several_variables_at_once() {
assert_clean("f() {\n local name=$1 expect=$2 body=$3\n echo \"$name$expect$body\"\n}");
}
#[test]
fn declaration_builtins_on_a_single_line_function_body() {
assert_clean("f() { local x=$1 y=$2; echo \"$x$y\"; }");
}
#[test]
fn local_then_assign_across_a_semicolon() {
assert_clean(
"f() {\n local dir; dir=$(mktemp -d); local marker=\"$dir/n\"\n echo \"$dir$marker\"\n}",
);
}
#[test]
fn export_readonly_declare_typeset_with_flags() {
assert_clean("export A=1 B=2\necho \"$A$B\"");
assert_clean("readonly r=1 s=2\necho \"$r$s\"");
assert_clean("declare -i n=1 m=2\necho \"$n$m\"");
assert_clean("typeset -r t=1 u=2\necho \"$t$u\"");
}
#[test]
fn assignment_prefixes_on_one_command() {
assert_clean("A=1 B=2 env >/dev/null\necho \"$A$B\"");
}
#[test]
fn append_and_array_subscript_assignment() {
assert_clean("p=1; p+=2\necho \"$p\"");
assert_clean("q[0]=1; q[1]=2\necho \"${q[0]}\"");
}
#[test]
fn must_still_fire_on_a_plainly_unassigned_variable() {
assert_eq!(flagged("echo \"$never_assigned\""), ["never_assigned"]);
}
#[test]
fn must_still_fire_when_the_equals_is_inside_an_option_argument() {
assert_eq!(
flagged("grep --color=auto q /dev/null\necho \"$color\""),
["color"]
);
}
#[test]
fn must_still_fire_when_the_equals_is_inside_a_string_literal() {
assert_eq!(flagged("echo \"x=1\"\necho \"$x\""), ["x"]);
assert_eq!(flagged("echo 'y=1'\necho \"$y\""), ["y"]);
}
#[test]
fn must_still_fire_when_the_equals_is_an_argument_word() {
assert_eq!(flagged("echo a=1\necho \"$a\""), ["a"]);
}
#[test]
fn must_still_fire_on_a_test_comparison() {
let got = flagged("[ \"$z\" = 1 ] && echo hi");
assert_eq!(got, ["z"]);
}
#[test]
fn must_still_fire_for_a_variable_assigned_only_in_a_comment() {
assert_eq!(flagged("# c=1\necho \"$c\""), ["c"]);
}
#[test]
fn mapfile_and_readarray_assign_their_array() {
assert_clean("mapfile -t names < <(ls)\necho \"${names[0]}\"");
assert_clean("readarray -t rows < <(ls)\necho \"${rows[0]}\"");
assert_clean("mapfile -t allow < <(grep -v '^#' f)\necho \"${allow[@]}\"");
}
#[test]
fn mapfile_flags_that_take_an_argument_are_skipped() {
assert_clean("mapfile -d '' -n 5 -O 1 -s 2 -u 3 -C cb -c 4 -t arr < <(ls)\necho \"${arr[0]}\"");
}
#[test]
fn mapfile_with_no_array_name_assigns_MAPFILE() {
assert_clean("mapfile -t < <(ls)\necho \"${MAPFILE[0]}\"");
}
#[test]
fn must_still_fire_alongside_a_mapfile() {
assert_eq!(
flagged("mapfile -t names < <(ls)\necho \"${names[0]}$ghost\""),
["ghost"]
);
}
#[test]
fn must_still_fire_when_mapfile_is_only_a_word_in_a_message() {
assert_eq!(flagged("echo 'use mapfile -t out'\necho \"$out\""), ["out"]);
}