diff --git a/README.md b/README.md index b17177b..5cecb95 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ cd src/components && run test # Finds package.json in parent dirs | **Elixir** | mix | | **Swift** | swift (SPM) | | **Zig** | zig | +| **Dart** | flutter → dart | | **Generic** | just → make | Detection is based on lockfiles first (more specific), then manifest files. diff --git a/src/detectors/dart.rs b/src/detectors/dart.rs new file mode 100644 index 0000000..29079ff --- /dev/null +++ b/src/detectors/dart.rs @@ -0,0 +1,199 @@ +// Copyright (C) 2025 Verseles +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +use super::{CommandSupport, CommandValidator, DetectedRunner, Ecosystem}; +use std::fs; +use std::path::Path; +use std::sync::Arc; + +pub const DART_BUILTIN: &[&str] = &[ + "analyze", "compile", "create", "doc", "fix", "format", "info", "pub", "run", "test", +]; + +pub const FLUTTER_BUILTIN: &[&str] = &[ + "analyze", + "assemble", + "attach", + "bash-completion", + "build", + "channel", + "clean", + "config", + "create", + "custom-devices", + "devices", + "doctor", + "downgrade", + "drive", + "emulators", + "gen-l10n", + "install", + "logs", + "pub", + "run", + "screenshot", + "symbolize", + "test", + "upgrade", +]; + +pub struct DartValidator { + pub is_flutter: bool, +} + +impl CommandValidator for DartValidator { + fn supports_command(&self, _working_dir: &Path, command: &str) -> CommandSupport { + if self.is_flutter { + if FLUTTER_BUILTIN.contains(&command) { + return CommandSupport::Supported; + } + } else if DART_BUILTIN.contains(&command) { + return CommandSupport::Supported; + } + + // Custom scripts or anything else might be supported via `dart run` or `flutter run` + CommandSupport::Unknown + } +} + +fn check_is_flutter(pubspec_path: &Path) -> bool { + let content = match fs::read_to_string(pubspec_path) { + Ok(c) => c, + Err(_) => return false, + }; + + let yaml: serde_yaml::Value = match serde_yaml::from_str(&content) { + Ok(v) => v, + Err(_) => return false, + }; + + // Check if dependencies -> flutter exists + if let Some(deps) = yaml.get("dependencies") { + if deps.get("flutter").is_some() { + return true; + } + } + + false +} + +pub fn detect(dir: &Path) -> Vec { + let mut runners = Vec::new(); + let pubspec = dir.join("pubspec.yaml"); + + if pubspec.exists() { + let is_flutter = check_is_flutter(&pubspec); + let validator: Arc = Arc::new(DartValidator { is_flutter }); + + if is_flutter { + runners.push(DetectedRunner::with_validator( + "flutter", + "pubspec.yaml", + Ecosystem::Dart, + 11, + validator, + )); + } else { + runners.push(DetectedRunner::with_validator( + "dart", + "pubspec.yaml", + Ecosystem::Dart, + 11, + validator, + )); + } + } + + runners +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn test_detect_dart() { + let dir = tempdir().unwrap(); + let mut file = File::create(dir.path().join("pubspec.yaml")).unwrap(); + writeln!( + file, + r#" +name: my_dart_project +dependencies: + path: ^1.8.0 +"# + ) + .unwrap(); + + let runners = detect(dir.path()); + assert_eq!(runners.len(), 1); + assert_eq!(runners[0].name, "dart"); + } + + #[test] + fn test_detect_flutter() { + let dir = tempdir().unwrap(); + let mut file = File::create(dir.path().join("pubspec.yaml")).unwrap(); + writeln!( + file, + r#" +name: my_flutter_project +dependencies: + flutter: + sdk: flutter +"# + ) + .unwrap(); + + let runners = detect(dir.path()); + assert_eq!(runners.len(), 1); + assert_eq!(runners[0].name, "flutter"); + } + + #[test] + fn test_dart_validator() { + let validator = DartValidator { is_flutter: false }; + let dir = Path::new(""); + assert_eq!( + validator.supports_command(dir, "run"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir, "test"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir, "unknown_command"), + CommandSupport::Unknown + ); + } + + #[test] + fn test_flutter_validator() { + let validator = DartValidator { is_flutter: true }; + let dir = Path::new(""); + assert_eq!( + validator.supports_command(dir, "run"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir, "build"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir, "unknown_command"), + CommandSupport::Unknown + ); + } +} diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index c252dd5..23cc966 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -10,6 +10,7 @@ // GNU Affero General Public License for more details. pub mod custom; +pub mod dart; pub mod deno; pub mod dotnet; pub mod elixir; @@ -483,6 +484,7 @@ pub enum Ecosystem { Elixir, Swift, Zig, + Dart, Generic, Custom, } @@ -502,6 +504,7 @@ impl Ecosystem { Ecosystem::Elixir => "Elixir", Ecosystem::Swift => "Swift", Ecosystem::Zig => "Zig", + Ecosystem::Dart => "Dart", Ecosystem::Generic => "Generic", Ecosystem::Custom => "Custom", } @@ -532,6 +535,7 @@ pub fn detect_all(dir: &Path, ignore_list: &[String]) -> Vec { add_runners(rust::detect(dir)); // Rust (9) add_runners(php::detect(dir)); // PHP (10) add_runners(just::detect(dir)); // Just (10) + add_runners(dart::detect(dir)); // Dart (11) add_runners(deno::detect(dir)); // Deno (22) add_runners(go::detect(dir)); // Go (11-12) add_runners(ruby::detect(dir)); // Ruby (13-14) diff --git a/src/main.rs b/src/main.rs index ee23013..842f4a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,12 +96,7 @@ fn main() { }; // Search for runners - let search_result = search_runners( - ¤t_dir, - max_levels, - &ignore_list, - verbose, - ); + let search_result = search_runners(¤t_dir, max_levels, &ignore_list, verbose); // Prepare to inject custom commands // Filter empty commands @@ -115,7 +110,7 @@ fn main() { let has_valid_commands = valid_config_commands .as_ref() - .map_or(false, |c| !c.is_empty()); + .is_some_and(|c| !c.is_empty()); let (mut runners, working_dir) = match search_result { Ok(result) => result, @@ -135,7 +130,10 @@ fn main() { if let Some(valid_config_commands) = valid_config_commands { if !valid_config_commands.is_empty() { // Check if we already have a custom runner - if let Some(idx) = runners.iter().position(|r| r.ecosystem == Ecosystem::Custom) { + if let Some(idx) = runners + .iter() + .position(|r| r.ecosystem == Ecosystem::Custom) + { // Merge config commands into existing runner (local overrides global) let mut merged_commands = valid_config_commands.clone(); if let Some(existing_cmds) = &runners[idx].custom_commands {