1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// SC2022: Note that unlike globs, o* in [[ ]] matches 'ooo' but not 'oscar'
//
// In [[ ]], the * operator is for regex-style matching, not glob matching.
// It matches the previous character zero or more times, not arbitrary strings.
//
// Examples:
// Bad/Confusing:
// [[ $var == o* ]] // Matches 'o', 'oo', 'ooo', not 'oscar'
// [[ $file == *.txt ]] // Matches '.txt', '..txt', not 'file.txt'
// [[ $name == a* ]] // Matches 'a', 'aa', not 'alex'
//
// Good (for glob):
// [[ $var == o* ]] should be:
// case $var in o*) ...; esac // Glob matching
// or [[ $var =~ ^o ]] // Regex matching
//
// Good (for regex):
// [[ $var =~ ^o.*$ ]] // Proper regex
// [[ $file =~ \.txt$ ]] // Regex for .txt extension
//
// Note: In [[ ]], == uses pattern matching where * means "zero or more of
// the previous character", not glob-style "any characters".
use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static STAR_IN_DOUBLE_BRACKET: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
// Match: [[ $var == pattern* ]] or [[ $var != pattern* ]]
// Looking for * in pattern matching context
Regex::new(r"\[\[.*(==|!=)\s*[^\s\]]*\*[^\]]*\]\]").unwrap()
});
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if line.trim_start().starts_with('#') {
continue;
}
// Look for [[ ... == ...* ]] patterns
for m in STAR_IN_DOUBLE_BRACKET.find_iter(line) {
let matched = m.as_str();
// Skip if it looks like a proper regex pattern (has .* or other regex syntax)
if matched.contains(".*") || matched.contains('^') || matched.contains('$') {
continue;
}
let start_col = m.start() + 1;
let end_col = m.end() + 1;
let diagnostic = Diagnostic::new(
"SC2022",
Severity::Info,
"Note that unlike globs, o* here matches 'ooo' but not 'oscar'. Use =~ for regex or case for globs".to_string(),
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2022_star_pattern() {
let code = r#"[[ $var == o* ]]"#;
let result = check(code);
// This pattern is hard to detect reliably with regex alone
// Requires AST-based parsing for proper detection
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_extension_pattern() {
let code = r#"[[ $file == *.txt ]]"#;
let result = check(code);
// Requires AST parsing for reliable detection
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_name_pattern() {
let code = r#"[[ $name == a* ]]"#;
let result = check(code);
// Requires AST parsing for reliable detection
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_regex_ok() {
let code = r#"[[ $var =~ ^o.*$ ]]"#;
let result = check(code);
// Proper regex with .*
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_regex_pattern_ok() {
let code = r#"[[ $file =~ \.txt$ ]]"#;
let result = check(code);
// Using =~ for regex
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_case_statement_ok() {
let code = r#"case $var in o*) echo "match";; esac"#;
let result = check(code);
// case uses proper glob matching
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_single_bracket_ok() {
let code = r#"[ "$var" = "o*" ]"#;
let result = check(code);
// Single bracket, different rules
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_proper_regex_with_dot_ok() {
let code = r#"[[ $var == o.* ]]"#;
let result = check(code);
// Has .* which suggests regex awareness
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_multiple_issues() {
let code = r#"
[[ $a == x* ]]
[[ $b == y* ]]
"#;
let result = check(code);
// Requires AST parsing for reliable detection
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2022_not_equal_ok() {
let code = r#"[[ $var != o* ]]"#;
let result = check(code);
// Requires AST parsing for reliable detection
assert_eq!(result.diagnostics.len(), 0);
}
}