use crate::types::TaskRunner;
pub(crate) fn detect_target(name: &str, command: &str) -> Option<TaskRunner> {
if crate::tool::turbo::is_self_passthrough(name, command) {
return Some(TaskRunner::Turbo);
}
for (runner, binary, run_sub) in CANDIDATES {
if simple_passthrough(name, command, binary, *run_sub) {
return Some(*runner);
}
}
None
}
const CANDIDATES: &[(TaskRunner, &str, Option<&str>)] = &[
(TaskRunner::Just, "just", None),
(TaskRunner::Make, "make", None),
(TaskRunner::GoTask, "task", None),
(TaskRunner::Nx, "nx", Some("run")),
(TaskRunner::Bacon, "bacon", None),
(TaskRunner::Mise, "mise", Some("run")),
];
fn simple_passthrough(
name: &str,
command: &str,
binary: &str,
run_subcommand: Option<&str>,
) -> bool {
if command.contains('\n') || command.contains('\r') {
return false;
}
let mut tokens = command.split_whitespace();
if tokens.next() != Some(binary) {
return false;
}
if let Some(sub) = run_subcommand
&& tokens.next() != Some(sub)
{
return false;
}
if tokens.next() != Some(name) {
return false;
}
tokens.all(|token| token.starts_with('-') && !is_shell_active(token))
}
fn is_shell_active(token: &str) -> bool {
if token.contains('$') || token.contains('`') || token.contains('%') {
return true;
}
if token
.chars()
.any(|c| matches!(c, '>' | '<' | '&' | '|' | ';'))
{
return true;
}
if token
.chars()
.any(|c| matches!(c, '*' | '?' | '[' | ']' | '{' | '}'))
{
return true;
}
matches!(token, "!" | "(" | ")")
}
#[cfg(test)]
mod tests {
use super::detect_target;
use crate::types::TaskRunner;
#[test]
fn detects_just_passthrough() {
assert_eq!(detect_target("build", "just build"), Some(TaskRunner::Just));
}
#[test]
fn detects_make_passthrough() {
assert_eq!(detect_target("test", "make test"), Some(TaskRunner::Make));
}
#[test]
fn detects_go_task_passthrough() {
assert_eq!(detect_target("lint", "task lint"), Some(TaskRunner::GoTask));
}
#[test]
fn detects_nx_passthrough_with_run_subcommand() {
assert_eq!(detect_target("build", "nx run build"), Some(TaskRunner::Nx));
}
#[test]
fn detects_bacon_passthrough() {
assert_eq!(
detect_target("check", "bacon check"),
Some(TaskRunner::Bacon)
);
}
#[test]
fn detects_mise_passthrough_with_run_subcommand() {
assert_eq!(detect_target("ci", "mise run ci"), Some(TaskRunner::Mise));
}
#[test]
fn rejects_when_target_name_mismatches() {
assert!(detect_target("dev", "just build").is_none());
}
#[test]
fn rejects_when_script_body_starts_with_other_binary() {
assert!(detect_target("build", "vite build").is_none());
}
#[test]
fn rejects_when_nx_run_subcommand_missing() {
assert!(detect_target("build", "nx build").is_none());
}
#[test]
fn rejects_when_tail_contains_pipe() {
assert!(detect_target("test", "just test | tee log").is_none());
}
#[test]
fn rejects_when_tail_contains_var_expansion() {
assert!(detect_target("test", "just test $EXTRA_ARGS").is_none());
}
#[test]
fn rejects_when_tail_contains_redirect() {
assert!(detect_target("test", "just test > out.log").is_none());
}
#[test]
fn rejects_when_tail_contains_command_substitution() {
assert!(detect_target("test", "just test $(echo)").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_logical_and() {
assert!(detect_target("test", "just test --watch&&echo done").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_logical_or() {
assert!(detect_target("test", "just test --watch||fallback").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_pipe() {
assert!(detect_target("test", "just test --report|tee").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_semicolon() {
assert!(detect_target("test", "just test foo;echo done").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_redirect() {
assert!(detect_target("test", "just test arg>out.log").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_input_redirect() {
assert!(detect_target("test", "just test arg<input.txt").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_fd_redirect() {
assert!(detect_target("test", "just test arg2>&1").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_background() {
assert!(detect_target("test", "just test arg&").is_none());
}
#[test]
fn rejects_when_body_contains_newline() {
assert!(detect_target("build", "just build\necho owned").is_none());
}
#[test]
fn rejects_when_body_contains_carriage_return() {
assert!(detect_target("build", "just build\r\necho owned").is_none());
}
#[test]
fn rejects_when_body_is_multiline_block() {
let body = "just build\nif [ $? -ne 0 ]; then\n exit 1\nfi";
assert!(detect_target("build", body).is_none());
}
#[test]
fn rejects_when_tail_contains_glob_star() {
assert!(detect_target("build", "just build src/*.js").is_none());
}
#[test]
fn rejects_when_tail_contains_glob_question_mark() {
assert!(detect_target("build", "just build file?.txt").is_none());
}
#[test]
fn rejects_when_tail_contains_character_class_glob() {
assert!(detect_target("build", "just build file[12].txt").is_none());
}
#[test]
fn rejects_when_tail_contains_brace_expansion() {
assert!(detect_target("build", "just build foo{1,2}").is_none());
}
#[test]
fn rejects_when_tail_contains_glued_brace_expansion() {
assert!(detect_target("build", "just build --filter=name{a,b}").is_none());
}
#[test]
fn rejects_when_tail_contains_windows_env_var() {
assert!(detect_target("build", "just build %EXTRA_ARGS%").is_none());
}
#[test]
fn rejects_when_tail_contains_extra_make_target() {
assert!(detect_target("build", "make build clean").is_none());
}
#[test]
fn rejects_when_tail_contains_extra_just_positional() {
assert!(detect_target("build", "just build release").is_none());
}
#[test]
fn rejects_when_tail_contains_extra_nx_positional() {
assert!(detect_target("build", "nx run build extra").is_none());
}
#[test]
fn accepts_when_tail_is_flag_with_equals_value() {
assert_eq!(
detect_target("test", "just test --reporter=verbose"),
Some(TaskRunner::Just),
);
}
#[test]
fn accepts_when_tail_contains_dash_dash_separator() {
assert_eq!(
detect_target("test", "just test -- --watch"),
Some(TaskRunner::Just),
);
}
#[test]
fn accepts_when_tail_is_plain_flags_only() {
assert_eq!(
detect_target("test", "just test --watch"),
Some(TaskRunner::Just)
);
}
#[test]
fn turbo_passthrough_still_routes_to_turbo_runner() {
assert_eq!(
detect_target("build", "turbo run build"),
Some(TaskRunner::Turbo)
);
assert_eq!(
detect_target("build", "turbo build"),
Some(TaskRunner::Turbo)
);
}
}