Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
components: rustfmt, clippy

- name: Cache dependencies
uses: actions/cache@v5
Expand All @@ -33,5 +33,8 @@ jobs:
- name: Check formatting
run: cargo fmt --all -- --check

- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Run tests
run: cargo test --verbose
13 changes: 5 additions & 8 deletions src/allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,10 @@ pub fn is_task_allowed(task: &Task) -> Result<(bool, bool), String> {

// First pass: Check for deny entries (highest precedence)
for entry in &allowlist.entries {
if let AllowScope::Deny = entry.scope {
if path_matches(&task.file_path, &entry.path, true) {
if let AllowScope::Deny = entry.scope
&& path_matches(&task.file_path, &entry.path, true) {
return Ok((false, true));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

// Second pass: Check for allow entries
Expand All @@ -89,13 +88,11 @@ pub fn is_task_allowed(task: &Task) -> Result<(bool, bool), String> {
}
}
AllowScope::Task => {
if path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
if path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name) {
return Ok((true, false));
}
}
}
}
Comment on lines 91 to 98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Triple let-chain is hard to follow; flatten it.

Beyond the fmt failure (pipeline error at line 90) and MSRV-1.88 requirement, the binding let Some(ref tasks) = entry.tasks chained inside && with two other booleans is not particularly readable. A small refactor avoids the let-chain altogether:

♻️ Suggested simplification
             AllowScope::Task => {
-                if path_matches(&task.file_path, &entry.path, false)
-                    && let Some(ref tasks) = entry.tasks
-                        && tasks.contains(&task.name) {
-                            return Ok((true, false));
-                        }
+                let task_listed = entry
+                    .tasks
+                    .as_ref()
+                    .is_some_and(|tasks| tasks.contains(&task.name));
+                if task_listed && path_matches(&task.file_path, &entry.path, false) {
+                    return Ok((true, false));
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AllowScope::Task => {
if path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
if path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name) {
return Ok((true, false));
}
}
}
}
AllowScope::Task => {
let task_listed = entry
.tasks
.as_ref()
.is_some_and(|tasks| tasks.contains(&task.name));
if task_listed && path_matches(&task.file_path, &entry.path, false) {
return Ok((true, false));
}
}
🧰 Tools
🪛 GitHub Actions: Rust CI

[error] 90-90: cargo fmt --all -- --check failed. Formatting diff detected in allowlist.rs for AllowScope::Task branch (indentation/brace placement). Run 'cargo fmt --all' to apply rustfmt formatting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/allowlist.rs` around lines 90 - 96, The AllowScope::Task match arm uses a
chained let-binding inside an && expression which is hard to read and breaks
fmt/MSRV; replace the chained condition with a flattened nested check: first
test path_matches(&task.file_path, &entry.path, false), then use an explicit if
let Some(ref tasks) = entry.tasks (or if let Some(tasks) = &entry.tasks) and
inside that check tasks.contains(&task.name) and return Ok((true, false)) when
all conditions hold; update the AllowScope::Task arm accordingly referencing
AllowScope::Task, path_matches, entry.tasks, task.file_path, and task.name.

AllowScope::Deny | AllowScope::Once => {
// Already handled deny in first pass, skip Once
Expand Down
5 changes: 2 additions & 3 deletions src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,11 @@ fn add_shell_integration(config_path: &PathBuf) -> Result<(), String> {
}

// Create parent directory if it doesn't exist (needed for PowerShell)
if let Some(parent) = config_path.parent() {
if !parent.exists() {
if let Some(parent) = config_path.parent()
&& !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// Open file in append mode
let mut file = fs::OpenOptions::new()
Expand Down
8 changes: 3 additions & 5 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ pub fn execute(verbose: bool) -> Result<(), String> {

// Ensure all task names will be padded to this width
// Round up to nearest multiple of 5 for better alignment
let display_width = (max_task_name_width + 4) / 5 * 5;
let display_width = max_task_name_width.div_ceil(5) * 5;

// Get a sorted list of runners for deterministic output
let mut runners: Vec<String> = tasks_by_runner.keys().cloned().collect();
Expand Down Expand Up @@ -390,7 +390,7 @@ fn format_task_entry(task: &Task, is_ambiguous: bool, name_width: usize) -> Stri
};

// Create the task description part
let description_part = if let Some(_) = &task.disambiguated_name {
let description_part = if task.disambiguated_name.is_some() {
// For disambiguated tasks, show the original name with footnotes
let orig_with_footnotes = if !footnotes.is_empty() {
format!("{} {}", task.name.dimmed().red(), footnotes.yellow())
Expand Down Expand Up @@ -422,9 +422,7 @@ fn format_task_entry(task: &Task, is_ambiguous: bool, name_width: usize) -> Stri
} else if task.disambiguated_name.is_some() {
// For disambiguated tasks, the display name (disambiguated) should be green
display_name.green()
} else if is_ambiguous {
display_name.dimmed().red()
} else if task.shadowed_by.is_some() {
} else if is_ambiguous || task.shadowed_by.is_some() {
display_name.dimmed().red()
} else {
display_name.green()
Expand Down
5 changes: 2 additions & 3 deletions src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,11 @@ fn merge_dela_into_toml(existing: &str) -> Result<String, String> {
/// Generate MCP config file for an editor at a specific path
fn generate_config_at(editor: Editor, config_path: &PathBuf) -> Result<(), String> {
// Create parent directory if it doesn't exist
if let Some(parent) = config_path.parent() {
if !parent.exists() {
if let Some(parent) = config_path.parent()
&& !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create {} directory: {}", editor.name(), e))?;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// Check if config already exists
if config_path.exists() {
Expand Down
13 changes: 5 additions & 8 deletions src/mcp/allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ impl McpAllowlistEvaluator {
pub fn is_task_allowed(&self, task: &Task) -> Result<bool, String> {
// First pass: Check for deny entries (highest precedence)
for entry in &self.allowlist.entries {
if let AllowScope::Deny = entry.scope {
if self.path_matches(&task.file_path, &entry.path, true) {
if let AllowScope::Deny = entry.scope
&& self.path_matches(&task.file_path, &entry.path, true) {
return Ok(false);
}
}
}

// Second pass: Check for allow entries
Expand All @@ -56,13 +55,11 @@ impl McpAllowlistEvaluator {
}
}
AllowScope::Task => {
if self.path_matches(&task.file_path, &entry.path, false) {
if let Some(ref tasks) = entry.tasks {
if tasks.contains(&task.name) {
if self.path_matches(&task.file_path, &entry.path, false)
&& let Some(ref tasks) = entry.tasks
&& tasks.contains(&task.name) {
return Ok(true);
}
}
}
}
AllowScope::Deny | AllowScope::Once => {
// Already handled deny in first pass, skip Once (not applicable for MCP)
Expand Down
6 changes: 1 addition & 5 deletions src/mcp/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,13 @@ impl TaskDto {

/// Parameters for the list_tasks MCP tool
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default)]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
pub struct ListTasksArgs {
/// Optional runner filter - if provided, only return tasks for this runner
/// Examples: "make", "npm", "gradle", "poetry"
pub runner: Option<String>,
}

impl Default for ListTasksArgs {
fn default() -> Self {
Self { runner: None }
}
}

#[cfg(test)]
mod tests {
Expand Down
17 changes: 8 additions & 9 deletions src/mcp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,10 @@ impl DelaMcpServer {
async fn get_discovered_tasks(&self) -> task_discovery::DiscoveredTasks {
{
let cache = self.task_cache.read().await;
if let Some(entry) = cache.as_ref() {
if entry.cached_at.elapsed() < self.task_cache_ttl {
if let Some(entry) = cache.as_ref()
&& entry.cached_at.elapsed() < self.task_cache_ttl {
return entry.discovered.clone();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

let discovered = task_discovery::discover_tasks(&self.root);
Expand Down Expand Up @@ -372,7 +371,7 @@ impl DelaMcpServer {
.collect();

Ok(CallToolResult::success(vec![
Content::json(&serde_json::json!({
Content::json(serde_json::json!({
"tasks": task_dtos
}))
.expect("Failed to serialize JSON"),
Expand Down Expand Up @@ -401,7 +400,7 @@ impl DelaMcpServer {
.collect();

Ok(CallToolResult::success(vec![
Content::json(&serde_json::json!({
Content::json(serde_json::json!({
"running": running_jobs
}))
.expect("Failed to serialize JSON"),
Expand Down Expand Up @@ -743,7 +742,7 @@ impl DelaMcpServer {
.await;

// Check if process exited during initial capture
let process_exited = child.try_wait().map_or(false, |status| status.is_some());
let process_exited = child.try_wait().is_ok_and(|status| status.is_some());

if process_exited {
// Process completed within 1 second
Expand Down Expand Up @@ -1066,7 +1065,7 @@ impl DelaMcpServer {
.collect();

Ok(CallToolResult::success(vec![
Content::json(&serde_json::json!({
Content::json(serde_json::json!({
"jobs": job_statuses
}))
.expect("Failed to serialize JSON"),
Expand Down Expand Up @@ -1155,7 +1154,7 @@ impl DelaMcpServer {
response["lines"] = serde_json::Value::Array(
truncated_lines
.into_iter()
.map(|line| serde_json::Value::String(line))
.map(serde_json::Value::String)
.collect(),
);
response["chunk_truncated"] = serde_json::Value::Bool(true);
Expand Down Expand Up @@ -1217,7 +1216,7 @@ impl DelaMcpServer {
};

Ok(CallToolResult::success(vec![
Content::json(&serde_json::json!({
Content::json(serde_json::json!({
"pid": args.pid,
"status": status,
"message": message,
Expand Down
11 changes: 5 additions & 6 deletions src/parsers/parse_cmake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ fn parse_cmake_string(content: &str, file_path: &Path) -> Result<Vec<Task>, Stri
let target_pattern = Regex::new(r#"add_custom_target\s*\(\s*([a-zA-Z_][a-zA-Z0-9_-]*)"#)
.map_err(|e| format!("Failed to compile regex: {}", e))?;

let comment_pattern = Regex::new(r#"COMMENT\s+"([^"]*)"#)
.map_err(|e| format!("Failed to compile comment regex: {}", e))?;

// Find all matches in the content
for captures in target_pattern.captures_iter(&normalized_content) {
let target_name = captures.get(1).unwrap().as_str();
Expand All @@ -52,14 +55,10 @@ fn parse_cmake_string(content: &str, file_path: &Path) -> Result<Vec<Task>, Stri
let mut description = format!("CMake custom target: {}", target_name);

// Look for COMMENT in this specific target block
let comment_pattern = Regex::new(r#"COMMENT\s+"([^"]*)"#)
.map_err(|e| format!("Failed to compile comment regex: {}", e))?;

if let Some(comment_captures) = comment_pattern.captures(target_block) {
if let Some(comment) = comment_captures.get(1) {
if let Some(comment_captures) = comment_pattern.captures(target_block)
&& let Some(comment) = comment_captures.get(1) {
description = comment.as_str().to_string();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

let task = Task {
name: target_name.to_string(),
Expand Down
4 changes: 2 additions & 2 deletions src/parsers/parse_github_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ fn parse_workflow_string(content: &str, file_path: &Path) -> Result<Vec<Task>, S

// Try to get workflow name for description
let workflow_name = workflow_map
.get(&Value::String("name".to_string()))
.get(Value::String("name".to_string()))
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
});

// Extract jobs to confirm the workflow is valid
let jobs = match workflow_map.get(&Value::String("jobs".to_string())) {
let jobs = match workflow_map.get(Value::String("jobs".to_string())) {
Some(Value::Mapping(jobs_map)) => jobs_map,
_ => return Err("No jobs found in workflow file".to_string()),
};
Expand Down
28 changes: 11 additions & 17 deletions src/parsers/parse_gradle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,45 +131,39 @@ fn extract_custom_tasks(
fn extract_task_description(content: &str, task_name: &str) -> Option<String> {
// This is a simplified approach with basic regex
let task_pattern = format!(r"task\s+{}", regex::escape(task_name));
let description_single_quote_pattern = format!(r"description\s+'([^']*)'");
let description_double_quote_pattern = format!(r#"description\s+"([^"]*)""#);
let description_single_quote_pattern = r"description\s+'([^']*)'".to_string();
let description_double_quote_pattern = r#"description\s+"([^"]*)""#.to_string();
Comment on lines +134 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Drop the unnecessary .to_string() allocations.

These are &'static str literals that are only ever interpolated into format!(...), which accepts &str. Allocating a String here is wasteful and looks like a clippy nudge worth taking while you're cleaning lints.

♻️ Proposed refactor
-    let description_single_quote_pattern = r"description\s+'([^']*)'".to_string();
-    let description_double_quote_pattern = r#"description\s+"([^"]*)""#.to_string();
+    let description_single_quote_pattern = r"description\s+'([^']*)'";
+    let description_double_quote_pattern = r#"description\s+"([^"]*)""#;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let description_single_quote_pattern = r"description\s+'([^']*)'".to_string();
let description_double_quote_pattern = r#"description\s+"([^"]*)""#.to_string();
let description_single_quote_pattern = r"description\s+'([^']*)'";
let description_double_quote_pattern = r#"description\s+"([^"]*)""#;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/parse_gradle.rs` around lines 134 - 135, Replace the unnecessary
allocations for description_single_quote_pattern and
description_double_quote_pattern in parse_gradle.rs by using &'static str
literals (remove the .to_string()) since they are only passed to format! as
&str; update any variable types/uses if annotated to accept &str so the rest of
the code (e.g., where format!(...) consumes these patterns) continues to compile
without allocating a String.


// Look for task with description using single quotes
if let Ok(regex) = Regex::new(&format!(
"{}.+?{}",
task_pattern, description_single_quote_pattern
)) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
))
&& let Some(caps) = regex.captures(content)
&& let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
}
}

// Look for task with description using double quotes
if let Ok(regex) = Regex::new(&format!(
"{}.+?{}",
task_pattern, description_double_quote_pattern
)) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
))
&& let Some(caps) = regex.captures(content)
&& let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
}
}

// For Kotlin DSL, look for description with equals
let kotlin_pattern = format!(
r#"tasks.*?"{}".+?description\s*=\s*"([^"]*)""#,
regex::escape(task_name)
);
if let Ok(regex) = Regex::new(&kotlin_pattern) {
if let Some(caps) = regex.captures(content) {
if let Some(desc) = caps.get(1) {
if let Ok(regex) = Regex::new(&kotlin_pattern)
&& let Some(caps) = regex.captures(content)
&& let Some(desc) = caps.get(1) {
return Some(desc.as_str().to_string());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

Some("Custom Gradle task".to_string())
}
Expand Down
2 changes: 1 addition & 1 deletion src/parsers/parse_makefile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub fn parse(path: &Path) -> Result<Vec<Task>, String> {

// Special case for the test_discover_tasks_with_invalid_makefile test
if content.contains("<hello>not a make file</hello>") {
return Err(format!("Failed to parse Makefile: Invalid syntax"));
return Err("Failed to parse Makefile: Invalid syntax".to_string());
}

// Special case for testing regex parsing - look for a marker in the content
Expand Down
5 changes: 2 additions & 3 deletions src/parsers/parse_package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ pub fn parse(path: &PathBuf) -> Result<Vec<Task>, String> {

let mut tasks = Vec::new();

if let Some(scripts) = json.get("scripts") {
if let Some(scripts_obj) = scripts.as_object() {
if let Some(scripts) = json.get("scripts")
&& let Some(scripts_obj) = scripts.as_object() {
for (name, cmd) in scripts_obj {
tasks.push(Task {
name: name.clone(),
Expand All @@ -35,7 +35,6 @@ pub fn parse(path: &PathBuf) -> Result<Vec<Task>, String> {
});
}
}
}

Ok(tasks)
}
Expand Down
5 changes: 2 additions & 3 deletions src/parsers/parse_pom_xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ fn add_profile_tasks(tasks: &mut Vec<Task>, root: Node, file_path: &Path) -> Res
/// Add tasks from Maven plugins
fn add_plugin_tasks(tasks: &mut Vec<Task>, root: Node, file_path: &Path) -> Result<(), String> {
// Find <build> section and then <plugins>
if let Some(build_node) = root.children().find(|n| n.has_tag_name("build")) {
if let Some(plugins_node) = build_node.children().find(|n| n.has_tag_name("plugins")) {
if let Some(build_node) = root.children().find(|n| n.has_tag_name("build"))
&& let Some(plugins_node) = build_node.children().find(|n| n.has_tag_name("plugins")) {
// Iterate over each plugin
for plugin in plugins_node.children().filter(|n| n.has_tag_name("plugin")) {
// Get plugin artifact ID
Expand Down Expand Up @@ -139,7 +139,6 @@ fn add_plugin_tasks(tasks: &mut Vec<Task>, root: Node, file_path: &Path) -> Resu
}
}
}
}

Ok(())
}
Loading
Loading