Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
199 changes: 199 additions & 0 deletions src/detectors/dart.rs
Original file line number Diff line number Diff line change
@@ -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<DetectedRunner> {
let mut runners = Vec::new();
let pubspec = dir.join("pubspec.yaml");

if pubspec.exists() {
let is_flutter = check_is_flutter(&pubspec);
let validator: Arc<dyn CommandValidator> = 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
);
}
}
4 changes: 4 additions & 0 deletions src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -483,6 +484,7 @@ pub enum Ecosystem {
Elixir,
Swift,
Zig,
Dart,
Generic,
Custom,
}
Expand All @@ -502,6 +504,7 @@ impl Ecosystem {
Ecosystem::Elixir => "Elixir",
Ecosystem::Swift => "Swift",
Ecosystem::Zig => "Zig",
Ecosystem::Dart => "Dart",
Ecosystem::Generic => "Generic",
Ecosystem::Custom => "Custom",
}
Expand Down Expand Up @@ -532,6 +535,7 @@ pub fn detect_all(dir: &Path, ignore_list: &[String]) -> Vec<DetectedRunner> {
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)
Expand Down
14 changes: 6 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,7 @@ fn main() {
};

// Search for runners
let search_result = search_runners(
&current_dir,
max_levels,
&ignore_list,
verbose,
);
let search_result = search_runners(&current_dir, max_levels, &ignore_list, verbose);

// Prepare to inject custom commands
// Filter empty commands
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Loading