Skip to content

Commit 8f7626a

Browse files
authored
refactor(cli): simplify abstractions (#574)
this PR refactors the `cli` crate. it currently has a bunch of issues that stem from its original implementation in biome, where file/stdin execution is the main use case. - a `CommandRunner` trait that tries to abstract a few common code blocks away. - a `Dummy` traversal mode which essentially means "do not traverse". - reports are tightly coupled to execution. we also need to report output of commands that do not traverse (e.g. `dblint`) - `execute_mode` does everything: stdin handling + traversal + reporting + exit codes. it cannot be re-used by commands that do not traverse all of these issues cause my brain to have a hard time understanding what is going on. This PR refactors the crate to gear it more towards our use-case. I spent a few cycles designing the refactor, and at least for me it makes much more sense now. I also added a dummy `dblint` command that will be implemented in follow-ups. right now, it prints an empty report. Below is a richer summary of the issues described above. ## Current Problems ### 1. CommandRunner Trait Forces All Commands Through Same Pipeline ```rust pub(crate) trait CommandRunner: Sized { fn run(&mut self, session: CliSession, cli_options: &CliOptions) -> Result<()> { // Forces: config loading → VCS setup → file traversal → reporting let (execution, paths) = self.configure_workspace(fs, console, workspace, cli_options)?; execute_mode(execution, session, cli_options, paths) } } ``` **Issues:** - Only `Check` command uses this trait - Commands like `version`, `clean`, `init` are ad-hoc functions - No way for commands to skip parts they don't need (e.g., dblint needs config but not traversal) - Forces unnecessary complexity on simple commands ### 2. TraversalMode::Dummy Exists For Wrong Reasons ```rust pub enum TraversalMode { Dummy, // Only exists because commands are forced through traversal Check { stdin: Option<Stdin>, vcs_targeted: VcsTargeted }, } ``` Commands that don't need traversal shouldn't have a "dummy" mode - they just shouldn't traverse. ### 3. Execution Struct Conflates Concerns ```rust pub struct Execution { report_mode: ReportMode, // How to report (Terminal/GitHub/GitLab) traversal_mode: TraversalMode, // How to process files max_diagnostics: u32, } ``` **Problem:** Bundles processing config with reporting config. Commands that don't traverse (like `dblint`) still need reporting but don't need traversal config. ### 4. execute_mode() is Monolithic ```rust pub fn execute_mode(execution: Execution, session: CliSession, ...) -> Result<()> { // Does: stdin handling + traversal + reporting + exit codes // Can't be reused by commands that don't traverse } ``` ### 5. Scattered and Inconsistent Structure - `commands/` has mix of trait impls and functions - `execute/` has traversal logic tightly coupled to reporting - No clear separation of workspace setup, execution, and reporting - Commands repeat boilerplate for config loading, setup, reporting
1 parent 02af023 commit 8f7626a

File tree

19 files changed

+859
-1066
lines changed

19 files changed

+859
-1066
lines changed
Lines changed: 119 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,138 @@
11
use crate::cli_options::CliOptions;
2-
use crate::{CliDiagnostic, Execution, TraversalMode};
3-
use biome_deserialize::Merge;
2+
use crate::commands::get_files_to_process_with_cli_options;
3+
use crate::execute::{StdinPayload, run_files, run_stdin};
4+
use crate::reporter::Report;
5+
use crate::{CliDiagnostic, CliSession, VcsIntegration};
6+
use crate::{ExecutionConfig, ExecutionMode, VcsTargeting};
47
use pgt_configuration::PartialConfiguration;
58
use pgt_console::Console;
9+
use pgt_diagnostics::category;
610
use pgt_fs::FileSystem;
7-
use pgt_workspace::{DynRef, Workspace, WorkspaceError, configuration::LoadedConfiguration};
11+
use pgt_workspace::DynRef;
812
use std::ffi::OsString;
913

10-
use super::{CommandRunner, get_files_to_process_with_cli_options};
14+
pub struct CheckArgs {
15+
pub configuration: Option<PartialConfiguration>,
16+
pub paths: Vec<OsString>,
17+
pub stdin_file_path: Option<String>,
18+
pub staged: bool,
19+
pub changed: bool,
20+
pub since: Option<String>,
21+
}
22+
23+
pub fn check(
24+
mut session: CliSession,
25+
cli_options: &CliOptions,
26+
args: CheckArgs,
27+
) -> Result<(), CliDiagnostic> {
28+
validate_args(&args)?;
29+
30+
let configuration = session.prepare_with_config(cli_options, args.configuration.clone())?;
31+
session.setup_workspace(configuration.clone(), VcsIntegration::Enabled)?;
32+
33+
let paths = resolve_paths(session.fs(), &configuration, &args)?;
34+
35+
let vcs = VcsTargeting {
36+
staged: args.staged,
37+
changed: args.changed,
38+
};
39+
40+
let max_diagnostics = if cli_options.reporter.is_default() {
41+
cli_options.max_diagnostics.into()
42+
} else {
43+
u32::MAX
44+
};
45+
46+
let mode = ExecutionMode::Check { vcs };
47+
let execution = ExecutionConfig::new(mode, max_diagnostics);
1148

12-
pub(crate) struct CheckCommandPayload {
13-
pub(crate) configuration: Option<PartialConfiguration>,
14-
pub(crate) paths: Vec<OsString>,
15-
pub(crate) stdin_file_path: Option<String>,
16-
pub(crate) staged: bool,
17-
pub(crate) changed: bool,
18-
pub(crate) since: Option<String>,
49+
if let Some(stdin_path) = args.stdin_file_path.as_deref() {
50+
let payload = read_stdin_payload(stdin_path, session.console())?;
51+
run_stdin(&mut session, &execution, payload)
52+
} else {
53+
let report: Report = run_files(&mut session, &execution, paths)?;
54+
55+
let exit_result = enforce_exit_codes(cli_options, &report);
56+
session.report("check", cli_options, &report)?;
57+
exit_result
58+
}
1959
}
2060

21-
impl CommandRunner for CheckCommandPayload {
22-
const COMMAND_NAME: &'static str = "check";
23-
24-
fn merge_configuration(
25-
&mut self,
26-
loaded_configuration: LoadedConfiguration,
27-
_fs: &DynRef<'_, dyn FileSystem>,
28-
_console: &mut dyn Console,
29-
) -> Result<PartialConfiguration, WorkspaceError> {
30-
let LoadedConfiguration {
31-
configuration: mut fs_configuration,
32-
..
33-
} = loaded_configuration;
34-
35-
if let Some(configuration) = self.configuration.clone() {
36-
// overwrite fs config with cli args
37-
fs_configuration.merge_with(configuration);
61+
fn resolve_paths(
62+
fs: &DynRef<'_, dyn FileSystem>,
63+
configuration: &PartialConfiguration,
64+
args: &CheckArgs,
65+
) -> Result<Vec<OsString>, CliDiagnostic> {
66+
let mut paths = get_files_to_process_with_cli_options(
67+
args.since.as_deref(),
68+
args.changed,
69+
args.staged,
70+
fs,
71+
configuration,
72+
)?
73+
.unwrap_or_else(|| args.paths.clone());
74+
75+
if paths.is_empty() && args.stdin_file_path.is_none() {
76+
if let Some(current_dir) = fs.working_directory() {
77+
paths.push(current_dir.into_os_string());
3878
}
79+
}
3980

40-
Ok(fs_configuration)
81+
Ok(paths)
82+
}
83+
84+
fn read_stdin_payload(
85+
path: &str,
86+
console: &mut dyn Console,
87+
) -> Result<StdinPayload, CliDiagnostic> {
88+
let input_code = console.read();
89+
if let Some(input_code) = input_code {
90+
Ok(StdinPayload {
91+
path: path.into(),
92+
content: input_code,
93+
})
94+
} else {
95+
Err(CliDiagnostic::missing_argument("stdin", "check"))
4196
}
97+
}
98+
99+
fn enforce_exit_codes(cli_options: &CliOptions, payload: &Report) -> Result<(), CliDiagnostic> {
100+
let traversal = payload.traversal.as_ref();
101+
let processed = traversal.map_or(0, |t| t.changed + t.unchanged);
102+
let skipped = traversal.map_or(0, |t| t.skipped);
42103

43-
fn get_files_to_process(
44-
&self,
45-
fs: &DynRef<'_, dyn FileSystem>,
46-
configuration: &PartialConfiguration,
47-
) -> Result<Vec<OsString>, CliDiagnostic> {
48-
let paths = get_files_to_process_with_cli_options(
49-
self.since.as_deref(),
50-
self.changed,
51-
self.staged,
52-
fs,
53-
configuration,
54-
)?
55-
.unwrap_or(self.paths.clone());
56-
57-
Ok(paths)
104+
if processed.saturating_sub(skipped) == 0 && !cli_options.no_errors_on_unmatched {
105+
return Err(CliDiagnostic::no_files_processed());
58106
}
59107

60-
fn get_stdin_file_path(&self) -> Option<&str> {
61-
self.stdin_file_path.as_deref()
108+
let warnings = payload.warnings;
109+
let errors = payload.errors;
110+
let category = category!("check");
111+
112+
if errors > 0 {
113+
return Err(CliDiagnostic::check_error(category));
62114
}
63115

64-
fn get_execution(
65-
&self,
66-
cli_options: &CliOptions,
67-
console: &mut dyn Console,
68-
_workspace: &dyn Workspace,
69-
) -> Result<Execution, CliDiagnostic> {
70-
Ok(Execution::new(TraversalMode::Check {
71-
stdin: self.get_stdin(console)?,
72-
vcs_targeted: (self.staged, self.changed).into(),
73-
})
74-
.set_report(cli_options))
116+
if warnings > 0 && cli_options.error_on_warnings {
117+
return Err(CliDiagnostic::check_warnings(category));
75118
}
119+
120+
Ok(())
121+
}
122+
123+
fn validate_args(args: &CheckArgs) -> Result<(), CliDiagnostic> {
124+
if args.since.is_some() {
125+
if !args.changed {
126+
return Err(CliDiagnostic::incompatible_arguments("since", "changed"));
127+
}
128+
if args.staged {
129+
return Err(CliDiagnostic::incompatible_arguments("since", "staged"));
130+
}
131+
}
132+
133+
if args.changed && args.staged {
134+
return Err(CliDiagnostic::incompatible_arguments("changed", "staged"));
135+
}
136+
137+
Ok(())
76138
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use crate::cli_options::CliOptions;
2+
use crate::reporter::Report;
3+
use crate::{CliDiagnostic, CliSession, VcsIntegration};
4+
use pgt_configuration::PartialConfiguration;
5+
6+
pub fn dblint(
7+
mut session: CliSession,
8+
cli_options: &CliOptions,
9+
cli_configuration: Option<PartialConfiguration>,
10+
) -> Result<(), CliDiagnostic> {
11+
let configuration = session.prepare_with_config(cli_options, cli_configuration)?;
12+
session.setup_workspace(configuration, VcsIntegration::Disabled)?;
13+
14+
// TODO: Implement actual dblint logic here
15+
let report = Report::new(vec![], std::time::Duration::new(0, 0), 0, None);
16+
17+
session.report("dblint", cli_options, &report)
18+
}

0 commit comments

Comments
 (0)