use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
use std::collections::{HashMap, HashSet};
static ASSIGN_PATTERN: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)=").unwrap());
static USE_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 assign_pattern = &*ASSIGN_PATTERN;
let use_pattern = &*USE_PATTERN;
let mut assigned: HashMap<String, usize> = HashMap::new();
let mut used: HashSet<String> = HashSet::new();
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if line.trim_start().starts_with('#') {
continue;
}
for cap in assign_pattern.captures_iter(line) {
let var_name = cap.get(1).unwrap().as_str().to_string();
assigned.insert(var_name, line_num);
}
for cap in use_pattern.captures_iter(line) {
let var_name = cap.get(1).unwrap().as_str().to_string();
used.insert(var_name);
}
}
for (var_name, line_num) in &assigned {
if !used.contains(var_name) {
if var_name.starts_with('_') || var_name.to_uppercase() == *var_name {
continue; }
let diagnostic = Diagnostic::new(
"SC2034",
Severity::Info,
format!("Variable '{}' is assigned but never used", var_name),
Span::new(*line_num, 1, *line_num, var_name.len() + 1),
);
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2034_basic_detection() {
let script = r#"
unused_var="value"
echo "Hello"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2034");
}
#[test]
fn test_sc2034_variable_used() {
let script = r#"
used_var="value"
echo "$used_var"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2034_multiple_unused() {
let script = r#"
unused1="value1"
unused2="value2"
echo "Hello"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_sc2034_skip_underscore_prefix() {
let script = r#"
_private="value"
echo "Hello"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0); }
#[test]
fn test_sc2034_skip_all_caps() {
let script = r#"
EXPORTED_VAR="value"
echo "Hello"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0); }
#[test]
fn test_sc2034_braced_usage() {
let script = r#"
used_var="value"
echo "${used_var}"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2034_used_in_condition() {
let script = r#"
check_var="test"
if [ "$check_var" = "test" ]; then
echo "yes"
fi
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2034_mixed_used_and_unused() {
let script = r#"
used="value1"
unused="value2"
echo "$used"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2034_used_in_assignment() {
let script = r#"
var1="value"
var2="$var1"
echo "$var2"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2034_no_false_positive_in_comment() {
let script = r#"
# unused_var="value"
echo "Hello"
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
}