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
// REPL Linter Integration Module
//
// Task: REPL-006-001 - Run linter from REPL
// Test Approach: RED → GREEN → REFACTOR → INTEGRATION
//
// Quality targets:
// - Unit tests: 3+ scenarios
// - Integration tests: CLI workflow
// - Complexity: <10 per function
use crate::linter::{lint_shell, LintResult, Severity};
/// Lint bash input and return diagnostics
///
/// # Examples
///
/// ```
/// use bashrs::repl::linter::lint_bash;
///
/// let result = lint_bash("cat file.txt | grep pattern");
/// assert!(result.is_ok());
/// ```
pub fn lint_bash(input: &str) -> anyhow::Result<LintResult> {
let result = lint_shell(input);
Ok(result)
}
/// Format lint results for display in REPL
pub fn format_lint_results(result: &LintResult) -> String {
let mut output = String::new();
if result.diagnostics.is_empty() {
output.push_str("✓ No issues found!\n");
return output;
}
// Count by severity
let errors = result
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.count();
let warnings = result
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Warning)
.count();
let info = result
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Info)
.count();
output.push_str(&format!("Found {} issue(s):\n", result.diagnostics.len()));
if errors > 0 {
output.push_str(&format!(" ✗ {} error(s)\n", errors));
}
if warnings > 0 {
output.push_str(&format!(" ⚠ {} warning(s)\n", warnings));
}
if info > 0 {
output.push_str(&format!(" ℹ {} info\n", info));
}
output.push('\n');
// Show diagnostics
for (i, diag) in result.diagnostics.iter().enumerate() {
let severity_icon = match diag.severity {
Severity::Error => "✗",
Severity::Warning => "⚠",
Severity::Info => "ℹ",
Severity::Note => "📝",
Severity::Perf => "⚡",
Severity::Risk => "⚠",
};
output.push_str(&format!(
"[{}] {} {} - {}\n",
i + 1,
severity_icon,
diag.code,
diag.message
));
if diag.span.start_line > 0 {
output.push_str(&format!(" Line {}\n", diag.span.start_line));
}
}
output
}
/// Format lint violations with source code context (REPL-014-003)
///
/// Displays each violation with:
/// - Line numbers (±2 lines of context)
/// - Source code at that location
/// - Visual indicator (caret) pointing to the issue
/// - Diagnostic message with rule code
/// - Fix suggestion if available
///
/// # Examples
///
/// ```no_run
/// use bashrs::repl::linter::{format_violations_with_context, lint_bash};
///
/// let source = "echo $RANDOM\nmkdir /app\n";
/// let result = lint_bash(source).unwrap();
/// let formatted = format_violations_with_context(&result, source);
/// ```
pub fn format_violations_with_context(result: &LintResult, source: &str) -> String {
let mut output = String::new();
if result.diagnostics.is_empty() {
return "✓ No violations\n".to_string();
}
let lines: Vec<&str> = source.lines().collect();
let max_line_num = lines.len();
let line_num_width = max_line_num.to_string().len().max(3);
for diagnostic in &result.diagnostics {
let line_idx = diagnostic.span.start_line.saturating_sub(1);
// Show context: ±2 lines
let start_line = line_idx.saturating_sub(2);
let end_line = (line_idx + 3).min(lines.len());
output.push('\n');
// Show context lines
for i in start_line..end_line {
if i < lines.len() {
let line_num = i + 1;
let prefix = if i == line_idx { ">" } else { " " };
if let Some(line) = lines.get(i) {
output.push_str(&format!(
"{} {:>width$} | {}\n",
prefix,
line_num,
line,
width = line_num_width
));
// Show indicator on the problematic line
if i == line_idx {
let col = diagnostic.span.start_col.saturating_sub(1);
let indicator_width =
if diagnostic.span.end_line == diagnostic.span.start_line {
diagnostic
.span
.end_col
.saturating_sub(diagnostic.span.start_col)
.max(1)
} else {
line.len().saturating_sub(col).max(1)
};
output.push_str(&format!(
" {:>width$} | {}{} {} [{}]: {}\n",
"",
" ".repeat(col),
"^".repeat(indicator_width),
diagnostic.severity,
diagnostic.code,
diagnostic.message,
width = line_num_width
));
}
}
}
}
// Show fix suggestion if available
if let Some(fix) = &diagnostic.fix {
output.push_str("\n Suggested fix:\n");
output.push_str(&format!(
" {:>width$} | {}\n",
line_idx + 1,
fix.replacement,
width = line_num_width
));
}
}
output
}
#[cfg(test)]
#[path = "linter_tests_repl_006.rs"]
mod tests_extracted;