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
157
// SC2140: Word is of the form "A"B"C" (B indicated). This is not concatenation.
//
// In shell, adjacent strings don't concatenate like "foo""bar" → "foobar".
// Quotes must be properly closed and reopened.
//
// Examples:
// Bad:
// echo "Hello "World"" // Not concatenation, syntax error
// var="foo"bar"baz" // Malformed string
// path="/usr""/local" // Incorrect
//
// Good:
// echo "Hello World" // Single quoted string
// echo "Hello ""World" // Two separate arguments
// echo "Hello World" // Or proper quoting
// var="foo""bar""baz" // Or: var="foobarbaz"
//
// Impact: Syntax error or unexpected word splitting
use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static MALFORMED_QUOTES: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
// Match: "string1"word"string2" where word is unquoted between quoted parts
// Matches: "foo"bar"baz" (malformed - unquoted word between quotes)
// Matches: "Hello "World"" (malformed - unquoted word with empty string after)
// May match: "foo""bar""baz" but we filter this in check() logic
Regex::new(r#""[^"]*"[a-zA-Z_][a-zA-Z0-9_]*"[^"]*""#).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;
}
// Check for malformed quote patterns
for mat in MALFORMED_QUOTES.find_iter(line) {
let matched = mat.as_str();
// Skip if this is proper concatenation like ""bar""
// Pattern: two adjacent quotes at the start mean empty string before the word
// This indicates: "foo""bar" not "foo"bar"
if matched.starts_with(r#""""#) {
// Starts with "" - proper concatenation
continue;
}
// Skip if it looks like proper separate arguments (space between quotes)
if matched.contains("\" \"") {
continue;
}
let start_col = mat.start() + 1;
let end_col = mat.end() + 1;
let diagnostic = Diagnostic::new(
"SC2140",
Severity::Warning,
"Word is split between quotes. Use proper quoting or concatenation".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_sc2140_malformed_quotes() {
let code = r#"echo "Hello "World"""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0]
.message
.contains("split between quotes"));
}
#[test]
fn test_sc2140_single_string_ok() {
let code = r#"echo "Hello World""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2140_separate_args_ok() {
let code = r#"echo "Hello" "World""#;
let result = check(code);
// Space between quotes = separate arguments (OK)
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2140_comment_ok() {
let code = r#"# echo "Hello "World"""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2140_variable_assignment() {
let code = r#"var="foo"bar"baz""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2140_path() {
let code = r#"path="/usr"local"/bin""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2140_proper_concat_ok() {
let code = r#"var="foo""bar""baz""#;
let result = check(code);
// Adjacent quoted strings without unquoted word in between
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2140_single_quotes_ok() {
let code = "echo 'Hello World'";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2140_multiple() {
let code = r#"
echo "Hello "World""
var="foo"bar"baz"
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_sc2140_escaped_ok() {
let code = r#"echo "Hello \"World\"""#;
let result = check(code);
// Escaped quotes inside string are OK
assert_eq!(result.diagnostics.len(), 0);
}
}