Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/vite_global_cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,10 @@ fn delegated_help_doc(command: &str) -> Option<HelpDoc> {
vec![
row("--fix", "Auto-fix format and lint issues"),
row("--no-fmt", "Skip format check"),
row("--no-lint", "Skip lint check"),
row(
"--no-lint",
"Skip lint rules; type-check still runs when `lint.options.typeCheck` is true",
),
row(
"--no-error-on-unmatched-pattern",
"Do not exit with error when pattern is unmatched",
Expand Down
7 changes: 6 additions & 1 deletion docs/guide/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ We recommend turning `typeCheck` on so `vp check` becomes the single command for

```bash
vp check
vp check --fix # Format and run autofixers.
vp check --fix # Format and run autofixers.
vp check --no-fmt # Skip format; run lint (and type-check if enabled).
vp check --no-lint # Skip lint rules; keep type-check when enabled.
vp check --no-fmt --no-lint # Type-check only (requires `typeCheck` enabled).
```

When `lint.options.typeCheck` is enabled, `--no-lint` keeps type diagnostics by forwarding Oxlint's `--type-check-only` flag — useful for triaging type errors without lint noise. If `typeCheck` is not enabled, `--no-lint` simply skips the lint phase altogether, and `vp check --no-fmt --no-lint` exits with `No checks enabled` (enable `lint.options.typeCheck` to use the type-check-only invocation).
Comment thread
fengmk2 marked this conversation as resolved.
Outdated

## Configuration

`vp check` uses the same configuration you already define for linting and formatting:
Expand Down
88 changes: 72 additions & 16 deletions packages/cli/binding/src/check/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,41 +39,61 @@ pub(super) struct LintFailure {
pub(super) enum LintMessageKind {
LintOnly,
LintAndTypeCheck,
TypeCheckOnly,
}

impl LintMessageKind {
pub(super) fn from_lint_config(lint_config: Option<&serde_json::Value>) -> Self {
let type_check_enabled = lint_config
.and_then(|config| config.get("options"))
.and_then(|options| options.get("typeCheck"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);

if type_check_enabled { Self::LintAndTypeCheck } else { Self::LintOnly }
pub(super) fn from_flags(lint_enabled: bool, type_check_enabled: bool) -> Self {
match (lint_enabled, type_check_enabled) {
(true, true) => Self::LintAndTypeCheck,
(true, false) => Self::LintOnly,
(false, true) => Self::TypeCheckOnly,
(false, false) => unreachable!(
"from_flags called with (false, false); caller must guard on run_lint_phase"
),
}
}

pub(super) fn success_label(self) -> &'static str {
match self {
Self::LintOnly => "Found no warnings or lint errors",
Self::LintAndTypeCheck => "Found no warnings, lint errors, or type errors",
Self::TypeCheckOnly => "Found no type errors",
}
}

pub(super) fn warning_heading(self) -> &'static str {
match self {
Self::LintOnly => "Lint warnings found",
Self::LintAndTypeCheck => "Lint or type warnings found",
Self::TypeCheckOnly => "Type warnings found",
}
}

pub(super) fn issue_heading(self) -> &'static str {
match self {
Self::LintOnly => "Lint issues found",
Self::LintAndTypeCheck => "Lint or type issues found",
Self::TypeCheckOnly => "Type errors found",
}
}
}

/// `typeCheck` requires `typeAware` as a prerequisite — oxlint's type-aware
/// analysis must be on for TypeScript diagnostics to surface.
pub(super) fn lint_config_type_check_enabled(lint_config: Option<&serde_json::Value>) -> bool {
let options = lint_config.and_then(|config| config.get("options"));
let type_aware = options
.and_then(|options| options.get("typeAware"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let type_check = options
.and_then(|options| options.get("typeCheck"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
type_aware && type_check
}

fn parse_check_summary(line: &str) -> Option<CheckSummary> {
let rest = line.strip_prefix("Finished in ")?;
let (duration, rest) = rest.split_once(" on ")?;
Expand Down Expand Up @@ -227,29 +247,65 @@ pub(super) fn analyze_lint_output(output: &str) -> Option<Result<LintSuccess, Li
mod tests {
use serde_json::json;

use super::LintMessageKind;
use super::{LintMessageKind, lint_config_type_check_enabled};

#[test]
fn lint_message_kind_defaults_to_lint_only_without_typecheck() {
assert_eq!(LintMessageKind::from_lint_config(None), LintMessageKind::LintOnly);
assert_eq!(
LintMessageKind::from_lint_config(Some(&json!({ "options": {} }))),
LintMessageKind::LintOnly
);
assert!(!lint_config_type_check_enabled(None));
assert!(!lint_config_type_check_enabled(Some(&json!({ "options": {} }))));
assert_eq!(LintMessageKind::from_flags(true, false), LintMessageKind::LintOnly);
}

#[test]
fn lint_message_kind_detects_typecheck_from_vite_config() {
let kind = LintMessageKind::from_lint_config(Some(&json!({
let config = json!({
"options": {
"typeAware": true,
"typeCheck": true
}
})));
});

assert!(lint_config_type_check_enabled(Some(&config)));

let kind = LintMessageKind::from_flags(true, true);
assert_eq!(kind, LintMessageKind::LintAndTypeCheck);
assert_eq!(kind.success_label(), "Found no warnings, lint errors, or type errors");
assert_eq!(kind.warning_heading(), "Lint or type warnings found");
assert_eq!(kind.issue_heading(), "Lint or type issues found");
}

#[test]
fn lint_message_kind_type_check_only_labels() {
let kind = LintMessageKind::from_flags(false, true);
assert_eq!(kind, LintMessageKind::TypeCheckOnly);
assert_eq!(kind.success_label(), "Found no type errors");
assert_eq!(kind.warning_heading(), "Type warnings found");
assert_eq!(kind.issue_heading(), "Type errors found");
}

#[test]
fn lint_config_type_check_enabled_rejects_non_bool_values() {
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeAware": true, "typeCheck": "true" }
}))));
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeAware": true, "typeCheck": 1 }
}))));
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeAware": true, "typeCheck": null }
}))));
}

#[test]
fn lint_config_type_check_requires_type_aware_prerequisite() {
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeCheck": true }
}))));
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeAware": false, "typeCheck": true }
}))));
assert!(!lint_config_type_check_enabled(Some(&json!({
"options": { "typeAware": true, "typeCheck": false }
}))));
}
}
77 changes: 52 additions & 25 deletions packages/cli/binding/src/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use vite_task::ExitStatus;

use self::analysis::{
LintMessageKind, analyze_fmt_check_output, analyze_lint_output, format_count, format_elapsed,
print_error_block, print_pass_line, print_stdout_block, print_summary_line,
lint_config_type_check_enabled, print_error_block, print_pass_line, print_stdout_block,
print_summary_line,
};
use crate::cli::{
CapturedCommandOutput, SubcommandResolver, SynthesizableSubcommand, resolve_and_capture_output,
Expand All @@ -28,14 +29,6 @@ pub(crate) async fn execute_check(
cwd: &AbsolutePathBuf,
cwd_arc: &Arc<AbsolutePath>,
) -> Result<ExitStatus, Error> {
if no_fmt && no_lint {
output::error("No checks enabled");
print_summary_line(
"`vp check` did not run because both `--no-fmt` and `--no-lint` were set",
);
return Ok(ExitStatus(1));
}

let mut status = ExitStatus::SUCCESS;
let has_paths = !paths.is_empty();
// In --fix mode with file paths (the lint-staged use case), implicitly suppress
Expand All @@ -46,6 +39,18 @@ pub(crate) async fn execute_check(
let mut deferred_lint_pass: Option<(String, String)> = None;
let resolved_vite_config = resolver.resolve_universal_vite_config().await?;

let type_check_enabled = lint_config_type_check_enabled(resolved_vite_config.lint.as_ref());
let lint_enabled = !no_lint;
let run_lint_phase = lint_enabled || type_check_enabled;

if no_fmt && !run_lint_phase {
output::error("No checks enabled");
print_summary_line(
"Enable `lint.options.typeCheck` in vite.config.ts to use `vp check --no-fmt --no-lint` for type-check only, or drop a flag to re-enable fmt/lint.",
);
return Ok(ExitStatus(1));
}

if !no_fmt {
let mut args = if fix { vec![] } else { vec!["--check".to_string()] };
if suppress_unmatched {
Expand Down Expand Up @@ -109,7 +114,7 @@ pub(crate) async fn execute_check(
}
}

if fix && no_lint && status == ExitStatus::SUCCESS {
if fix && !run_lint_phase && status == ExitStatus::SUCCESS {
print_pass_line(
"Formatting completed for checked files",
Some(&format!("({})", format_elapsed(fmt_start.elapsed()))),
Expand All @@ -127,11 +132,12 @@ pub(crate) async fn execute_check(
}
}

if !no_lint {
let lint_message_kind =
LintMessageKind::from_lint_config(resolved_vite_config.lint.as_ref());
if run_lint_phase {
let lint_message_kind = LintMessageKind::from_flags(lint_enabled, type_check_enabled);
let mut args = Vec::new();
if fix {
// oxlint cannot auto-fix type diagnostics, so `--fix` is dropped on the
// type-check-only path.
if fix && lint_enabled {
args.push("--fix".to_string());
}
// `vp check` parses oxlint's human-readable summary output to print
Expand All @@ -140,6 +146,9 @@ pub(crate) async fn execute_check(
// parser think linting never started. Force the default reporter here so the
// captured output is stable across local and CI environments.
args.push("--format=default".to_string());
if !lint_enabled && type_check_enabled {
args.push("--type-check-only".to_string());
}
if suppress_unmatched {
args.push("--no-error-on-unmatched-pattern".to_string());
}
Expand Down Expand Up @@ -207,13 +216,18 @@ pub(crate) async fn execute_check(
}
}
if status != ExitStatus::SUCCESS {
// Surface fmt `--fix` completion before bailing so users can see
// the working tree was mutated before the lint/type-check error.
if fix && !no_fmt {
flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass);
}
return Ok(status);
}
}

// Re-run fmt after lint --fix, since lint fixes can break formatting
// (e.g. the curly rule adding braces to if-statements)
if fix && !no_fmt && !no_lint {
// (e.g. the curly rule adding braces to if-statements).
if fix && !no_fmt && lint_enabled {
let mut args = Vec::new();
if suppress_unmatched {
args.push("--no-error-on-unmatched-pattern".to_string());
Expand Down Expand Up @@ -241,20 +255,33 @@ pub(crate) async fn execute_check(
);
return Ok(status);
}
if let Some(started) = fmt_fix_started {
print_pass_line(
"Formatting completed for checked files",
Some(&format!("({})", format_elapsed(started.elapsed()))),
);
}
if let Some((message, detail)) = deferred_lint_pass.take() {
print_pass_line(&message, Some(&detail));
}
flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass);
}

// Type-check-only mode skips the re-fmt block above, so flush deferred
// pass lines here.
if fix && !no_fmt && run_lint_phase && !lint_enabled {
flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass);
}

Ok(status)
}

fn flush_deferred_pass_lines(
fmt_fix_started: &mut Option<Instant>,
deferred_lint_pass: &mut Option<(String, String)>,
) {
if let Some(started) = fmt_fix_started.take() {
print_pass_line(
"Formatting completed for checked files",
Some(&format!("({})", format_elapsed(started.elapsed()))),
);
}
if let Some((message, detail)) = deferred_lint_pass.take() {
print_pass_line(&message, Some(&detail));
}
}

/// Combine stdout and stderr from a captured command output.
fn combine_output(captured: CapturedCommandOutput) -> (ExitStatus, String) {
let combined = if captured.stderr.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/binding/src/cli/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub enum SynthesizableSubcommand {
/// Skip format check
#[arg(long = "no-fmt")]
no_fmt: bool,
/// Skip lint check
/// Skip lint rules; type-check still runs when `lint.options.typeCheck` is true
#[arg(long = "no-lint")]
no_lint: bool,
/// Do not exit with error when pattern is unmatched
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/snap-tests-global/command-check-help/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Run format, lint, and type checks.
Options:
--fix Auto-fix format and lint issues
--no-fmt Skip format check
--no-lint Skip lint check
--no-lint Skip lint rules; type-check still runs when `lint.options.typeCheck` is true
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
-h, --help Print help

Expand All @@ -30,7 +30,7 @@ Run format, lint, and type checks.
Options:
--fix Auto-fix format and lint issues
--no-fmt Skip format check
--no-lint Skip lint check
--no-lint Skip lint rules; type-check still runs when `lint.options.typeCheck` is true
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
-h, --help Print help

Expand All @@ -52,7 +52,7 @@ Run format, lint, and type checks.
Options:
--fix Auto-fix format and lint issues
--no-fmt Skip format check
--no-lint Skip lint check
--no-lint Skip lint rules; type-check still runs when `lint.options.typeCheck` is true
--no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched
-h, --help Print help

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/snap-tests/check-all-skipped/snap.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[1]> vp check --no-fmt --no-lint
error: No checks enabled

`vp check` did not run because both `--no-fmt` and `--no-lint` were set
Enable `lint.options.typeCheck` in vite.config.ts to use `vp check --no-fmt --no-lint` for type-check only, or drop a flag to re-enable fmt/lint.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ error: Lint issues found
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 (<variable>ms, <variable> threads)
pass: Formatting completed for checked files (<variable>ms)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "check-fix-no-fmt-no-lint-typecheck",
"version": "0.0.0",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> vp check --fix --no-fmt --no-lint
pass: Found no type errors in 2 files (<variable>ms, <variable> threads)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const value: number = 42;
export { value };
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env": {
"VITE_DISABLE_AUTO_INSTALL": "1"
},
"commands": ["vp check --fix --no-fmt --no-lint"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
lint: {
options: {
typeAware: true,
typeCheck: true,
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "check-fix-no-lint-typecheck-fail",
"version": "0.0.0",
"private": true
}
Loading
Loading