use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use cargo_metadata::Metadata;
use crate::cargo_health::{self, CargoHealthScan};
use crate::configuration::{self, WorkspaceConfiguration};
use crate::internal_error::InternalError;
use crate::policy::{PolicyPlan, Producer};
use crate::repo_hygiene::{self, RepoScan};
use crate::scan_target::{self, ResolvedScanTarget};
use crate::source_kernel::{self, SourceScan};
use crate::structure::{self, StructureScan};
mod baseline;
mod clippy;
mod messages;
#[cfg(test)]
mod tests;
pub(crate) use baseline::{BaselineExecution, execute as execute_baseline};
pub(crate) use clippy::ClippyExecution;
#[cfg(test)]
pub(crate) use clippy::arguments_for_rules as clippy_arguments_for_rules;
pub(crate) use messages::{
CapturedDiagnostic, CapturedMessage, CapturedSpan, CompilerMessageData, ScanExecution,
};
#[cfg(test)]
pub(crate) use messages::{CapturedDiagnosticCode, CapturedTarget};
#[derive(Debug)]
pub(crate) struct ExecutionResult {
pub(crate) manifest_path: Option<PathBuf>,
pub(crate) metadata: Option<Metadata>,
pub(crate) toolchain: Option<Toolchain>,
pub(crate) scan: ClippyExecution,
pub(crate) source: Option<SourceScan>,
pub(crate) structure: Option<StructureScan>,
pub(crate) cargo_health: Option<CargoHealthScan>,
pub(crate) repo: Option<RepoScan>,
pub(crate) error: Option<InternalError>,
}
#[derive(Debug)]
pub(crate) struct ProducerError<'a> {
pub(crate) stage: &'static str,
pub(crate) code: &'static str,
pub(crate) message: &'a str,
}
impl ExecutionResult {
fn failed(
manifest_path: Option<PathBuf>,
metadata: Option<Metadata>,
error: InternalError,
) -> Self {
Self {
manifest_path,
metadata,
toolchain: None,
scan: ClippyExecution::NotRun,
source: None,
structure: None,
cargo_health: None,
repo: None,
error: Some(error),
}
}
pub(crate) fn producer_errors(&self) -> impl Iterator<Item = ProducerError<'_>> {
let source = self.source.iter().flat_map(|scan| {
scan.errors
.iter()
.map(|error| ProducerError::new("source", error.code, &error.message))
});
let structure = self.structure.iter().flat_map(|scan| {
scan.errors
.iter()
.map(|error| ProducerError::new("structure", error.code, &error.message))
});
let dependencies = self.cargo_health.iter().flat_map(|scan| {
scan.errors
.iter()
.map(|error| ProducerError::new("dependencies", error.code, error.message))
});
let repo = self.repo.iter().flat_map(|scan| {
scan.errors
.iter()
.map(|error| ProducerError::new("repo", error.code, error.message))
});
source.chain(structure).chain(dependencies).chain(repo)
}
pub(crate) fn is_complete(&self) -> bool {
self.error.is_none() && self.scan.is_complete() && self.producer_errors().next().is_none()
}
}
impl<'a> ProducerError<'a> {
const fn new(stage: &'static str, code: &'static str, message: &'a str) -> Self {
Self {
stage,
code,
message,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Toolchain {
pub(crate) cargo: String,
pub(crate) rustc: String,
pub(crate) clippy: String,
}
#[derive(Debug)]
pub(crate) struct PreparedInspection {
target: ResolvedScanTarget,
pub(crate) configuration: WorkspaceConfiguration,
}
impl PreparedInspection {
pub(crate) fn workspace_root(&self) -> &Path {
self.target.workspace_root()
}
pub(crate) fn fail(self, error: InternalError) -> ExecutionResult {
ExecutionResult::failed(
Some(self.target.manifest_path),
Some(self.target.metadata),
error,
)
}
}
#[derive(Debug)]
struct Programs {
cargo: PathBuf,
rustc: PathBuf,
}
impl Default for Programs {
fn default() -> Self {
Self {
cargo: PathBuf::from("cargo"),
rustc: PathBuf::from("rustc"),
}
}
}
#[derive(Debug, Clone, Copy)]
struct ExecutionContext<'a> {
programs: &'a Programs,
plan: &'a PolicyPlan,
settings: &'a structure::StructureSettings,
environment: &'a CommandEnvironment,
}
#[derive(Debug, Clone, Default)]
struct CommandEnvironment {
rustup_toolchain: Option<OsString>,
}
impl CommandEnvironment {
fn apply(&self, command: &mut Command) {
if let Some(toolchain) = self.rustup_toolchain.as_ref() {
command.env("RUSTUP_TOOLCHAIN", toolchain);
}
}
}
pub(crate) fn prepare(path: &Path) -> Result<PreparedInspection, Box<ExecutionResult>> {
prepare_with(path, &Programs::default())
}
fn prepare_with(
path: &Path,
programs: &Programs,
) -> Result<PreparedInspection, Box<ExecutionResult>> {
let target = match scan_target::resolve(path, &programs.cargo) {
Ok(target) => target,
Err(failure) => {
return Err(Box::new(ExecutionResult::failed(
failure.manifest_path,
None,
failure.error,
)));
}
};
let configuration = match configuration::load(&target) {
Ok(configuration) => configuration,
Err(error) => {
return Err(Box::new(ExecutionResult::failed(
Some(target.manifest_path),
Some(target.metadata),
error,
)));
}
};
Ok(PreparedInspection {
target,
configuration,
})
}
pub(crate) fn execute(prepared: PreparedInspection, plan: &PolicyPlan) -> ExecutionResult {
execute_with_plan(prepared, &Programs::default(), plan)
}
fn execute_with_plan(
prepared: PreparedInspection,
programs: &Programs,
plan: &PolicyPlan,
) -> ExecutionResult {
execute_into(prepared, programs, plan, None)
}
fn execute_into(
prepared: PreparedInspection,
programs: &Programs,
plan: &PolicyPlan,
target_dir: Option<&Path>,
) -> ExecutionResult {
let environment = CommandEnvironment::default();
let toolchain = match resolve_toolchain(programs, prepared.workspace_root(), &environment) {
Ok(toolchain) => toolchain,
Err(error) => return prepared.fail(error),
};
let context = ExecutionContext {
programs,
plan,
settings: &prepared.configuration.structure,
environment: &environment,
};
context.run(prepared.target, toolchain, target_dir)
}
impl ExecutionContext<'_> {
fn run(
&self,
target: ResolvedScanTarget,
toolchain: Toolchain,
target_dir: Option<&Path>,
) -> ExecutionResult {
let ResolvedScanTarget {
manifest_path,
metadata,
} = target;
let source_rules = self.active(Producer::SourceKernel);
let structure_rules = self.active(Producer::Structure);
let dependency_truth = cargo_health::dependency_truth_required(self.plan);
let mut cargo_health = self
.active(Producer::CargoHealth)
.then(|| cargo_health::inspect(&metadata, self.plan));
let repo = self
.active(Producer::Repo)
.then(|| repo_hygiene::inspect(metadata.workspace_root.as_std_path(), self.plan));
let scan = if self.active(Producer::Clippy) {
match clippy::run(
self.programs,
metadata.workspace_root.as_std_path(),
self.plan,
target_dir,
self.environment,
) {
Ok(scan) => ClippyExecution::Finished(scan),
Err(error) => {
return ExecutionResult {
toolchain: Some(toolchain),
cargo_health,
repo,
..ExecutionResult::failed(Some(manifest_path), Some(metadata), error)
};
}
}
} else {
ClippyExecution::Disabled
};
let (source, structure) = if source_rules || structure_rules || dependency_truth {
let enumeration = source_kernel::enumerate(&metadata);
let scans = (
source_rules.then(|| source_kernel::inspect(&enumeration, self.plan)),
structure_rules
.then(|| structure::analyze(&metadata, &enumeration, self.plan, self.settings)),
);
if let Some(scan) = cargo_health.as_mut().filter(|_| dependency_truth) {
let references = source_kernel::references::collect(&enumeration);
cargo_health::inspect_dependency_truth(
&metadata,
&enumeration,
&references,
self.plan,
scan,
);
}
scans
} else {
(None, None)
};
ExecutionResult {
manifest_path: Some(manifest_path),
metadata: Some(metadata),
toolchain: Some(toolchain),
scan,
source,
structure,
cargo_health,
repo,
error: None,
}
}
fn active(&self, producer: Producer) -> bool {
self.plan.active_rules(producer).next().is_some()
}
}
fn resolve_toolchain(
programs: &Programs,
workspace_root: &Path,
environment: &CommandEnvironment,
) -> Result<Toolchain, InternalError> {
let cargo = tool_version(
&programs.cargo,
&["--version"],
workspace_root,
"cargo-unavailable",
"Cargo",
environment,
)?;
let rustc = tool_version(
&programs.rustc,
&["--version"],
workspace_root,
"rustc-unavailable",
"rustc",
environment,
)?;
let clippy = tool_version(
&programs.cargo,
&["clippy", "--version"],
workspace_root,
"clippy-unavailable",
"Clippy",
environment,
)?;
Ok(Toolchain {
cargo,
rustc,
clippy,
})
}
fn tool_version(
program: &Path,
arguments: &[&str],
working_directory: &Path,
code: &'static str,
label: &str,
environment: &CommandEnvironment,
) -> Result<String, InternalError> {
let output = version_command(program, arguments, working_directory, environment)
.output()
.map_err(|error| {
InternalError::new(
"execution",
code,
format!("{label} could not be started: {error}"),
)
})?;
if !output.status.success() {
return Err(InternalError::new(
"execution",
code,
format!("{label} exited with status {}", output.status),
));
}
let version = String::from_utf8(output.stdout).map_err(|error| {
InternalError::new(
"execution",
code,
format!("{label} returned non-UTF-8 version output: {error}"),
)
})?;
let version = version.trim();
if version.is_empty() {
return Err(InternalError::new(
"execution",
code,
format!("{label} returned an empty version"),
));
}
Ok(version.to_owned())
}
fn version_command(
program: &Path,
arguments: &[&str],
working_directory: &Path,
environment: &CommandEnvironment,
) -> Command {
let mut command = Command::new(program);
command
.args(arguments)
.current_dir(working_directory)
.stdin(Stdio::null())
.stderr(Stdio::null());
environment.apply(&mut command);
command
}