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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// REPL Parser Integration Module
//
// Task: REPL-004-001 - Embed bash parser into REPL
// Test Approach: RED → GREEN → REFACTOR → PROPERTY → MUTATION
//
// Quality targets:
// - Unit tests: 6+ scenarios
// - Property tests: 2+ generators
// - Mutation score: ≥90%
// - Complexity: <10 per function
use crate::bash_parser::{BashAst, BashParser, ParseError};
/// Parse bash input and return AST
///
/// # Examples
///
/// ```
/// use bashrs::repl::parser::parse_bash;
///
/// let result = parse_bash("echo hello");
/// assert!(result.is_ok());
/// ```
pub fn parse_bash(input: &str) -> Result<BashAst, ParseError> {
let mut parser = BashParser::new(input)?;
parser.parse()
}
/// Format parse error for display in REPL with enhanced context
///
/// Provides helpful error messages with line numbers for better debugging.
/// Handles all ParseError variants with appropriate formatting.
///
/// # Examples
///
/// ```
/// use bashrs::repl::parser::{parse_bash, format_parse_error};
///
/// let result = parse_bash("if then fi"); // Missing condition
/// assert!(result.is_err());
///
/// let error = result.unwrap_err();
/// let formatted = format_parse_error(&error);
/// assert!(formatted.contains("Syntax error"));
/// ```
pub fn format_parse_error(error: &ParseError) -> String {
match error {
ParseError::UnexpectedToken {
expected,
found,
line,
} => {
format!(
"Syntax error at line {}: expected {}, but found {}",
line, expected, found
)
}
ParseError::UnexpectedEof => String::from(
"Syntax error: unexpected end of file (did you forget to close a quote or bracket?)",
),
ParseError::InvalidSyntax(msg) => {
// Try to add generic line info even if not provided in the variant
format!("Syntax error: {}", msg)
}
ParseError::LexerError(lexer_err) => {
format!("Lexer error: {}", lexer_err)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// ===== UNIT TESTS (RED PHASE) =====
/// Test: REPL-004-001-001 - Parse simple command
#[test]
fn test_REPL_004_001_parse_simple_command() {
let input = "echo hello";
let result = parse_bash(input);
assert!(result.is_ok(), "Should parse simple command");
let ast = result.unwrap();
assert_eq!(ast.statements.len(), 1, "Should have 1 statement");
}
/// Test: REPL-004-001-002 - Parse command with arguments
#[test]
fn test_REPL_004_001_parse_command_with_args() {
let input = "ls -la /tmp";
let result = parse_bash(input);
assert!(
result.is_ok(),
"Should parse command with arguments: {:?}",
result
);
let ast = result.unwrap();
assert_eq!(ast.statements.len(), 1, "Should have 1 statement");
}
/// Test: REPL-004-001-003 - Parse control flow (if statement)
#[test]
fn test_REPL_004_001_parse_control_flow() {
let input = r#"if true; then
echo success
fi"#;
let result = parse_bash(input);
assert!(result.is_ok(), "Should parse if statement: {:?}", result);
let ast = result.unwrap();
assert_eq!(ast.statements.len(), 1, "Should have 1 if statement");
}
/// Test: REPL-004-001-004 - Parse error for unclosed quote
#[test]
fn test_REPL_004_001_parse_error_unclosed_quote() {
let input = r#"echo "hello"#;
let result = parse_bash(input);
assert!(result.is_err(), "Should fail on unclosed quote");
let error = result.unwrap_err();
let error_msg = format_parse_error(&error);
// Updated to match new error format
assert!(
error_msg.contains("Syntax error") || error_msg.contains("Lexer error"),
"Should format error: {}",
error_msg
);
}
/// Test: REPL-004-001-005 - Parse error for invalid syntax
#[test]
fn test_REPL_004_001_parse_error_invalid_syntax() {
let input = "if then fi"; // Missing condition
let result = parse_bash(input);
assert!(result.is_err(), "Should fail on invalid syntax");
}
/// Test: REPL-004-001-006 - Parse empty input
#[test]
fn test_REPL_004_001_parse_empty_input() {
let input = "";
let result = parse_bash(input);
assert!(result.is_ok(), "Should handle empty input");
let ast = result.unwrap();
assert_eq!(ast.statements.len(), 0, "Should have no statements");
}
/// Test: REPL-004-001-007 - Parse multiline input
#[test]
fn test_REPL_004_001_parse_multiline() {
let input = "echo line1\necho line2\necho line3";
let result = parse_bash(input);
assert!(result.is_ok(), "Should parse multiline input");
let ast = result.unwrap();
assert_eq!(ast.statements.len(), 3, "Should have 3 statements");
}
/// Test: REPL-004-001-008 - Format error message
#[test]
fn test_REPL_004_001_format_error_message() {
let input = "echo \"unclosed";
let result = parse_bash(input);
assert!(result.is_err());
let error = result.unwrap_err();
let formatted = format_parse_error(&error);
// Updated to match new improved error formatting
assert!(
formatted.contains("Syntax error") || formatted.contains("Lexer error"),
"Should have error type: {}",
formatted
);
assert!(!formatted.is_empty());
}
// ===== REPL-004-003 TESTS (RED PHASE) =====
/// Test: REPL-004-003-001 - Syntax error includes line number
#[test]
fn test_REPL_004_003_syntax_error_unclosed_quote() {
let input = r#"echo "hello
world"#;
let result = parse_bash(input);
assert!(result.is_err(), "Should fail on unclosed quote");
let error = result.unwrap_err();
let formatted = format_parse_error(&error);
// Should contain line number information
assert!(
formatted.contains("line") || formatted.contains("Line"),
"Error should mention line number: {}",
formatted
);
assert!(!formatted.is_empty(), "Formatted error should not be empty");
}
/// Test: REPL-004-003-002 - Invalid operator error shows context
#[test]
fn test_REPL_004_003_syntax_error_invalid_operator() {
let input = "if | then echo test; fi";
let result = parse_bash(input);
assert!(result.is_err(), "Should fail on invalid syntax");
let error = result.unwrap_err();
let formatted = format_parse_error(&error);
// Should contain helpful error message
assert!(!formatted.is_empty(), "Formatted error should not be empty");
}
/// Test: REPL-004-003-003 - Error messages are helpful and clear
#[test]
fn test_REPL_004_003_syntax_error_helpful_message() {
let input = r#"echo line1
echo line2
if then echo problem
echo line4"#;
let result = parse_bash(input);
assert!(result.is_err(), "Should fail on line 3");
let error = result.unwrap_err();
let formatted = format_parse_error(&error);
// Should have clear syntax error message
assert!(
formatted.contains("Syntax error") || formatted.contains("syntax error"),
"Error should be clear syntax error: {}",
formatted
);
// Should not just be the generic "Parse error:" prefix
assert!(
!formatted.starts_with("Parse error:"),
"Should use specific error formatting, not generic prefix"
);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
// ===== PROPERTY TESTS (PROPERTY PHASE) =====
// Property: Parser should never panic on any input
proptest! {
#[test]
fn prop_parse_never_panics(input in ".*{0,1000}") {
// Test that parser gracefully handles any input without panicking
let _ = parse_bash(&input);
// If we get here without panic, test passes
}
}
// Property: Parser should produce valid AST or error
proptest! {
#[test]
fn prop_parse_produces_valid_result(input in "[a-z ]{1,100}") {
let result = parse_bash(&input);
// Result must be either Ok(AST) or Err(ParseError)
// Both variants are valid - no undefined state
match result {
Ok(ast) => {
// Valid AST should have consistent structure
prop_assert!(ast.statements.len() < 1000, "AST size reasonable");
}
Err(_) => {
// Error is also a valid outcome
}
}
}
}
// Property: Empty/whitespace input should always succeed
proptest! {
#[test]
fn prop_parse_empty_whitespace_succeeds(
spaces in r"[ \t\n]{0,100}"
) {
let result = parse_bash(&spaces);
prop_assert!(result.is_ok(), "Empty/whitespace should parse successfully");
let ast = result.unwrap();
prop_assert_eq!(ast.statements.len(), 0, "Should have no statements");
}
}
// Property: Valid commands should parse successfully
proptest! {
#[test]
fn prop_parse_valid_commands(
cmd in "[a-z]{1,10}",
arg in "[a-z0-9]{0,20}"
) {
let input = if arg.is_empty() {
cmd.clone()
} else {
format!("{} {}", cmd, arg)
};
let result = parse_bash(&input);
// Should either parse successfully or fail with clear error
// (some generated commands may not be valid syntax)
match result {
Ok(ast) => {
prop_assert!(!ast.statements.is_empty(), "Should have at least 1 statement");
}
Err(error) => {
let formatted = format_parse_error(&error);
// Updated to match new improved error formatting
let has_error_type = formatted.contains("Syntax error")
|| formatted.contains("syntax error")
|| formatted.contains("Lexer error");
prop_assert!(has_error_type, "Should format error message: {}", formatted);
}
}
}
}
// Property: Parse error formatting should never be empty
proptest! {
#[test]
fn prop_error_formatting_never_empty(input in ".*{1,100}") {
if let Err(error) = parse_bash(&input) {
let formatted = format_parse_error(&error);
prop_assert!(!formatted.is_empty(), "Error message should not be empty");
// Updated to match new improved error formatting
let has_error_type = formatted.contains("Syntax error")
|| formatted.contains("syntax error")
|| formatted.contains("Lexer error");
prop_assert!(has_error_type, "Should have specific error type: {}", formatted);
}
}
}
// Property: Multiline commands should parse or error gracefully
proptest! {
#[test]
fn prop_parse_multiline_graceful(
line1 in "[a-z ]{1,50}",
line2 in "[a-z ]{1,50}",
line3 in "[a-z ]{1,50}"
) {
let input = format!("{}\n{}\n{}", line1, line2, line3);
let result = parse_bash(&input);
// Should handle multiline input without panicking
match result {
Ok(ast) => {
// Multiple lines may produce multiple statements
prop_assert!(ast.statements.len() <= 3, "Should not exceed line count");
}
Err(_) => {
// Error is valid for malformed multiline
}
}
}
}
// ===== REPL-004-003 PROPERTY TESTS (PROPERTY PHASE) =====
// Property: All syntax errors have helpful, non-empty messages
proptest! {
#[test]
fn prop_syntax_errors_always_helpful(input in ".*{1,100}") {
if let Err(error) = parse_bash(&input) {
let formatted = format_parse_error(&error);
// Error messages must never be empty
prop_assert!(!formatted.is_empty(), "Error message should not be empty");
// Error messages should be helpful (contain key words)
let is_helpful = formatted.contains("Syntax error")
|| formatted.contains("syntax error")
|| formatted.contains("Lexer error")
|| formatted.contains("line")
|| formatted.contains("expected")
|| formatted.contains("found")
|| formatted.contains("unexpected");
prop_assert!(
is_helpful,
"Error message should be helpful and descriptive: {}",
formatted
);
}
}
}
// Property: Errors with line numbers always format them correctly
proptest! {
#[test]
fn prop_line_numbers_formatted_correctly(input in ".*{1,200}") {
if let Err(error) = parse_bash(&input) {
let formatted = format_parse_error(&error);
// If the error mentions a line number, it should be formatted correctly
if formatted.contains("line") {
// Line numbers should be positive integers
// This is a simple sanity check
prop_assert!(
!formatted.contains("line 0"),
"Line numbers should start at 1, not 0"
);
}
}
}
}
}