use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Instant;
use clap::ColorChoice;
use clap::Parser;
use mago_database::DatabaseReader;
use mago_database::file::FileType;
use mago_guard::settings::GuardMode;
use mago_prelude::Prelude;
use crate::commands::args::baseline_reporting::BaselineReportingArgs;
use crate::commands::args::substitution::SubstitutionArgs;
use crate::commands::outcome::CommandOutcome;
use crate::commands::stdin_input;
use crate::config::Configuration;
use crate::error::Error;
use crate::utils::create_orchestrator;
use crate::utils::drop_in_background;
#[derive(Parser, Debug, Default)]
#[command(name = "guard")]
pub struct GuardCommand {
#[arg()]
pub path: Vec<PathBuf>,
#[arg(long, default_value_t = false)]
pub no_stubs: bool,
#[arg(long, conflicts_with = "perimeter")]
pub structural: bool,
#[arg(long, conflicts_with = "structural")]
pub perimeter: bool,
#[arg(long, default_value_t = false)]
pub stdin_input: bool,
#[clap(flatten)]
pub baseline_reporting: BaselineReportingArgs,
#[clap(flatten)]
pub substitution: SubstitutionArgs,
}
impl GuardCommand {
pub fn execute(self, mut configuration: Configuration, color_choice: ColorChoice) -> Result<CommandOutcome, Error> {
let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
let command_start = trace_enabled.then(Instant::now);
let prelude_start = trace_enabled.then(Instant::now);
let Prelude { database, metadata, .. } =
if self.no_stubs { Prelude::default() } else { crate::prelude::load() };
let prelude_duration = prelude_start.map(|s| s.elapsed());
let cli_mode = if self.structural {
Some(GuardMode::Structural)
} else if self.perimeter {
Some(GuardMode::Perimeter)
} else {
None
};
if let Some(mode) = cli_mode {
let config_mode = configuration.guard.settings.mode;
if config_mode != GuardMode::Default && config_mode != mode {
tracing::info!(
"Overriding guard mode from configuration `{}` with CLI flag `{}`",
config_mode.as_str(),
mode.as_str()
);
} else if config_mode == mode {
tracing::warn!(
"CLI flag --{} is redundant: same mode already set in configuration",
if mode == GuardMode::Structural { "structural" } else { "perimeter" }
);
}
configuration.guard.settings.mode = mode;
}
let editor_url = configuration.editor_url.take();
let orchestrator_init_start = trace_enabled.then(Instant::now);
let substitutions = self.substitution.resolve()?;
let substitution_excludes: Vec<String> =
substitutions.iter().map(|s| s.original.to_string_lossy().into_owned()).collect();
let mut orchestrator = create_orchestrator(&configuration, color_choice, false, true, false);
orchestrator.add_exclude_patterns(configuration.guard.excludes.iter());
orchestrator.add_exclude_patterns(substitution_excludes.iter());
for substitution in &substitutions {
orchestrator.config.paths.push(substitution.temporary.to_string_lossy().into_owned());
}
let stdin_override = stdin_input::resolve_stdin_override(
self.stdin_input,
&self.path,
&configuration.source.workspace,
&mut orchestrator,
)?;
if !self.stdin_input && !self.path.is_empty() {
stdin_input::set_source_paths_from_paths(&mut orchestrator, &self.path);
}
let orchestrator_init_duration = orchestrator_init_start.map(|s| s.elapsed());
let load_database_start = trace_enabled.then(Instant::now);
let include_externals = !self.path.is_empty()
|| self.stdin_input
|| !substitutions.is_empty()
|| !orchestrator.config.patches.is_empty()
|| !has_disjoint_includes(
&configuration.source.workspace,
&orchestrator.config.paths,
&orchestrator.config.includes,
);
let mut database = orchestrator.load_database(
&configuration.source.workspace,
include_externals,
Some(database),
stdin_override,
)?;
let load_database_duration = load_database_start.map(|s| s.elapsed());
if !database.files().any(|f| f.file_type == FileType::Host) {
tracing::warn!("No files found to check with guard.");
return Ok(ExitCode::SUCCESS.into());
}
let guard_run_start = trace_enabled.then(Instant::now);
let service = orchestrator.get_guard_service(database.read_only(), metadata);
let result = service.run()?;
let guard_run_duration = guard_run_start.map(|s| s.elapsed());
if result.missing_perimeter_configuration {
tracing::warn!("Perimeter guard checks were skipped due to missing configuration.");
tracing::warn!("Please define perimeter rules in your mago.toml to enable these checks.");
}
if result.missing_structural_configuration {
tracing::warn!("Structural guard checks were skipped based on the current configuration.");
tracing::warn!("Please review your mago.toml guard settings to enable structural checks.");
}
let report_start = trace_enabled.then(Instant::now);
let baseline = configuration.guard.baseline.as_deref();
let baseline_variant = configuration.guard.baseline_variant;
let processor = self.baseline_reporting.get_processor(
color_choice,
baseline,
baseline_variant,
editor_url,
configuration.guard.minimum_fail_level,
!self.path.is_empty() || self.stdin_input,
);
let (exit_code, changed_file_ids) = processor.process_issues(&orchestrator, &mut database, result.issues)?;
let outcome = CommandOutcome::with_changes(exit_code, &database, changed_file_ids)?;
let report_duration = report_start.map(|s| s.elapsed());
let drop_database_start = trace_enabled.then(Instant::now);
drop_in_background(database.into_static());
let drop_database_duration = drop_database_start.map(|s| s.elapsed());
let drop_orchestrator_start = trace_enabled.then(Instant::now);
drop(orchestrator);
let drop_orchestrator_duration = drop_orchestrator_start.map(|s| s.elapsed());
if let Some(start) = command_start {
tracing::trace!("Prelude decoded in {:?}.", prelude_duration.unwrap_or_default());
tracing::trace!("Orchestrator initialized in {:?}.", orchestrator_init_duration.unwrap_or_default());
tracing::trace!("Database loaded in {:?}.", load_database_duration.unwrap_or_default());
tracing::trace!("Guard service ran in {:?}.", guard_run_duration.unwrap_or_default());
tracing::trace!("Issues filtered and reported in {:?}.", report_duration.unwrap_or_default());
tracing::trace!("Database dropped in {:?}.", drop_database_duration.unwrap_or_default());
tracing::trace!("Orchestrator dropped in {:?}.", drop_orchestrator_duration.unwrap_or_default());
tracing::trace!("Guard command finished in {:?}.", start.elapsed());
}
Ok(outcome)
}
}
fn has_disjoint_includes(workspace: &Path, paths: &[String], includes: &[String]) -> bool {
if includes.is_empty() {
return true;
}
if paths.is_empty() || paths.iter().chain(includes).any(|path| path.contains(['*', '?', '[', '{'])) {
return false;
}
let resolve =
|roots: &[String]| roots.iter().map(|root| workspace.join(root).canonicalize()).collect::<Result<Vec<_>, _>>();
let (Ok(paths), Ok(includes)) = (resolve(paths), resolve(includes)) else {
return false;
};
paths.iter().all(|path| includes.iter().all(|include| !path.starts_with(include) && !include.starts_with(path)))
}