Skip to content
Open
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
10 changes: 2 additions & 8 deletions crates/wright-analyzer/src/canonical/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,9 @@ pub struct Finding {
pub value: Option<ValueId>,
pub evidence: EvidenceClass,
pub boundedness: Option<Boundedness>,
/// The mechanically determined correction for this finding, when one
/// exists and its safety preconditions hold (#556). The plan names the
/// spans to rewrite; the driver materializes it into a validated
/// source-edit transaction.
/// The correction plan for a finding whose safety preconditions hold.
pub fix: Option<LintFix>,
/// The repeated-expression facts behind a `repeated-value` finding
/// (#791): the expression, how often the loop scope evaluates it, and
/// which location `span` reports. Renderers consume these fields
/// instead of parsing `message`; `None` on every other finding.
/// The expression, evaluation count, and reported span of a repeated value.
pub repeated: Option<RepeatedValue>,
}

Expand Down
11 changes: 6 additions & 5 deletions crates/wright-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ UPDATE OPTIONS:

#[derive(Debug, Subcommand)]
pub(crate) enum Command {
/// Parse, lower, validate, and emit Workshop text.
/// Parse, lower, validate, lint, and emit Workshop text.
Compile(CompileArgs),
/// Reconstruct validated Workshop input as canonical OPY source; OSTW is
/// recognized but unavailable because DEL/OSTW provider support is not
/// currently shipped.
Convert(ConvertArgs),
/// Check frontend, project, semantic, and validation correctness.
/// Check frontend, project, semantic, validation, and lint diagnostics.
Check(ReportArgs),
/// Summarize Workshop cost, complexity hotspots, risk indicators, and
/// cross-cutting state.
Expand Down Expand Up @@ -136,6 +136,8 @@ pub(crate) enum Command {
pub(crate) struct CompileArgs {
#[command(flatten)]
pub(crate) common: CommonArgs,
#[command(flatten)]
pub(crate) policy: crate::serve::LintPolicyArgs,
/// Write the program to PATH instead of `<input stem>.ow`; `-` prints the
/// program to stdout and nothing else.
#[arg(short = 'o', long, value_name = "PATH")]
Expand Down Expand Up @@ -190,9 +192,8 @@ 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>,
#[command(flatten)]
pub(crate) policy: crate::serve::LintPolicyArgs,
}

/// Arguments of `analyze`: shared workflow options, finding selection, and
Expand Down
137 changes: 137 additions & 0 deletions crates/wright-cli/src/compile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::io::{IsTerminal, Write};
use std::process::ExitCode;

use wright_driver::result::exit;
use wright_driver::{CompileWrite, OutputDest, Overwrite};

use crate::cli::{self, OutputFormatArg};
use crate::{config_from_common, present, run_configured};

pub(crate) fn run(args: &cli::CompileArgs) -> ExitCode {
let to_stdout = args.output.as_deref().is_some_and(|p| p.as_os_str() == "-");
if to_stdout && args.common.format == OutputFormatArg::Json {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
"-o - prints the program to stdout and cannot be combined with --format json",
);
return ExitCode::from(exit::USAGE);
}
let dest = match &args.output {
_ if to_stdout => OutputDest::Stdout,
Some(path) => OutputDest::Path(path.clone()),
None => OutputDest::Default,
};
let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let copy = args.copy;
let force = args.force;
let mut compile_config = config_from_common(&args.common);
if let Err(message) = args.policy.apply_to(&mut compile_config) {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
&message,
);
return ExitCode::from(exit::USAGE);
}
compile_config.run = wright_driver::config::RunOptions {
no_cache: args.no_cache,
baseline: args.baseline.clone(),
save_baseline: args.save_baseline.clone(),
max_total: None,
};
run_configured(
"compile",
compile_config,
present::Presentation::from_common(&args.common),
None,
move |session: &mut wright_driver::CompilerSession| {
let mut confirm = |path: &std::path::Path| {
confirm_overwrite(
path,
&mut std::io::stdin().lock(),
&mut std::io::stderr().lock(),
)
};
let overwrite = if force {
Overwrite::Force
} else if interactive {
Overwrite::Ask(&mut confirm)
} else {
Overwrite::Refuse
};
let to = args.to.clone();
let mut envelope = session.compile_to(CompileWrite {
dest,
overwrite,
to,
});
let copied = envelope
.result
.output
.as_ref()
.filter(|_| copy && envelope.ok)
.map(|output| {
arboard::Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(&output.text))
});
if let Some(Err(error)) = copied {
envelope
.diagnostics
.push(wright_driver::Diagnostic::warning(
"clipboard-failed",
wright_driver::Stage::Emission,
format!("cannot copy to the clipboard: {error}"),
));
}
envelope
},
)
}

fn confirm_overwrite(
path: &std::path::Path,
input: &mut impl std::io::BufRead,
prompt: &mut impl Write,
) -> bool {
let _ = write!(
prompt,
"Overwrite {}? [y/N] ",
wright_driver::input::display_path(path)
);
let _ = prompt.flush();
let mut answer = String::new();
input.read_line(&mut answer).is_ok()
&& matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn overwrite_prompt_defaults_to_no_and_accepts_only_yes() {
for (answer, expected) in [
("y\n", true),
("Yes\n", true),
("\n", false),
("n\n", false),
("", false),
("maybe\n", false),
] {
let mut prompt = Vec::new();
let confirmed = confirm_overwrite(
std::path::Path::new("mode.ow"),
&mut answer.as_bytes(),
&mut prompt,
);
assert_eq!(confirmed, expected, "{answer:?}");
assert_eq!(
String::from_utf8(prompt).unwrap(),
"Overwrite mode.ow? [y/N] "
);
}
}
}
131 changes: 12 additions & 119 deletions crates/wright-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod agent;
mod agenttools;
mod cli;
mod compile;
mod completion;
mod essential;
mod lint;
Expand All @@ -13,8 +14,8 @@ mod tooldefs;
mod update;
mod usage_hint;

use std::io::Read;
use std::io::Write;
use std::io::{IsTerminal, Read};
use std::process::ExitCode;
use std::sync::Arc;

Expand All @@ -23,7 +24,6 @@ use clap::{CommandFactory, Parser, error::ErrorKind};
use wright_driver::config::{InputSpec, OutputFormat, SessionConfig, SourceKind};
use wright_driver::result::exit;
use wright_driver::source_provider::SourceBackend;
use wright_driver::{CompileWrite, OutputDest, Overwrite};

use crate::cli::{Cli, Command, CommonArgs, ConvertTargetArg, OutputFormatArg};

Expand Down Expand Up @@ -160,7 +160,7 @@ fn main() -> ExitCode {

fn run_workflow(command: Command) -> ExitCode {
match command {
Command::Compile(args) => run_compile(&args),
Command::Compile(args) => compile::run(&args),
Command::Convert(args) => {
let target = match args.target {
ConvertTargetArg::Opy => wright_driver::ConvertTarget::Opy,
Expand All @@ -177,7 +177,15 @@ 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;
if let Err(message) = args.policy.apply_to(&mut config) {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"check",
&message,
);
return ExitCode::from(exit::USAGE);
}
run_configured(
"check",
config,
Expand Down Expand Up @@ -368,97 +376,6 @@ pub(crate) fn run_configured<
ExitCode::from(run_command(&mut session, run, presentation, subject))
}

fn run_compile(args: &cli::CompileArgs) -> ExitCode {
let to_stdout = args.output.as_deref().is_some_and(|p| p.as_os_str() == "-");
if to_stdout && args.common.format == OutputFormatArg::Json {
present::terminal::report_error(
present::Presentation::from_common(&args.common),
"USAGE",
"compile",
"-o - prints the program to stdout and cannot be combined with --format json",
);
return ExitCode::from(exit::USAGE);
}
let dest = match &args.output {
_ if to_stdout => OutputDest::Stdout,
Some(path) => OutputDest::Path(path.clone()),
None => OutputDest::Default,
};
let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let copy = args.copy;
let force = args.force;
let mut compile_config = config_from_common(&args.common);
compile_config.run = wright_driver::config::RunOptions {
no_cache: args.no_cache,
baseline: args.baseline.clone(),
save_baseline: args.save_baseline.clone(),
max_total: None,
};
run_configured(
"compile",
compile_config,
present::Presentation::from_common(&args.common),
None,
move |session: &mut wright_driver::CompilerSession| {
let mut confirm = |path: &std::path::Path| {
confirm_overwrite(
path,
&mut std::io::stdin().lock(),
&mut std::io::stderr().lock(),
)
};
let overwrite = if force {
Overwrite::Force
} else if interactive {
Overwrite::Ask(&mut confirm)
} else {
Overwrite::Refuse
};
let to = args.to.clone();
let mut envelope = session.compile_to(CompileWrite {
dest,
overwrite,
to,
});
let copied = envelope
.result
.output
.as_ref()
.filter(|_| copy && envelope.ok)
.map(|output| {
arboard::Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(&output.text))
});
if let Some(Err(error)) = copied {
envelope
.diagnostics
.push(wright_driver::Diagnostic::warning(
"clipboard-failed",
wright_driver::Stage::Emission,
format!("cannot copy to the clipboard: {error}"),
));
}
envelope
},
)
}

fn confirm_overwrite(
path: &std::path::Path,
input: &mut impl std::io::BufRead,
prompt: &mut impl Write,
) -> bool {
let _ = write!(
prompt,
"Overwrite {}? [y/N] ",
wright_driver::input::display_path(path)
);
let _ = prompt.flush();
let mut answer = String::new();
input.read_line(&mut answer).is_ok()
&& matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}

fn run_lookup(args: &cli::LookupArgs) -> ExitCode {
let pres = present::Presentation::from_output(args.format, args.renderer, args.color);
let config = SessionConfig {
Expand Down Expand Up @@ -760,30 +677,6 @@ mod tests {
}
}

#[test]
fn overwrite_prompt_defaults_to_no_and_accepts_only_yes() {
for (answer, expected) in [
("y\n", true),
("Yes\n", true),
("\n", false),
("n\n", false),
("", false),
("maybe\n", false),
] {
let mut prompt = Vec::new();
let confirmed = confirm_overwrite(
std::path::Path::new("mode.ow"),
&mut answer.as_bytes(),
&mut prompt,
);
assert_eq!(confirmed, expected, "{answer:?}");
assert_eq!(
String::from_utf8(prompt).unwrap(),
"Overwrite mode.ow? [y/N] "
);
}
}

#[test]
fn every_opy_workflow_selects_the_provider_backend() {
let opy = common(Some("main.opy"), cli::SourceKindArg::Auto);
Expand Down
Loading
Loading