Skip to content

Commit 6e06c17

Browse files
authored
feat(check): support --no-error-on-unmatched-pattern for fmt and lint (#1354)
## Summary - In `--fix` mode with file paths, implicitly pass `--no-error-on-unmatched-pattern` to both oxfmt and oxlint, preventing errors when all staged files are excluded by ignorePatterns (the lint-staged use case). - Expose `--no-error-on-unmatched-pattern` as an explicit CLI flag for non-fix use cases. Both oxfmt and oxlint support this flag natively. - Add snap tests covering: unmatched patterns pass in fix mode, non-lintable files (package.json) pass in fix mode, real lint errors are not swallowed, and `vp lint package.json` exits non-zero. Closes #1210 ## Test plan - [x] `pnpm -F vite-plus snap-test-local check-fix-no-error-unmatched` — verifies implicit/explicit flag behavior - [x] `pnpm -F vite-plus snap-test-local check-fix-lint-error-not-swallowed` — verifies real lint errors surface - [x] `pnpm -F vite-plus snap-test-local lint-unmatched-pattern` — verifies `vp lint package.json` exits non-zero - [x] `pnpm -F vite-plus snap-test-local check` — all 25 check snap tests pass - [x] `pnpm -F vite-plus snap-test-global command-check-help` — help output includes new flag
1 parent e159b84 commit 6e06c17

File tree

19 files changed

+163
-26
lines changed

19 files changed

+163
-26
lines changed

crates/vite_global_cli/src/help.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,10 @@ fn delegated_help_doc(command: &str) -> Option<HelpDoc> {
760760
row("--fix", "Auto-fix format and lint issues"),
761761
row("--no-fmt", "Skip format check"),
762762
row("--no-lint", "Skip lint check"),
763+
row(
764+
"--no-error-on-unmatched-pattern",
765+
"Do not exit with error when pattern is unmatched",
766+
),
763767
row("-h, --help", "Print help"),
764768
],
765769
),

packages/cli/binding/src/check/mod.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) async fn execute_check(
2222
fix: bool,
2323
no_fmt: bool,
2424
no_lint: bool,
25+
no_error_on_unmatched_pattern: bool,
2526
paths: Vec<String>,
2627
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
2728
cwd: &AbsolutePathBuf,
@@ -37,14 +38,20 @@ pub(crate) async fn execute_check(
3738

3839
let mut status = ExitStatus::SUCCESS;
3940
let has_paths = !paths.is_empty();
41+
// In --fix mode with file paths (the lint-staged use case), implicitly suppress
42+
// "no matching files" errors. This is also available as an explicit flag for
43+
// non-fix use cases.
44+
let suppress_unmatched = no_error_on_unmatched_pattern || (fix && has_paths);
4045
let mut fmt_fix_started: Option<Instant> = None;
4146
let mut deferred_lint_pass: Option<(String, String)> = None;
4247
let resolved_vite_config = resolver.resolve_universal_vite_config().await?;
4348

4449
if !no_fmt {
4550
let mut args = if fix { vec![] } else { vec!["--check".to_string()] };
46-
if has_paths {
51+
if suppress_unmatched {
4752
args.push("--no-error-on-unmatched-pattern".to_string());
53+
}
54+
if has_paths {
4855
args.extend(paths.iter().cloned());
4956
}
5057
let fmt_start = Instant::now();
@@ -87,11 +94,17 @@ pub(crate) async fn execute_check(
8794
));
8895
}
8996
None => {
90-
print_error_block(
91-
"Formatting could not start",
92-
&combined_output,
93-
"Formatting failed before analysis started",
94-
);
97+
// oxfmt handles --no-error-on-unmatched-pattern natively and
98+
// exits 0 when no files match, so we only need to guard
99+
// against the edge case where output is unparsable but the
100+
// process still succeeded.
101+
if !(suppress_unmatched && status == ExitStatus::SUCCESS) {
102+
print_error_block(
103+
"Formatting could not start",
104+
&combined_output,
105+
"Formatting failed before analysis started",
106+
);
107+
}
95108
}
96109
}
97110
}
@@ -127,6 +140,9 @@ pub(crate) async fn execute_check(
127140
// parser think linting never started. Force the default reporter here so the
128141
// captured output is stable across local and CI environments.
129142
args.push("--format=default".to_string());
143+
if suppress_unmatched {
144+
args.push("--no-error-on-unmatched-pattern".to_string());
145+
}
130146
if has_paths {
131147
args.extend(paths.iter().cloned());
132148
}
@@ -177,11 +193,17 @@ pub(crate) async fn execute_check(
177193
));
178194
}
179195
None => {
180-
output::error("Linting could not start");
181-
if !combined_output.trim().is_empty() {
182-
print_stdout_block(&combined_output);
196+
// oxlint handles --no-error-on-unmatched-pattern natively and
197+
// exits 0 when no files match, so we only need to guard
198+
// against the edge case where output is unparsable but the
199+
// process still succeeded.
200+
if !(suppress_unmatched && status == ExitStatus::SUCCESS) {
201+
output::error("Linting could not start");
202+
if !combined_output.trim().is_empty() {
203+
print_stdout_block(&combined_output);
204+
}
205+
print_summary_line("Linting failed before analysis started");
183206
}
184-
print_summary_line("Linting failed before analysis started");
185207
}
186208
}
187209
if status != ExitStatus::SUCCESS {
@@ -193,8 +215,10 @@ pub(crate) async fn execute_check(
193215
// (e.g. the curly rule adding braces to if-statements)
194216
if fix && !no_fmt && !no_lint {
195217
let mut args = Vec::new();
196-
if has_paths {
218+
if suppress_unmatched {
197219
args.push("--no-error-on-unmatched-pattern".to_string());
220+
}
221+
if has_paths {
198222
args.extend(paths.into_iter());
199223
}
200224
let captured = resolve_and_capture_output(

packages/cli/binding/src/cli/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,23 @@ async fn execute_direct_subcommand(
6363
let cwd_arc: Arc<AbsolutePath> = cwd.clone().into();
6464

6565
let status = match subcommand {
66-
SynthesizableSubcommand::Check { fix, no_fmt, no_lint, paths } => {
66+
SynthesizableSubcommand::Check {
67+
fix,
68+
no_fmt,
69+
no_lint,
70+
no_error_on_unmatched_pattern,
71+
paths,
72+
} => {
6773
return crate::check::execute_check(
68-
&resolver, fix, no_fmt, no_lint, paths, &envs, cwd, &cwd_arc,
74+
&resolver,
75+
fix,
76+
no_fmt,
77+
no_lint,
78+
no_error_on_unmatched_pattern,
79+
paths,
80+
&envs,
81+
cwd,
82+
&cwd_arc,
6983
)
7084
.await;
7185
}

packages/cli/binding/src/cli/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ pub enum SynthesizableSubcommand {
9393
/// Skip lint check
9494
#[arg(long = "no-lint")]
9595
no_lint: bool,
96+
/// Do not exit with error when pattern is unmatched
97+
#[arg(long = "no-error-on-unmatched-pattern")]
98+
no_error_on_unmatched_pattern: bool,
9699
/// File paths to check (passed through to fmt and lint)
97100
#[arg(trailing_var_arg = true)]
98101
paths: Vec<String>,

packages/cli/snap-tests-global/command-check-help/snap.txt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ Usage: vp check [OPTIONS] [PATHS]...
66
Run format, lint, and type checks.
77

88
Options:
9-
--fix Auto-fix format and lint issues
10-
--no-fmt Skip format check
11-
--no-lint Skip lint check
12-
-h, --help Print help
9+
--fix Auto-fix format and lint issues
10+
--no-fmt Skip format check
11+
--no-lint Skip lint check
12+
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
13+
-h, --help Print help
1314

1415
Examples:
1516
vp check
@@ -27,10 +28,11 @@ Usage: vp check [OPTIONS] [PATHS]...
2728
Run format, lint, and type checks.
2829

2930
Options:
30-
--fix Auto-fix format and lint issues
31-
--no-fmt Skip format check
32-
--no-lint Skip lint check
33-
-h, --help Print help
31+
--fix Auto-fix format and lint issues
32+
--no-fmt Skip format check
33+
--no-lint Skip lint check
34+
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
35+
-h, --help Print help
3436

3537
Examples:
3638
vp check
@@ -48,10 +50,11 @@ Usage: vp check [OPTIONS] [PATHS]...
4850
Run format, lint, and type checks.
4951

5052
Options:
51-
--fix Auto-fix format and lint issues
52-
--no-fmt Skip format check
53-
--no-lint Skip lint check
54-
-h, --help Print help
53+
--fix Auto-fix format and lint issues
54+
--no-fmt Skip format check
55+
--no-lint Skip lint check
56+
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
57+
-h, --help Print help
5558

5659
Examples:
5760
vp check
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "check-fix-lint-error-not-swallowed",
3+
"version": "0.0.0",
4+
"private": true
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[1]> vp check --fix src/index.js # real lint error with --fix and paths (suppress_unmatched active), error must not be swallowed
2+
error: Lint issues found
3+
× eslint(no-eval): eval can be harmful.
4+
╭─[src/index.js:2:3]
5+
1 │ function hello() {
6+
2 │ eval("code");
7+
· ────
8+
3 │ return "hello";
9+
╰────
10+
help: Avoid eval(). For JSON parsing use JSON.parse(); for dynamic property access use bracket notation (obj[key]); for other cases refactor to avoid evaluating strings as code.
11+
12+
Found 1 error and 0 warnings in 1 file (<variable>ms, <variable> threads)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
function hello() {
2+
eval("code");
3+
return "hello";
4+
}
5+
6+
export { hello };
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"commands": [
3+
"vp check --fix src/index.js # real lint error with --fix and paths (suppress_unmatched active), error must not be swallowed"
4+
]
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
lint: {
3+
rules: {
4+
"no-eval": "error",
5+
},
6+
},
7+
};

0 commit comments

Comments
 (0)