rrubocop 0.8.15

10x faster RuboCop drop-in. Same output format and configs, without maintaining RuboCop gems.
//! Rails/RakeEnvironment — rake tasks should depend on `:environment`.

use tree_sitter::Node;

use crate::cop::shared::{argument_nodes, call_method_name, call_receiver, node_bytes};
use crate::cop::{Cop, CopConfig};
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;

pub struct RakeEnvironment;

impl Cop for RakeEnvironment {
    fn name(&self) -> &'static str {
        "Rails/RakeEnvironment"
    }

    fn default_include(&self) -> &'static [&'static str] {
        &["**/Rakefile", "**/*.rake"]
    }

    fn default_exclude(&self) -> &'static [&'static str] {
        &["**/lib/capistrano/tasks/**/*.rake"]
    }

    fn interested_node_kinds(&self) -> &'static [&'static str] {
        &["call", "command", "command_call"]
    }

    fn interested_call_names(&self) -> &'static [&'static [u8]] {
        &[b"task"]
    }

    fn check_node(
        &self,
        source: &SourceFile,
        node: Node<'_>,
        _config: &CopConfig,
        diagnostics: &mut Vec<Diagnostic>,
        _corrections: Option<&mut Vec<crate::correction::Correction>>,
    ) {
        if call_receiver(node).is_some() {
            return;
        }
        if call_method_name(source, node) != Some(b"task") {
            return;
        }
        if !has_task_block(node) {
            return;
        }
        if task_name_is_default(source, node) {
            return;
        }
        if with_dependencies(node) {
            return;
        }
        let (line, column) = source.offset_to_line_col(node.start_byte());
        diagnostics.push(self.diagnostic(
            source,
            line,
            column,
            "Include `:environment` task as a dependency for all Rake tasks.".into(),
        ));
    }
}

fn has_task_block(node: Node<'_>) -> bool {
    if node.child_by_field_name("block").is_some() {
        return true;
    }
    let mut cur = node.walk();
    node.named_children(&mut cur)
        .any(|c| matches!(c.kind(), "block" | "do_block"))
}

fn task_name_is_default(source: &SourceFile, task: Node<'_>) -> bool {
    let Some(first) = argument_nodes(task).into_iter().next() else {
        return false;
    };
    match first.kind() {
        "simple_symbol" | "symbol" | "string" | "string_content" => {
            symbol_or_string_is_default(source, first)
        }
        "hash" => hash_key_is_default(source, first),
        "pair" => pair_key_is_default(source, first),
        _ => false,
    }
}

fn symbol_or_string_is_default(source: &SourceFile, node: Node<'_>) -> bool {
    let b = node_bytes(source, node);
    let name = b.strip_prefix(b":").unwrap_or(b);
    let name = name
        .strip_prefix(b"'")
        .or_else(|| name.strip_prefix(b"\""))
        .unwrap_or(name);
    let name = name
        .strip_suffix(b"'")
        .or_else(|| name.strip_suffix(b"\""))
        .unwrap_or(name);
    name == b"default"
}

fn pair_key_is_default(source: &SourceFile, pair: Node<'_>) -> bool {
    let Some(key) = pair.child_by_field_name("key").or_else(|| {
        let mut c = pair.walk();
        pair.named_children(&mut c).next()
    }) else {
        return false;
    };
    let b = node_bytes(source, key);
    let name = b.strip_prefix(b":").unwrap_or(b);
    name.strip_suffix(b":").unwrap_or(name) == b"default"
        || symbol_or_string_is_default(source, key)
}

fn hash_key_is_default(source: &SourceFile, hash: Node<'_>) -> bool {
    let mut cur = hash.walk();
    let pairs: Vec<_> = hash
        .named_children(&mut cur)
        .filter(|n| n.kind() == "pair")
        .collect();
    if pairs.len() != 1 {
        return false;
    }
    pair_key_is_default(source, pairs[0])
}

fn with_dependencies(node: Node<'_>) -> bool {
    let args = argument_nodes(node);
    let Some(first) = args.first() else {
        return false;
    };
    if is_hash_arg(*first) {
        return hash_style_deps(*first);
    }
    if let Some(second) = args.get(1) {
        if is_hash_arg(*second) {
            return hash_style_deps(*second);
        }
    }
    false
}

fn is_hash_arg(node: Node<'_>) -> bool {
    matches!(node.kind(), "hash" | "pair")
}

fn hash_style_deps(node: Node<'_>) -> bool {
    let Some(value) = first_pair_value(node) else {
        return false;
    };
    if value.kind() == "array" {
        let mut c = value.walk();
        return value.named_children(&mut c).next().is_some();
    }
    true
}

fn first_pair_value(node: Node<'_>) -> Option<Node<'_>> {
    let pair = if node.kind() == "pair" {
        node
    } else {
        let mut cur = node.walk();
        node.named_children(&mut cur).find(|n| n.kind() == "pair")?
    };
    pair.child_by_field_name("value").or_else(|| {
        let mut c = pair.walk();
        pair.named_children(&mut c).nth(1)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    crate::cop_fixture_tests!(RakeEnvironment, "cops/rails/rake_environment");
}