Skip to content

Commit 6ebba00

Browse files
committed
refactor: extract check command into its own module (#1350)
Move the check command's orchestration logic, output analysis types, and parsing functions from cli.rs into a dedicated check/ directory, following the same pattern as exec/. cli.rs: 1,778 → 1,314 lines check/mod.rs: execute_check() orchestration (247 lines) check/analysis.rs: types, parsers, formatters (255 lines) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Primarily a refactor, but it changes how `vp check` is dispatched and how fmt/lint output is captured/parsed, which could affect exit codes or CI output stability. > > **Overview** > Refactors the `vp check` implementation by extracting its orchestration and output-parsing logic from `cli.rs` into a new `check/` module (`check/mod.rs` + `check/analysis.rs`), keeping the same fmt→lint flow and `--fix` re-format pass. > > Updates `cli.rs` visibility to reuse `resolve_universal_vite_config` and `resolve_and_capture_output`, and switches the `Check` subcommand handler to delegate to `crate::check::execute_check`; related `LintMessageKind` tests move with the extraction. Documentation in `rfcs/check-command.md` is updated to reflect the new module layout and cache-input helper naming. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e9cf38e. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6a9eb3d commit 6ebba00

File tree

5 files changed

+527
-484
lines changed

5 files changed

+527
-484
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use std::io::IsTerminal;
2+
3+
use owo_colors::OwoColorize;
4+
use vite_shared::output;
5+
6+
#[derive(Debug, Clone)]
7+
pub(super) struct CheckSummary {
8+
pub duration: String,
9+
pub files: usize,
10+
pub threads: usize,
11+
}
12+
13+
#[derive(Debug)]
14+
pub(super) struct FmtSuccess {
15+
pub summary: CheckSummary,
16+
}
17+
18+
#[derive(Debug)]
19+
pub(super) struct FmtFailure {
20+
pub summary: CheckSummary,
21+
pub issue_files: Vec<String>,
22+
pub issue_count: usize,
23+
}
24+
25+
#[derive(Debug)]
26+
pub(super) struct LintSuccess {
27+
pub summary: CheckSummary,
28+
}
29+
30+
#[derive(Debug)]
31+
pub(super) struct LintFailure {
32+
pub summary: CheckSummary,
33+
pub warnings: usize,
34+
pub errors: usize,
35+
pub diagnostics: String,
36+
}
37+
38+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39+
pub(super) enum LintMessageKind {
40+
LintOnly,
41+
LintAndTypeCheck,
42+
}
43+
44+
impl LintMessageKind {
45+
pub(super) fn from_lint_config(lint_config: Option<&serde_json::Value>) -> Self {
46+
let type_check_enabled = lint_config
47+
.and_then(|config| config.get("options"))
48+
.and_then(|options| options.get("typeCheck"))
49+
.and_then(serde_json::Value::as_bool)
50+
.unwrap_or(false);
51+
52+
if type_check_enabled { Self::LintAndTypeCheck } else { Self::LintOnly }
53+
}
54+
55+
pub(super) fn success_label(self) -> &'static str {
56+
match self {
57+
Self::LintOnly => "Found no warnings or lint errors",
58+
Self::LintAndTypeCheck => "Found no warnings, lint errors, or type errors",
59+
}
60+
}
61+
62+
pub(super) fn warning_heading(self) -> &'static str {
63+
match self {
64+
Self::LintOnly => "Lint warnings found",
65+
Self::LintAndTypeCheck => "Lint or type warnings found",
66+
}
67+
}
68+
69+
pub(super) fn issue_heading(self) -> &'static str {
70+
match self {
71+
Self::LintOnly => "Lint issues found",
72+
Self::LintAndTypeCheck => "Lint or type issues found",
73+
}
74+
}
75+
}
76+
77+
fn parse_check_summary(line: &str) -> Option<CheckSummary> {
78+
let rest = line.strip_prefix("Finished in ")?;
79+
let (duration, rest) = rest.split_once(" on ")?;
80+
let files = rest.split_once(" file")?.0.parse().ok()?;
81+
let (_, threads_part) = rest.rsplit_once(" using ")?;
82+
let threads = threads_part.split_once(" thread")?.0.parse().ok()?;
83+
84+
Some(CheckSummary { duration: duration.to_string(), files, threads })
85+
}
86+
87+
fn parse_issue_count(line: &str, prefix: &str) -> Option<usize> {
88+
let rest = line.strip_prefix(prefix)?;
89+
rest.split_once(" file")?.0.parse().ok()
90+
}
91+
92+
fn parse_warning_error_counts(line: &str) -> Option<(usize, usize)> {
93+
let rest = line.strip_prefix("Found ")?;
94+
let (warnings, rest) = rest.split_once(" warning")?;
95+
let (_, rest) = rest.split_once(" and ")?;
96+
let errors = rest.split_once(" error")?.0;
97+
Some((warnings.parse().ok()?, errors.parse().ok()?))
98+
}
99+
100+
pub(super) fn format_elapsed(elapsed: std::time::Duration) -> String {
101+
if elapsed.as_millis() < 1000 {
102+
format!("{}ms", elapsed.as_millis())
103+
} else {
104+
format!("{:.1}s", elapsed.as_secs_f64())
105+
}
106+
}
107+
108+
pub(super) fn format_count(count: usize, singular: &str, plural: &str) -> String {
109+
if count == 1 { format!("1 {singular}") } else { format!("{count} {plural}") }
110+
}
111+
112+
pub(super) fn print_stdout_block(block: &str) {
113+
let trimmed = block.trim_matches('\n');
114+
if trimmed.is_empty() {
115+
return;
116+
}
117+
118+
use std::io::Write;
119+
let mut stdout = std::io::stdout().lock();
120+
let _ = stdout.write_all(trimmed.as_bytes());
121+
let _ = stdout.write_all(b"\n");
122+
}
123+
124+
pub(super) fn print_summary_line(message: &str) {
125+
output::raw("");
126+
if std::io::stdout().is_terminal() && message.contains('`') {
127+
let mut formatted = String::with_capacity(message.len());
128+
let mut segments = message.split('`');
129+
if let Some(first) = segments.next() {
130+
formatted.push_str(first);
131+
}
132+
let mut is_accent = true;
133+
for segment in segments {
134+
if is_accent {
135+
formatted.push_str(&format!("{}", format!("`{segment}`").bright_blue()));
136+
} else {
137+
formatted.push_str(segment);
138+
}
139+
is_accent = !is_accent;
140+
}
141+
output::raw(&formatted);
142+
} else {
143+
output::raw(message);
144+
}
145+
}
146+
147+
pub(super) fn print_error_block(error_msg: &str, combined_output: &str, summary_msg: &str) {
148+
output::error(error_msg);
149+
if !combined_output.trim().is_empty() {
150+
print_stdout_block(combined_output);
151+
}
152+
print_summary_line(summary_msg);
153+
}
154+
155+
pub(super) fn print_pass_line(message: &str, detail: Option<&str>) {
156+
if let Some(detail) = detail {
157+
output::raw(&format!("{} {message} {}", "pass:".bright_blue().bold(), detail.dimmed()));
158+
} else {
159+
output::pass(message);
160+
}
161+
}
162+
163+
pub(super) fn analyze_fmt_check_output(output: &str) -> Option<Result<FmtSuccess, FmtFailure>> {
164+
let trimmed = output.trim();
165+
if trimmed.is_empty() {
166+
return None;
167+
}
168+
169+
let lines: Vec<&str> = trimmed.lines().collect();
170+
let finish_line = lines.iter().rev().find(|line| line.starts_with("Finished in "))?;
171+
let summary = parse_check_summary(finish_line)?;
172+
173+
if lines.iter().any(|line| *line == "All matched files use the correct format.") {
174+
return Some(Ok(FmtSuccess { summary }));
175+
}
176+
177+
let issue_line = lines.iter().find(|line| line.starts_with("Format issues found in above "))?;
178+
let issue_count = parse_issue_count(issue_line, "Format issues found in above ")?;
179+
180+
let mut issue_files = Vec::new();
181+
let mut collecting = false;
182+
for line in lines {
183+
if line == "Checking formatting..." {
184+
collecting = true;
185+
continue;
186+
}
187+
if !collecting {
188+
continue;
189+
}
190+
if line.is_empty() {
191+
continue;
192+
}
193+
if line.starts_with("Format issues found in above ") || line.starts_with("Finished in ") {
194+
break;
195+
}
196+
issue_files.push(line.to_string());
197+
}
198+
199+
Some(Err(FmtFailure { summary, issue_files, issue_count }))
200+
}
201+
202+
pub(super) fn analyze_lint_output(output: &str) -> Option<Result<LintSuccess, LintFailure>> {
203+
let trimmed = output.trim();
204+
if trimmed.is_empty() {
205+
return None;
206+
}
207+
208+
let lines: Vec<&str> = trimmed.lines().collect();
209+
let counts_idx = lines.iter().position(|line| {
210+
line.starts_with("Found ") && line.contains(" warning") && line.contains(" error")
211+
})?;
212+
let finish_line =
213+
lines.iter().skip(counts_idx + 1).find(|line| line.starts_with("Finished in "))?;
214+
215+
let summary = parse_check_summary(finish_line)?;
216+
let (warnings, errors) = parse_warning_error_counts(lines[counts_idx])?;
217+
let diagnostics = lines[..counts_idx].join("\n").trim_matches('\n').to_string();
218+
219+
if warnings == 0 && errors == 0 {
220+
return Some(Ok(LintSuccess { summary }));
221+
}
222+
223+
Some(Err(LintFailure { summary, warnings, errors, diagnostics }))
224+
}
225+
226+
#[cfg(test)]
227+
mod tests {
228+
use serde_json::json;
229+
230+
use super::LintMessageKind;
231+
232+
#[test]
233+
fn lint_message_kind_defaults_to_lint_only_without_typecheck() {
234+
assert_eq!(LintMessageKind::from_lint_config(None), LintMessageKind::LintOnly);
235+
assert_eq!(
236+
LintMessageKind::from_lint_config(Some(&json!({ "options": {} }))),
237+
LintMessageKind::LintOnly
238+
);
239+
}
240+
241+
#[test]
242+
fn lint_message_kind_detects_typecheck_from_vite_config() {
243+
let kind = LintMessageKind::from_lint_config(Some(&json!({
244+
"options": {
245+
"typeAware": true,
246+
"typeCheck": true
247+
}
248+
})));
249+
250+
assert_eq!(kind, LintMessageKind::LintAndTypeCheck);
251+
assert_eq!(kind.success_label(), "Found no warnings, lint errors, or type errors");
252+
assert_eq!(kind.warning_heading(), "Lint or type warnings found");
253+
assert_eq!(kind.issue_heading(), "Lint or type issues found");
254+
}
255+
}

0 commit comments

Comments
 (0)