use crate::linter::{Diagnostic, LintResult, Severity, Span};
fn is_comment_line(line: &str) -> bool {
line.trim_start().starts_with('#')
}
fn is_case_start(line: &str) -> bool {
line.trim_start().starts_with("case ")
}
fn is_loop_start(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with("for ") || trimmed.starts_with("while ") || trimmed.starts_with("until ")
}
fn is_function_start(line: &str) -> bool {
line.trim_start().contains("() {") || line.trim_start().starts_with("function ")
}
fn is_case_end(line: &str) -> bool {
line.trim_start() == "esac"
}
fn is_loop_end(line: &str) -> bool {
line.trim_start() == "done"
}
fn is_function_end(line: &str) -> bool {
line.trim_start() == "}"
}
fn has_break_or_continue(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.contains("break") || trimmed.contains("continue")
}
fn build_diagnostic(line_num: usize, line_len: usize) -> Diagnostic {
Diagnostic::new(
"SC2242",
Severity::Error,
"Can only break/continue from loops. Use 'exit' to exit case or function".to_string(),
Span::new(line_num, 1, line_num, line_len + 1),
)
}
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let mut in_case = false;
let mut in_loop = false;
let mut in_function = false;
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if is_comment_line(line) {
continue;
}
if is_case_start(line) {
in_case = true;
}
if is_loop_start(line) {
in_loop = true;
}
if is_function_start(line) {
in_function = true;
}
if is_case_end(line) {
in_case = false;
}
if is_loop_end(line) {
in_loop = false;
}
if is_function_end(line) {
in_function = false;
}
if in_case && !in_loop && !in_function && has_break_or_continue(line) {
let diagnostic = build_diagnostic(line_num, line.len());
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2242_break_in_case() {
let code = "case $x in\n a) break;;\nesac";
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2242_break_in_loop_ok() {
let code = "while true; do\n break\ndone";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_continue_in_case() {
let code = "case $x in\n a) continue;;\nesac";
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2242_exit_in_case_ok() {
let code = "case $x in\n a) exit 1;;\nesac";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_comment_skipped() {
let code = "# case x in a) break;; esac";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_case_in_loop() {
let code = "for x in *; do\n case $x in\n *.txt) break;;\n esac\ndone";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0); }
#[test]
fn test_sc2242_no_code() {
let code = "";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_normal_command() {
let code = "echo test";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_return_in_function_ok() {
let code = "foo() {\n case $1 in\n a) return 1;;\n esac\n}";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2242_simple_case() {
let code = "case $var in\n x) echo ok;;\nesac";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0); }
}