use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static ASSOC_DECL_PATTERN: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"declare\s+-A\s+([A-Za-z_][A-Za-z0-9_]*)").unwrap());
static ARRAY_ASSIGN_PATTERN: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"([A-Za-z_][A-Za-z0-9_]*)=\(([^)]+)\)").unwrap());
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let mut assoc_arrays = std::collections::HashSet::new();
let assoc_decl_pattern = &*ASSOC_DECL_PATTERN;
let array_assign_pattern = &*ARRAY_ASSIGN_PATTERN;
for line in source.lines() {
if line.trim_start().starts_with('#') {
continue;
}
for cap in assoc_decl_pattern.captures_iter(line) {
let var_name = cap.get(1).unwrap().as_str();
assoc_arrays.insert(var_name.to_string());
}
}
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if line.trim_start().starts_with('#') {
continue;
}
for cap in array_assign_pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let var_name = cap.get(1).unwrap().as_str();
let content = cap.get(2).unwrap().as_str();
if assoc_arrays.contains(var_name) {
if !content.contains('[') || !content.contains("]=") {
let start_col = full_match.start() + 1;
let end_col = full_match.end() + 1;
let diagnostic = Diagnostic::new(
"SC2190",
Severity::Error,
"Elements in associative arrays need index, e.g. array=([key]=value)",
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2190_basic_detection() {
let script = "declare -A assoc\nassoc=(value1 value2)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2190");
}
#[test]
fn test_sc2190_multiple_values() {
let script = "declare -A map\nmap=(a b c d)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2190_single_value() {
let script = "declare -A dict\ndict=(value)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2190_false_positive_with_keys() {
let script = "declare -A assoc\nassoc=([key1]=value1 [key2]=value2)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2190_false_positive_single_key() {
let script = "declare -A map\nmap=([foo]=bar)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2190_false_positive_regular_array() {
let script = "declare -a array\narray=(a b c)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2190_false_positive_in_comment() {
let script = "declare -A assoc\n# assoc=(value)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2190_multiline() {
let script = "declare -A dict\necho test\ndict=(val1 val2)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2190_in_function() {
let script = "func() {\n declare -A local_map\n local_map=(x y)\n}";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2190_multiple_assoc_arrays() {
let script = "declare -A a1\ndeclare -A a2\na1=(v1)\na2=(v2)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
}
}