Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions crates/wright-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ pub(crate) struct ReportArgs {
pub(crate) common: CommonArgs,
#[command(flatten)]
pub(crate) select: SelectArgs,
/// Load a local YAML rule file or directory (repeatable).
#[arg(long = "rule-file", value_name = "PATH")]
pub(crate) rule_file: Vec<PathBuf>,
}

/// Arguments of `analyze`: shared workflow options, finding selection, and
Expand Down Expand Up @@ -294,6 +297,9 @@ pub(crate) struct LookupArgs {
/// Override the first-party OPY provider executable.
#[arg(long, value_name = "PATH")]
pub(crate) opy_provider: Option<PathBuf>,
/// Load a local YAML rule file or directory (repeatable).
#[arg(long = "rule-file", value_name = "PATH")]
pub(crate) rule_file: Vec<PathBuf>,
/// Output format.
#[arg(short = 'f', long, value_enum, default_value_t = OutputFormatArg::Text)]
pub(crate) format: OutputFormatArg,
Expand Down
18 changes: 6 additions & 12 deletions crates/wright-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod mcp;
mod plain;
mod present;
mod serve;
mod session;
mod tooldefs;
mod update;
mod usage_hint;
Expand Down Expand Up @@ -176,6 +177,7 @@ fn run_workflow(command: Command) -> ExitCode {
Command::Check(args) => {
let mut config = config_from_common(&args.common);
config.selection = selection_from_args(&args.select);
config.lint_rule_paths = args.rule_file;
run_configured(
"check",
config,
Expand Down Expand Up @@ -360,10 +362,7 @@ pub(crate) fn run_configured<
) -> ExitCode {
let mut session = match wright_driver::CompilerSession::new(config) {
Ok(session) => session,
Err(diagnostic) => {
present::terminal::report_error(presentation, "USAGE", name, &diagnostic.message);
return ExitCode::from(exit::USAGE);
}
Err(diagnostic) => return session::refuse(presentation, name, &diagnostic),
};

ExitCode::from(run_command(&mut session, run, presentation, subject))
Expand Down Expand Up @@ -467,6 +466,7 @@ fn run_lookup(args: &cli::LookupArgs) -> ExitCode {
text: String::new(),
path: None,
},
lint_rule_paths: args.rule_file.clone(),
opy_provider: wright_driver::OpyProviderConfig {
executable: args.opy_provider.clone(),
..wright_driver::OpyProviderConfig::default()
Expand All @@ -475,17 +475,11 @@ fn run_lookup(args: &cli::LookupArgs) -> ExitCode {
};
let mut session = match wright_driver::CompilerSession::new(config) {
Ok(session) => session,
Err(diagnostic) => {
present::terminal::report_error(pres, "USAGE", "lookup", &diagnostic.message);
return ExitCode::from(exit::USAGE);
}
Err(diagnostic) => return session::refuse(pres, "lookup", &diagnostic),
};
let mut service = match wright_driver::service::ToolService::new(&mut session) {
Ok(service) => service,
Err(diagnostic) => {
present::terminal::report_error(pres, "USAGE", "lookup", &diagnostic.message);
return ExitCode::from(exit::USAGE);
}
Err(diagnostic) => return session::refuse(pres, "lookup", &diagnostic),
};
let request = wright_driver::service::ToolRequest::Lookup {
language: args.language.as_str().to_string(),
Expand Down
4 changes: 2 additions & 2 deletions crates/wright-cli/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@ pub(crate) fn run(args: ServeArgs) -> ExitCode {
Ok(session) => session,
Err(diagnostic) => {
eprintln!("wright: {}", diagnostic.message);
return ExitCode::from(2);
return ExitCode::from(diagnostic.class.exit_code());
}
};
let mut service = match ToolService::new(&mut session) {
Ok(service) => service,
Err(diagnostic) => {
eprintln!("wright: {}", diagnostic.message);
return ExitCode::from(2);
return ExitCode::from(diagnostic.class.exit_code());
}
};

Expand Down
16 changes: 16 additions & 0 deletions crates/wright-cli/src/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::process::ExitCode;

use wright_driver::Diagnostic;

use crate::present::{self, Presentation};

pub(crate) fn refuse(pres: Presentation, name: &str, diagnostic: &Diagnostic) -> ExitCode {
let code = diagnostic.class.exit_code();
present::terminal::report_error(
pres,
present::terminal::exit_word(code),
name,
&diagnostic.message,
);
ExitCode::from(code)
}
54 changes: 54 additions & 0 deletions crates/wright-cli/tests/session_exits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::path::{Path, PathBuf};
use std::process::Command;

fn wright() -> &'static str {
env!("CARGO_BIN_EXE_wright")
}

fn run(args: &[&str]) -> std::process::Output {
Command::new(wright())
.args(args)
.output()
.expect("wright runs")
}

fn temp_dir() -> PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let dir = std::env::temp_dir().join(format!(
"wright-session-exits-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).unwrap();
dir
}

#[test]
fn session_construction_exits_follow_the_diagnostic_class() {
let dir = temp_dir();
let missing = dir.join("missing.yaml");
let invalid = dir.join("invalid.yaml");
std::fs::write(&invalid, "id: [unclosed\n").unwrap();
for command in [&["check"][..], &["lookup", "wait"][..], &["serve"][..]] {
for (rule_file, code) in [(&missing, 2), (&invalid, 1)] {
let args: Vec<String> = command
.iter()
.map(|arg| arg.to_string())
.chain([
"--rule-file".to_string(),
rule_file.to_string_lossy().into_owned(),
])
.collect();
let output = run(&args.iter().map(String::as_str).collect::<Vec<_>>());
assert_eq!(
output.status.code(),
Some(code),
"{args:?}: status={} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
let _ = std::fs::remove_dir_all(Path::new(&dir));
}
3 changes: 3 additions & 0 deletions crates/wright-driver/src/lint_package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ pub(crate) fn activate_pinned(
let pin_error = |message: String| {
let error = LintPackageError::source(message);
Diagnostic::error(error.code(), Stage::Discovery, error.to_string())
.with_class(error.exit_class())
};
let store = packages.store();
for pin in &lint.packages {
Expand All @@ -505,6 +506,7 @@ pub(crate) fn activate_pinned(
Stage::Discovery,
format!("lint package pin '{}': {error}", pin.repo),
)
.with_class(error.exit_class())
})?;
if let Some(reference) = reference {
return Err(pin_error(format!(
Expand All @@ -529,6 +531,7 @@ pub(crate) fn activate_pinned(
stage,
format!("lint package '{source}' at {}: {error}", pin.sha),
)
.with_class(error.exit_class())
})?;
let rules_dir = installed.rules_dir();
let loaded = if source.is_first_party() {
Expand Down
10 changes: 7 additions & 3 deletions crates/wright-driver/src/session/registry.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use wright_analyzer::registry::LintRegistry;
use wright_analyzer::registry::{LintRegistry, RuleRegistryError};

use crate::config::SessionConfig;
use crate::diag::{Diagnostic, Stage};
use crate::diag::{Diagnostic, ExitClass, Stage};

pub(super) fn compose(config: &SessionConfig) -> Result<LintRegistry, Diagnostic> {
let mut registry = LintRegistry::default();
Expand All @@ -12,7 +12,11 @@ pub(super) fn compose(config: &SessionConfig) -> Result<LintRegistry, Diagnostic
crate::lint_package::activate_pinned(&config.lint, &config.lint_package, &mut registry)?;
for path in &config.lint_rule_paths {
registry.load_path(path).map_err(|error| {
Diagnostic::error("lint-rule-error", Stage::Analysis, error.to_string())
let class = match error {
RuleRegistryError::Io(..) => ExitClass::Usage,
_ => ExitClass::SourceError,
};
Diagnostic::classed_error("lint-rule-error", Stage::Analysis, class, error.to_string())
})?;
}
if let Err(message) = config.selection.validate(&registry) {
Expand Down
32 changes: 28 additions & 4 deletions crates/wright-driver/tests/check_failures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,6 @@ fn check_surfaces_every_failure_kind_with_its_code_and_exit_class() {
// The session machinery's own refusal kinds: construction
// diagnostics (observed as the session boundary's `Err`), the
// fixed-config refusal, and the `source-provider-*` load refusals.
// `lint-rule-error` is deliberately unpinned: its declared
// `SourceError` class disagrees with the CLI wrappers, which exit
// `USAGE` for every session-construction failure — the discrepancy
// awaits a separate owner decision.
let Err(diagnostic) = CompilerSession::new(SessionConfig {
selection: FindingSelection {
rule: Some("no-such-rule".to_string()),
Expand All @@ -472,6 +468,34 @@ fn check_surfaces_every_failure_kind_with_its_code_and_exit_class() {
assert_eq!(diagnostic.class, ExitClass::Usage);
assert_eq!(diagnostic.class.exit_code(), ExitClass::Usage.exit_code());

{
let dir = temp_dir("check-rule-file");
let Err(diagnostic) = CompilerSession::new(SessionConfig {
lint_rule_paths: vec![dir.join("missing.yaml")],
..SessionConfig::default()
}) else {
panic!("a missing --rule-file must refuse session construction");
};
assert_eq!(diagnostic.code, "lint-rule-error");
assert_eq!(diagnostic.class, ExitClass::Usage);
assert_eq!(diagnostic.class.exit_code(), ExitClass::Usage.exit_code());

let invalid = write_file(&dir, "invalid.yaml", "id: [unclosed\n");
let Err(diagnostic) = CompilerSession::new(SessionConfig {
lint_rule_paths: vec![invalid],
..SessionConfig::default()
}) else {
panic!("an invalid --rule-file must refuse session construction");
};
assert_eq!(diagnostic.code, "lint-rule-error");
assert_eq!(diagnostic.class, ExitClass::SourceError);
assert_eq!(
diagnostic.class.exit_code(),
ExitClass::SourceError.exit_code()
);
std::fs::remove_dir_all(&dir).expect("cleanup");
}

// `session-config-changed` — the fixed-config refusal.
{
let dir = temp_dir("check-config");
Expand Down
60 changes: 47 additions & 13 deletions crates/wright-driver/tests/lookup_failures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,26 @@ fn lookup_surfaces_every_failure_kind_with_its_code_and_exit_class() {
// The session machinery's own failure kinds at the service
// boundary: construction diagnostics keep their own code and class,
// and `session-config-changed` refuses `ToolService::new` itself.
// `lint-rule-error` is deliberately unpinned: its declared
// `SourceError` class disagrees with the CLI wrappers, which exit
// `USAGE` for every session-construction failure — the discrepancy
// awaits a separate owner decision.
let session_failures: &[(&str, ExitClass)] = &[
("invalid-selection", ExitClass::Usage),
("session-config-changed", ExitClass::SourceError),
let session_failures: &[(&str, &str, ExitClass)] = &[
("invalid-selection", "invalid-selection", ExitClass::Usage),
(
"session-config-changed",
"session-config-changed",
ExitClass::SourceError,
),
(
"lint-rule-error:missing",
"lint-rule-error",
ExitClass::Usage,
),
(
"lint-rule-error:invalid",
"lint-rule-error",
ExitClass::SourceError,
),
];
for (code, class) in session_failures {
let diagnostic = match *code {
for (name, code, class) in session_failures {
let diagnostic = match *name {
"session-config-changed" => {
let mut session = CompilerSession::new(SessionConfig {
input: InputSpec::Path(workshop_path()),
Expand All @@ -418,11 +428,35 @@ fn lookup_surfaces_every_failure_kind_with_its_code_and_exit_class() {
};
diagnostic
}
_ => unreachable!("{code} has no session-boundary trigger"),
"lint-rule-error:missing" => {
let dir = temp_dir("lookup-rule-file");
let Err(diagnostic) = CompilerSession::new(SessionConfig {
lint_rule_paths: vec![dir.join("missing.yaml")],
..SessionConfig::default()
}) else {
panic!("a missing --rule-file must refuse the session");
};
std::fs::remove_dir_all(&dir).expect("cleanup");
diagnostic
}
"lint-rule-error:invalid" => {
let dir = temp_dir("lookup-rule-file");
let path = dir.join("invalid.yaml");
std::fs::write(&path, "id: [unclosed\n").expect("write file");
let Err(diagnostic) = CompilerSession::new(SessionConfig {
lint_rule_paths: vec![path],
..SessionConfig::default()
}) else {
panic!("an invalid --rule-file must refuse the session");
};
std::fs::remove_dir_all(&dir).expect("cleanup");
diagnostic
}
_ => unreachable!("{name} has no session-boundary trigger"),
};
assert_eq!(diagnostic.code, *code);
assert_eq!(diagnostic.class, *class);
assert_eq!(diagnostic.class.exit_code(), class.exit_code());
assert_eq!(diagnostic.code, *code, "{name}");
assert_eq!(diagnostic.class, *class, "{name}");
assert_eq!(diagnostic.class.exit_code(), class.exit_code(), "{name}");
}
}

Expand Down
Loading