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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
//! BASH006: Missing Function Documentation
//!
//! **Rule**: Detect functions without docstring comments
//!
//! **Why this matters**:
//! Undocumented functions reduce code maintainability:
//! - Harder to understand function purpose
//! - Parameters and return values unclear
//! - Team collaboration suffers
//! - Onboarding new developers takes longer
//!
//! **Examples**:
//!
//! ❌ **BAD** (missing documentation):
//! ```bash
//! process_data() {
//! local input="$1"
//! echo "$input" | jq '.items[]'
//! }
//! ```
//!
//! ✅ **GOOD** (with documentation):
//! ```bash
//! # Process JSON data and extract items array
//! # Arguments:
//! # $1 - JSON input string
//! # Returns:
//! # Items array, one per line
//! process_data() {
//! local input="$1"
//! echo "$input" | jq '.items[]'
//! }
//! ```
//!
//! ## Detection Logic
//!
//! This rule detects:
//! - Function definitions: `function_name()` or `function function_name()`
//! - Missing comment block immediately before function
//!
//! ## Auto-fix
//!
//! Suggests adding documentation template:
//! ```bash
//! # Brief description of function
//! # Arguments:
//! # $1 - Description of first argument
//! # Returns:
//! # Description of return value
//! ```
use crate::linter::LintResult;
use crate::linter::{Diagnostic, Severity, Span};
/// Check for functions without documentation
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let lines: Vec<&str> = source.lines().collect();
for (line_num, line) in lines.iter().enumerate() {
let trimmed = line.trim();
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
// Detect function definitions
// Pattern 1: function_name() {
// Pattern 2: function function_name() {
if is_function_definition(trimmed) {
// Check if previous line is a comment
let has_doc = if line_num > 0 {
has_documentation_comment(&lines, line_num)
} else {
false
};
if !has_doc {
let span = Span::new(line_num + 1, 1, line_num + 1, line.len());
let function_name = extract_function_name(trimmed);
let diag = Diagnostic::new(
"BASH006",
Severity::Info,
format!(
"Function '{}' lacks documentation - add comment describing purpose, arguments, and return value for better maintainability",
function_name
),
span,
);
result.add(diag);
}
}
}
result
}
/// Check if line is a function definition
fn is_function_definition(line: &str) -> bool {
// Pattern 1: function_name() {
if line.contains("()") && (line.contains('{') || line.ends_with("()")) {
// Exclude common non-function patterns (control flow statements)
// Use word boundaries to avoid excluding function names like "if_", "while_", "for_loop"
if line.starts_with("if ") || line.starts_with("while ") || line.starts_with("for ") {
return false;
}
return true;
}
// Pattern 2: function function_name() {
if line.starts_with("function ") && line.contains("()") {
return true;
}
false
}
/// Extract function name from definition
fn extract_function_name(line: &str) -> String {
// Remove "function " prefix if present
let without_keyword = line.strip_prefix("function ").unwrap_or(line);
// Extract name before ()
if let Some(pos) = without_keyword.find('(') {
without_keyword[..pos].trim().to_string()
} else {
"unknown".to_string()
}
}
/// Check if function has documentation comment
fn has_documentation_comment(lines: &[&str], func_line: usize) -> bool {
// Look for comment immediately before function (allowing blank lines)
let mut check_line = func_line;
// Skip back over blank lines
while check_line > 0 {
check_line -= 1;
let trimmed = lines[check_line].trim();
if trimmed.is_empty() {
continue; // Skip blank lines
}
// Check if it's a comment (but not a shebang)
if trimmed.starts_with('#') && !trimmed.starts_with("#!") {
return true; // Found documentation comment
}
// Non-comment, non-blank line (or shebang)
return false;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
// RED Phase: Write failing tests first (EXTREME TDD)
/// RED TEST 1: Detect function without documentation
#[test]
fn test_BASH006_detects_undocumented_function() {
let script = r#"#!/bin/bash
process_data() {
echo "Processing"
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "BASH006");
assert_eq!(diag.severity, Severity::Info);
assert!(diag.message.contains("process_data"));
assert!(diag.message.contains("documentation"));
}
/// RED TEST 2: Pass when function has documentation
#[test]
fn test_BASH006_passes_with_documentation() {
let script = r#"#!/bin/bash
# Process data from input
# Arguments: $1 - input file
process_data() {
echo "Processing"
}
"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"Should pass with documentation"
);
}
/// RED TEST 3: Detect function keyword syntax
#[test]
fn test_BASH006_detects_function_keyword() {
let script = r#"#!/bin/bash
function build() {
make all
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "BASH006");
assert!(diag.message.contains("build"));
}
/// RED TEST 4: Pass with single-line comment
#[test]
fn test_BASH006_passes_with_single_comment() {
let script = r#"#!/bin/bash
# Build the project
function build() {
make all
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Should pass with comment");
}
/// RED TEST 5: Detect multiple undocumented functions
#[test]
fn test_BASH006_detects_multiple_functions() {
let script = r#"#!/bin/bash
build() {
make all
}
test() {
make test
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
assert_eq!(result.diagnostics[0].code, "BASH006");
assert_eq!(result.diagnostics[1].code, "BASH006");
}
/// RED TEST 6: Pass with blank line between comment and function
#[test]
fn test_BASH006_passes_with_blank_line() {
let script = r#"#!/bin/bash
# Deploy application
deploy() {
echo "Deploying"
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Should pass with blank line");
}
/// RED TEST 7: Ignore non-function patterns
#[test]
fn test_BASH006_ignores_non_functions() {
let script = r#"#!/bin/bash
if [ -f file ]; then
echo "exists"
fi
for i in $(seq 1 10); do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Should ignore if/for");
}
/// RED TEST 8: Detect function with multi-line definition
#[test]
fn test_BASH006_detects_multiline_function() {
let script = r#"#!/bin/bash
complex_function() {
local var="value"
echo "$var"
}
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(diag.message.contains("complex_function"));
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(10))]
/// PROPERTY TEST 1: Never panics on any input
#[test]
fn prop_bash006_never_panics(s in ".*") {
let _ = check(&s);
}
/// PROPERTY TEST 2: Always detects undocumented function
#[test]
fn prop_bash006_detects_undocumented(
func_name in "[a-z_]{3,15}",
) {
let script = format!("{}() {{\n echo test\n}}", func_name);
let result = check(&script);
prop_assert_eq!(result.diagnostics.len(), 1);
prop_assert_eq!(result.diagnostics[0].code.as_str(), "BASH006");
}
/// PROPERTY TEST 3: Passes with documentation
#[test]
fn prop_bash006_passes_with_doc(
func_name in "[a-z_]{3,15}",
) {
let script = format!("# Function doc\n{}() {{\n echo test\n}}", func_name);
let result = check(&script);
prop_assert_eq!(result.diagnostics.len(), 0);
}
}
}