standarbuild-detect 0.2.0

Detect project kind (Rust, Node, Bun, Deno, Python, Lua, C/C++) and scan polyglot monorepo workspaces
Documentation
//! Built-in detectors for the project kinds the library knows about. Each
//! type is `pub` and `Default`-constructible so callers can pick and choose
//! (`registry.add(builtin::RustDetector)`), tweak the registry (remove +
//! add a stricter version), or use [`register_all`] to install the whole
//! lot.

use std::path::Path;

use crate::detector::{DetectMatch, Detector, DetectorRegistry};
use crate::kind::KindId;

fn signal_if_exists(dir: &Path, name: &str, out: &mut Vec<String>) -> bool {
	if dir.join(name).exists() {
		out.push(name.to_string());
		true
	} else {
		false
	}
}

fn any_exists(dir: &Path, names: &[&str], out: &mut Vec<String>) -> bool {
	let mut found = false;
	for n in names {
		if signal_if_exists(dir, n, out) {
			found = true;
		}
	}
	found
}

fn has_extension(dir: &Path, ext: &str) -> bool {
	let Ok(read) = std::fs::read_dir(dir) else {
		return false;
	};
	for entry in read.flatten() {
		if entry.path().extension().map(|e| e == ext).unwrap_or(false) {
			return true;
		}
	}
	false
}

// ---- Rust ------------------------------------------------------------------

#[derive(Default)]
pub struct RustDetector;

impl Detector for RustDetector {
	fn kind(&self) -> KindId { KindId::RUST }
	fn priority(&self) -> i32 { 100 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if signal_if_exists(dir, "Cargo.toml", &mut signals) {
			Some(DetectMatch { kind: KindId::RUST, signals })
		} else {
			None
		}
	}
}

// ---- Bun -------------------------------------------------------------------

#[derive(Default)]
pub struct BunDetector;

impl Detector for BunDetector {
	fn kind(&self) -> KindId { KindId::BUN }
	fn priority(&self) -> i32 { 80 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if any_exists(dir, &["bun.lock", "bun.lockb", "bunfig.toml"], &mut signals) {
			Some(DetectMatch { kind: KindId::BUN, signals })
		} else {
			None
		}
	}
}

// ---- Deno ------------------------------------------------------------------

#[derive(Default)]
pub struct DenoDetector;

impl Detector for DenoDetector {
	fn kind(&self) -> KindId { KindId::DENO }
	fn priority(&self) -> i32 { 70 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if any_exists(dir, &["deno.json", "deno.jsonc", "deno.lock"], &mut signals) {
			Some(DetectMatch { kind: KindId::DENO, signals })
		} else {
			None
		}
	}
}

// ---- Node ------------------------------------------------------------------

#[derive(Default)]
pub struct NodeDetector;

impl Detector for NodeDetector {
	fn kind(&self) -> KindId { KindId::NODE }
	fn priority(&self) -> i32 { 50 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if signal_if_exists(dir, "package.json", &mut signals) {
			Some(DetectMatch { kind: KindId::NODE, signals })
		} else {
			None
		}
	}
}

// ---- Python ----------------------------------------------------------------

#[derive(Default)]
pub struct PythonDetector;

impl Detector for PythonDetector {
	fn kind(&self) -> KindId { KindId::PYTHON }
	fn priority(&self) -> i32 { 40 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if any_exists(
			dir,
			&["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg"],
			&mut signals,
		) {
			Some(DetectMatch { kind: KindId::PYTHON, signals })
		} else {
			None
		}
	}
}

// ---- Lua -------------------------------------------------------------------

#[derive(Default)]
pub struct LuaDetector;

impl Detector for LuaDetector {
	fn kind(&self) -> KindId { KindId::LUA }
	fn priority(&self) -> i32 { 30 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		let luarc = signal_if_exists(dir, ".luarc.json", &mut signals);
		let rockspec = has_extension(dir, "rockspec");
		if rockspec {
			signals.push("*.rockspec".to_string());
		}
		if luarc || rockspec {
			Some(DetectMatch { kind: KindId::LUA, signals })
		} else {
			None
		}
	}
}

// ---- C++ -------------------------------------------------------------------

#[derive(Default)]
pub struct CppDetector;

impl Detector for CppDetector {
	fn kind(&self) -> KindId { KindId::CPP }
	fn priority(&self) -> i32 { 20 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if !signal_if_exists(dir, "CMakeLists.txt", &mut signals) {
			return None;
		}
		if has_extension(dir, "cpp") || has_extension(dir, "cc") || has_extension(dir, "cxx") {
			Some(DetectMatch { kind: KindId::CPP, signals })
		} else {
			None
		}
	}
}

// ---- C ---------------------------------------------------------------------

#[derive(Default)]
pub struct CDetector;

impl Detector for CDetector {
	fn kind(&self) -> KindId { KindId::C }
	fn priority(&self) -> i32 { 10 }
	fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		let mut signals = Vec::new();
		if !signal_if_exists(dir, "CMakeLists.txt", &mut signals) {
			return None;
		}
		// Only match as pure C when there are no C++ sources around — otherwise
		// CppDetector should win (and does, on priority anyway).
		if has_extension(dir, "cpp") || has_extension(dir, "cc") || has_extension(dir, "cxx") {
			return None;
		}
		if has_extension(dir, "c") {
			Some(DetectMatch { kind: KindId::C, signals })
		} else {
			None
		}
	}
}

// ---- Registration helper ---------------------------------------------------

pub fn register_all(registry: &mut DetectorRegistry) {
	registry
		.add(RustDetector)
		.add(BunDetector)
		.add(DenoDetector)
		.add(NodeDetector)
		.add(PythonDetector)
		.add(LuaDetector)
		.add(CppDetector)
		.add(CDetector);
}