diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index e3e1e956f8..1933a1cf43 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -759,6 +759,10 @@ fn delegated_help_doc(command: &str) -> Option { row("--fix", "Auto-fix format and lint issues"), row("--no-fmt", "Skip format check"), row("--no-lint", "Skip lint check"), + row( + "--no-error-on-unmatched-pattern", + "Do not exit with error when pattern is unmatched", + ), row("-h, --help", "Print help"), ], ), diff --git a/packages/cli/binding/src/check/mod.rs b/packages/cli/binding/src/check/mod.rs index d166723e19..2f81d89ee7 100644 --- a/packages/cli/binding/src/check/mod.rs +++ b/packages/cli/binding/src/check/mod.rs @@ -22,6 +22,7 @@ pub(crate) async fn execute_check( fix: bool, no_fmt: bool, no_lint: bool, + no_error_on_unmatched_pattern: bool, paths: Vec, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, @@ -37,14 +38,20 @@ pub(crate) async fn execute_check( let mut status = ExitStatus::SUCCESS; let has_paths = !paths.is_empty(); + // In --fix mode with file paths (the lint-staged use case), implicitly suppress + // "no matching files" errors. This is also available as an explicit flag for + // non-fix use cases. + let suppress_unmatched = no_error_on_unmatched_pattern || (fix && has_paths); let mut fmt_fix_started: Option = None; let mut deferred_lint_pass: Option<(String, String)> = None; let resolved_vite_config = resolver.resolve_universal_vite_config().await?; if !no_fmt { let mut args = if fix { vec![] } else { vec!["--check".to_string()] }; - if has_paths { + if suppress_unmatched { args.push("--no-error-on-unmatched-pattern".to_string()); + } + if has_paths { args.extend(paths.iter().cloned()); } let fmt_start = Instant::now(); @@ -87,11 +94,17 @@ pub(crate) async fn execute_check( )); } None => { - print_error_block( - "Formatting could not start", - &combined_output, - "Formatting failed before analysis started", - ); + // oxfmt handles --no-error-on-unmatched-pattern natively and + // exits 0 when no files match, so we only need to guard + // against the edge case where output is unparsable but the + // process still succeeded. + if !(suppress_unmatched && status == ExitStatus::SUCCESS) { + print_error_block( + "Formatting could not start", + &combined_output, + "Formatting failed before analysis started", + ); + } } } } @@ -177,11 +190,18 @@ pub(crate) async fn execute_check( )); } None => { - output::error("Linting could not start"); - if !combined_output.trim().is_empty() { - print_stdout_block(&combined_output); + // Only suppress when the output is empty (no files to lint). + // If oxlint produced error output (config error, crash, etc.), + // surface it even when suppress_unmatched is active. + if suppress_unmatched && combined_output.trim().is_empty() { + status = ExitStatus::SUCCESS; + } else { + output::error("Linting could not start"); + if !combined_output.trim().is_empty() { + print_stdout_block(&combined_output); + } + print_summary_line("Linting failed before analysis started"); } - print_summary_line("Linting failed before analysis started"); } } if status != ExitStatus::SUCCESS { @@ -193,8 +213,10 @@ pub(crate) async fn execute_check( // (e.g. the curly rule adding braces to if-statements) if fix && !no_fmt && !no_lint { let mut args = Vec::new(); - if has_paths { + if suppress_unmatched { args.push("--no-error-on-unmatched-pattern".to_string()); + } + if has_paths { args.extend(paths.into_iter()); } let captured = resolve_and_capture_output( diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 5811ef75bc..f94443964b 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -63,9 +63,23 @@ async fn execute_direct_subcommand( let cwd_arc: Arc = cwd.clone().into(); let status = match subcommand { - SynthesizableSubcommand::Check { fix, no_fmt, no_lint, paths } => { + SynthesizableSubcommand::Check { + fix, + no_fmt, + no_lint, + no_error_on_unmatched_pattern, + paths, + } => { return crate::check::execute_check( - &resolver, fix, no_fmt, no_lint, paths, &envs, cwd, &cwd_arc, + &resolver, + fix, + no_fmt, + no_lint, + no_error_on_unmatched_pattern, + paths, + &envs, + cwd, + &cwd_arc, ) .await; } diff --git a/packages/cli/binding/src/cli/types.rs b/packages/cli/binding/src/cli/types.rs index 6e408460b6..8764d001a4 100644 --- a/packages/cli/binding/src/cli/types.rs +++ b/packages/cli/binding/src/cli/types.rs @@ -93,6 +93,9 @@ pub enum SynthesizableSubcommand { /// Skip lint check #[arg(long = "no-lint")] no_lint: bool, + /// Do not exit with error when pattern is unmatched + #[arg(long = "no-error-on-unmatched-pattern")] + no_error_on_unmatched_pattern: bool, /// File paths to check (passed through to fmt and lint) #[arg(trailing_var_arg = true)] paths: Vec, diff --git a/packages/cli/snap-tests-global/command-check-help/snap.txt b/packages/cli/snap-tests-global/command-check-help/snap.txt index acfb446b24..830728f645 100644 --- a/packages/cli/snap-tests-global/command-check-help/snap.txt +++ b/packages/cli/snap-tests-global/command-check-help/snap.txt @@ -6,10 +6,11 @@ Usage: vp check [OPTIONS] [PATHS]... Run format, lint, and type checks. Options: - --fix Auto-fix format and lint issues - --no-fmt Skip format check - --no-lint Skip lint check - -h, --help Print help + --fix Auto-fix format and lint issues + --no-fmt Skip format check + --no-lint Skip lint check + --no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched + -h, --help Print help Examples: vp check @@ -27,10 +28,11 @@ Usage: vp check [OPTIONS] [PATHS]... Run format, lint, and type checks. Options: - --fix Auto-fix format and lint issues - --no-fmt Skip format check - --no-lint Skip lint check - -h, --help Print help + --fix Auto-fix format and lint issues + --no-fmt Skip format check + --no-lint Skip lint check + --no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched + -h, --help Print help Examples: vp check @@ -48,10 +50,11 @@ Usage: vp check [OPTIONS] [PATHS]... Run format, lint, and type checks. Options: - --fix Auto-fix format and lint issues - --no-fmt Skip format check - --no-lint Skip lint check - -h, --help Print help + --fix Auto-fix format and lint issues + --no-fmt Skip format check + --no-lint Skip lint check + --no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched + -h, --help Print help Examples: vp check diff --git a/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/package.json b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/package.json new file mode 100644 index 0000000000..efa7212f3f --- /dev/null +++ b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fix-lint-error-not-swallowed", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/snap.txt b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/snap.txt new file mode 100644 index 0000000000..a9604d89a4 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/snap.txt @@ -0,0 +1,12 @@ +[1]> vp check --fix src/index.js # real lint error with --fix and paths (suppress_unmatched active), error must not be swallowed +error: Lint issues found +× eslint(no-eval): eval can be harmful. + ╭─[src/index.js:2:3] + 1 │ function hello() { + 2 │ eval("code"); + · ──── + 3 │ return "hello"; + ╰──── + 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. + +Found 1 error and 0 warnings in 1 file (ms, threads) diff --git a/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/src/index.js b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/src/index.js new file mode 100644 index 0000000000..e916f931f1 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/src/index.js @@ -0,0 +1,6 @@ +function hello() { + eval("code"); + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/steps.json b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/steps.json new file mode 100644 index 0000000000..1905b1a113 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/steps.json @@ -0,0 +1,5 @@ +{ + "commands": [ + "vp check --fix src/index.js # real lint error with --fix and paths (suppress_unmatched active), error must not be swallowed" + ] +} diff --git a/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/vite.config.ts b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/vite.config.ts new file mode 100644 index 0000000000..96170b5f50 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-lint-error-not-swallowed/vite.config.ts @@ -0,0 +1,7 @@ +export default { + lint: { + rules: { + "no-eval": "error", + }, + }, +}; diff --git a/packages/cli/snap-tests/check-fix-no-error-unmatched/package.json b/packages/cli/snap-tests/check-fix-no-error-unmatched/package.json new file mode 100644 index 0000000000..b0aa60f883 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-no-error-unmatched/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fix-no-error-unmatched", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fix-no-error-unmatched/snap.txt b/packages/cli/snap-tests/check-fix-no-error-unmatched/snap.txt new file mode 100644 index 0000000000..cffc37c6d2 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-no-error-unmatched/snap.txt @@ -0,0 +1,16 @@ +> vp check --fix src/ignored/index.js # all files excluded by ignorePatterns, should pass in --fix mode +pass: Formatting completed for checked files (ms) +pass: Found no warnings or lint errors in 0 files (ms, threads) + +> vp check --no-error-on-unmatched-pattern src/ignored/index.js # explicit flag without --fix, should also pass +pass: Found no warnings or lint errors in 0 files (ms, threads) + +> vp check --fix --no-error-on-unmatched-pattern src/ignored/index.js # both flags set, should pass +pass: Formatting completed for checked files (ms) +pass: Found no warnings or lint errors in 0 files (ms, threads) + +[2]> vp check src/ignored/index.js # without --fix or explicit flag, should exit non-zero +error: Formatting could not start +Expected at least one target file + +Formatting failed before analysis started diff --git a/packages/cli/snap-tests/check-fix-no-error-unmatched/src/ignored/index.js b/packages/cli/snap-tests/check-fix-no-error-unmatched/src/ignored/index.js new file mode 100644 index 0000000000..ed0c6f4959 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-no-error-unmatched/src/ignored/index.js @@ -0,0 +1,2 @@ +// This file is excluded by both fmt and lint ignorePatterns. +export const hello = 'world'; diff --git a/packages/cli/snap-tests/check-fix-no-error-unmatched/steps.json b/packages/cli/snap-tests/check-fix-no-error-unmatched/steps.json new file mode 100644 index 0000000000..71394e3264 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-no-error-unmatched/steps.json @@ -0,0 +1,8 @@ +{ + "commands": [ + "vp check --fix src/ignored/index.js # all files excluded by ignorePatterns, should pass in --fix mode", + "vp check --no-error-on-unmatched-pattern src/ignored/index.js # explicit flag without --fix, should also pass", + "vp check --fix --no-error-on-unmatched-pattern src/ignored/index.js # both flags set, should pass", + "vp check src/ignored/index.js # without --fix or explicit flag, should exit non-zero" + ] +} diff --git a/packages/cli/snap-tests/check-fix-no-error-unmatched/vite.config.ts b/packages/cli/snap-tests/check-fix-no-error-unmatched/vite.config.ts new file mode 100644 index 0000000000..2f7870dfd7 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-no-error-unmatched/vite.config.ts @@ -0,0 +1,8 @@ +export default { + fmt: { + ignorePatterns: ['src/ignored/**/*'], + }, + lint: { + ignorePatterns: ['src/ignored/**/*'], + }, +}; diff --git a/rfcs/check-command.md b/rfcs/check-command.md index 58916604a5..c442c5767b 100644 --- a/rfcs/check-command.md +++ b/rfcs/check-command.md @@ -53,6 +53,7 @@ vp check --no-type-check | `--lint` / `--no-lint` | ON | Run lint check (`vp lint`) | | `--type-aware` / `--no-type-aware` | ON | Enable type-aware lint rules (oxlint `--type-aware`) | | `--type-check` / `--no-type-check` | ON | Enable TypeScript type checking (oxlint `--type-check`) | +| `--no-error-on-unmatched-pattern` | OFF | Do not exit with error when pattern is unmatched | **Flag dependency:** `--type-check` requires `--type-aware` as a prerequisite. @@ -73,8 +74,9 @@ vp check --fix src/index.ts src/utils.ts When file paths are provided: -- `--no-error-on-unmatched-pattern` is automatically added to `fmt` args (prevents errors when paths don't match fmt patterns) - Paths are appended to both `fmt` and `lint` sub-commands +- In `--fix` mode, `--no-error-on-unmatched-pattern` is implicitly enabled for both `fmt` and `lint`, preventing errors when all provided paths are excluded by ignorePatterns. This is the common lint-staged use case where staged files may not match tool-specific patterns. +- Without `--fix`, unmatched patterns are reported as errors unless `--no-error-on-unmatched-pattern` is explicitly passed. Note that oxfmt supports this flag natively, while oxlint does not — `vp check` handles the lint side by treating unparsable lint output as a pass when the flag is active. This enables lint-staged integration: @@ -208,6 +210,7 @@ Options: --lint Run lint check [default: true] --type-aware Enable type-aware linting [default: true] --type-check Enable TypeScript type checking [default: true] + --no-error-on-unmatched-pattern Do not exit with error when no files match -h, --help Print help ```