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
9 changes: 4 additions & 5 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use vite_task_graph::{
loader::UserConfigLoader,
};
use vite_task_plan::{
ExecutionPlan, TaskGraphLoader, TaskPlanErrorKind,
ExecutionPlan, TaskGraphLoader,
plan_request::{PlanRequest, ScriptCommand, SyntheticPlanRequest},
prepend_path_env,
};
Expand Down Expand Up @@ -484,16 +484,15 @@ impl<'a> Session<'a> {
let plan_request = match command.into_plan_request(&cwd) {
Ok(plan_request) => plan_request,
Err(crate::cli::CLITaskQueryError::MissingTaskSpecifier) => {
return Err(TaskPlanErrorKind::MissingTaskSpecifier.with_empty_call_stack());
return Err(vite_task_plan::Error::MissingTaskSpecifier);
}
Err(error) => {
return Err(TaskPlanErrorKind::ParsePlanRequestError {
return Err(vite_task_plan::Error::ParsePlanRequest {
error: error.into(),
program: Str::from("vp"),
args: Arc::default(),
cwd: Arc::clone(&cwd),
}
.with_empty_call_stack());
});
}
};
let plan = ExecutionPlan::plan(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> vp run run-typo-task
Error: Failed to plan execution, task call stack: task-select-test#run-typo-task
Error: Failed to plan tasks from `vp run nonexistent-xyz` in task task-select-test#run-typo-task

Caused by:
0: Failed to query tasks from task graph
Expand Down
58 changes: 2 additions & 56 deletions crates/vite_task_plan/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::{env::JoinPathsError, ffi::OsStr, fmt::Display, ops::Range, sync::Arc};
use std::{env::JoinPathsError, ffi::OsStr, ops::Range, sync::Arc};

use rustc_hash::FxHashMap;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task_graph::{IndexedTaskGraph, TaskNodeIndex, display::TaskDisplay};
use vite_task_graph::{IndexedTaskGraph, TaskNodeIndex};

use crate::{PlanRequestParser, path_env::prepend_path_env};

Expand Down Expand Up @@ -41,46 +41,6 @@ pub struct PlanContext<'a> {
indexed_task_graph: &'a IndexedTaskGraph,
}

/// A human-readable frame in the task call stack.
#[derive(Debug, Clone)]
pub struct TaskCallStackFrameDisplay {
pub task_display: TaskDisplay,

#[expect(dead_code, reason = "to be used in terminal error display")]
pub command_span: Range<usize>,
}

impl Display for TaskCallStackFrameDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// TODO: display command_span
write!(f, "{}", self.task_display)
}
}

/// A human-readable display of the task call stack.
#[derive(Default, Debug, Clone)]
pub struct TaskCallStackDisplay {
frames: Arc<[TaskCallStackFrameDisplay]>,
}

impl TaskCallStackDisplay {
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
}

impl Display for TaskCallStackDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, frame) in self.frames.iter().enumerate() {
if i > 0 {
write!(f, " -> ")?;
}
write!(f, "{frame}")?;
}
Ok(())
}
}

impl<'a> PlanContext<'a> {
pub fn new(
workspace_path: &'a Arc<AbsolutePath>,
Expand All @@ -104,20 +64,6 @@ impl<'a> PlanContext<'a> {
&self.envs
}

/// Get a human-readable display of the current task call stack.
pub fn display_call_stack(&self) -> TaskCallStackDisplay {
TaskCallStackDisplay {
frames: self
.task_call_stack
.iter()
.map(|(idx, span)| TaskCallStackFrameDisplay {
task_display: self.indexed_task_graph.display_task(*idx),
command_span: span.clone(),
})
.collect(),
}
}

/// Check if adding the given task node index would create a recursion in the call stack.
pub fn check_recursion(
&self,
Expand Down
114 changes: 29 additions & 85 deletions crates/vite_task_plan/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
reason = "Arc<Path> is used for non-UTF-8 path data in error types"
)]
use std::path::Path;
use std::{env::JoinPathsError, ffi::OsStr, fmt::Display, sync::Arc};
use std::{env::JoinPathsError, ffi::OsStr, sync::Arc};

use vite_path::{AbsolutePath, relative::InvalidPathDataError};
use vite_str::Str;
use vite_task_graph::display::TaskDisplay;

use crate::{
context::{PlanContext, TaskCallStackDisplay, TaskRecursionError},
envs::ResolveEnvError,
};
use crate::{context::TaskRecursionError, envs::ResolveEnvError};

#[derive(Debug, thiserror::Error)]
pub enum CdCommandError {
Expand All @@ -30,7 +28,7 @@ pub struct WhichError {
#[source]
pub error: which::Error,
}
impl Display for WhichError {
impl std::fmt::Display for WhichError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
Expand Down Expand Up @@ -66,7 +64,7 @@ pub enum PathType {
Program,
PackagePath,
}
impl Display for PathType {
impl std::fmt::Display for PathType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cwd => write!(f, "current working directory"),
Expand All @@ -84,18 +82,26 @@ pub struct PathFingerprintError {
pub kind: PathFingerprintErrorKind,
}

/// Errors that can occur when planning a specific execution from a task .
/// Errors that can occur when planning a specific execution from a task.
#[derive(Debug, thiserror::Error)]
pub enum TaskPlanErrorKind {
pub enum Error {
#[error("Failed to plan tasks from `{command}` in task {task_display}")]
NestPlan {
task_display: TaskDisplay,
command: Str,
#[source]
error: Box<Self>,
},

#[error("Failed to load task graph")]
TaskGraphLoadError(
TaskGraphLoad(
#[source]
#[from]
vite_task_graph::TaskGraphLoadError,
),

#[error("Failed to execute 'cd' command")]
CdCommandError(
CdCommand(
#[source]
#[from]
CdCommandError,
Expand All @@ -105,10 +111,10 @@ pub enum TaskPlanErrorKind {
ProgramNotFound(#[from] WhichError),

#[error(transparent)]
PathFingerprintError(#[from] PathFingerprintError),
PathFingerprint(#[from] PathFingerprintError),

#[error("Failed to query tasks from task graph")]
TaskQueryError(
TaskQuery(
#[source]
#[from]
vite_task_graph::query::TaskQueryError,
Expand All @@ -118,7 +124,7 @@ pub enum TaskPlanErrorKind {
TaskRecursionDetected(#[from] TaskRecursionError),

#[error("Invalid vite task command: {program} with args {args:?} under cwd {cwd:?}")]
ParsePlanRequestError {
ParsePlanRequest {
program: Str,
args: Arc<[Str]>,
cwd: Arc<AbsolutePath>,
Expand All @@ -127,100 +133,38 @@ pub enum TaskPlanErrorKind {
},

#[error("Failed to add node_modules/.bin to PATH environment variable")]
AddNodeModulesBinPathError {
AddNodeModulesBinPath {
#[source]
join_paths_error: JoinPathsError,
},

#[error("Failed to resolve environment variables")]
ResolveEnvError(#[source] ResolveEnvError),
ResolveEnv(#[source] ResolveEnvError),

#[error("No task specifier provided for 'run' command")]
MissingTaskSpecifier,
}

#[derive(Debug, thiserror::Error)]
pub struct Error {
task_call_stack: TaskCallStackDisplay,

#[source]
kind: TaskPlanErrorKind,
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to plan execution")?;
if !self.task_call_stack.is_empty() {
write!(f, ", task call stack: {}", self.task_call_stack)?;
}
Ok(())
}
}

impl TaskPlanErrorKind {
#[must_use]
pub fn with_empty_call_stack(self) -> Error {
Error { task_call_stack: TaskCallStackDisplay::default(), kind: self }
}
}

impl Error {
#[must_use]
pub const fn is_missing_task_specifier(&self) -> bool {
matches!(self.kind, TaskPlanErrorKind::MissingTaskSpecifier)
matches!(self, Self::MissingTaskSpecifier)
}

/// If this error represents a top-level task-not-found lookup failure,
/// returns the task name that the user typed.
///
/// Returns `None` if the error occurred in a nested task (non-empty call stack),
/// Returns `None` if the error occurred in a nested task (wrapped in `NestPlan`),
/// since nested task errors should propagate as-is rather than triggering
/// interactive task selection.
#[must_use]
pub fn task_not_found_name(&self) -> Option<&str> {
if !self.task_call_stack.is_empty() {
return None;
}
match &self.kind {
TaskPlanErrorKind::TaskQueryError(
vite_task_graph::query::TaskQueryError::SpecifierLookupError { specifier, .. },
) => Some(specifier.task_name.as_str()),
_ => None,
}
}
}

#[expect(
clippy::result_large_err,
reason = "Error wraps TaskPlanErrorKind with call stack for diagnostics"
)]
pub trait TaskPlanErrorKindResultExt {
type Ok;
/// Attach the current task call stack from the planning context to the error.
fn with_plan_context(self, context: &PlanContext<'_>) -> Result<Self::Ok, Error>;

/// Attach an empty task call stack to the error.
fn with_empty_call_stack(self) -> Result<Self::Ok, Error>;
}

impl<T> TaskPlanErrorKindResultExt for Result<T, TaskPlanErrorKind> {
type Ok = T;

/// Attach the current task call stack from the planning context to the error.
fn with_plan_context(self, context: &PlanContext<'_>) -> Result<T, Error> {
match self {
Ok(value) => Ok(value),
Err(kind) => {
let task_call_stack = context.display_call_stack();
Err(Error { task_call_stack, kind })
}
}
}

fn with_empty_call_stack(self) -> Result<T, Error> {
match self {
Ok(value) => Ok(value),
Err(kind) => Err(kind.with_empty_call_stack()),
Self::TaskQuery(vite_task_graph::query::TaskQueryError::SpecifierLookupError {
specifier,
..
}) => Some(specifier.task_name.as_str()),
_ => None,
}
}
}
17 changes: 5 additions & 12 deletions crates/vite_task_plan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ pub mod plan_request;
use std::{collections::BTreeMap, ffi::OsStr, fmt::Debug, sync::Arc};

use context::PlanContext;
use error::TaskPlanErrorKindResultExt;
pub use error::{Error, TaskPlanErrorKind};
pub use error::Error;
use execution_graph::ExecutionGraph;
use in_process::InProcessExecution;
pub use path_env::{get_path_env, prepend_path_env};
Expand Down Expand Up @@ -216,11 +215,7 @@ impl ExecutionPlan {
) -> Result<Self, Error> {
let root_node = match plan_request {
PlanRequest::Query(query_plan_request) => {
let indexed_task_graph = task_graph_loader
.load_task_graph()
.await
.map_err(TaskPlanErrorKind::TaskGraphLoadError)
.with_empty_call_stack()?;
let indexed_task_graph = task_graph_loader.load_task_graph().await?;

let context = PlanContext::new(
workspace_path,
Expand All @@ -240,8 +235,7 @@ impl ExecutionPlan {
None,
cwd,
ParentCacheConfig::None,
)
.with_empty_call_stack()?;
)?;
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution))
}
};
Expand All @@ -252,7 +246,7 @@ impl ExecutionPlan {
///
/// # Errors
/// Returns an error if the program is not found or path fingerprinting fails.
#[expect(clippy::result_large_err, reason = "Error contains task call stack for diagnostics")]
#[expect(clippy::result_large_err, reason = "Error is large for diagnostics")]
pub fn plan_synthetic(
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
Expand All @@ -267,8 +261,7 @@ impl ExecutionPlan {
Some(execution_cache_key),
cwd,
ParentCacheConfig::None,
)
.with_empty_call_stack()?;
)?;
Ok(Self { root_node: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution)) })
}
}
Loading
Loading